Compare commits

...

533 Commits

Author SHA1 Message Date
fca30f8522 Merged dev into feature/workflows-support 2025-07-18 11:13:39 -05:00
b2861b1567 Fix parsing of joins in query posts for api middleware v1 2025-07-17 15:00:07 -05:00
ac94ca625d Add secondary sort (QFieldMetaData::getName) to execute method, for increased stability 2025-07-17 11:56:05 -05:00
646d20de6f Merge pull request #190 from Kingsrook/feature/cleanup-deprecated-calls
Feature/cleanup deprecated calls
2025-07-15 11:14:03 -05:00
928f4a2543 Removed assertions about logs that have been removed 2025-07-15 10:33:51 -05:00
b78519aa55 add toYamlCustomized / toJsonCustomized methods, that expose jackson's now-preferred Builder objects to be configured on instead of doing config directly on mapper objects. 2025-07-15 08:48:57 -05:00
01f1e074e2 Add @SuppressWarnings("deprecation") for usage of metaDataFilter (as framework still supports it, despite it being deprecated, so we don't want warnings about it in here) 2025-07-15 08:30:07 -05:00
2548725be2 Fix whitespace 2025-07-15 08:29:28 -05:00
dd67ed317c Replace deprecated backend.setVariantOptions properties with VariantsConfig 2025-07-15 08:29:10 -05:00
f7ca485e6e Replace deprecated IOUtils.readLines call 2025-07-15 08:06:33 -05:00
cdfc58adc6 Replaced deprecated withIn and withType methods that took strings in favor of ones that take enum constants 2025-07-14 20:40:18 -05:00
06ed66f640 Update al lURLEncoder.encode calls to use StandardCharsets.UTF_8 (some were Charset.defaultCharset, some were deprecated single-arg) 2025-07-14 20:19:54 -05:00
80e1d8143c Replace QProcessMetaData.addStep and addOptionalStep with .withStep and .withOptionalStep 2025-07-14 20:03:29 -05:00
78b893616b Merged dev into feature/workflows-support 2025-07-14 19:44:38 -05:00
d311e28e60 Revert "Rewrite deploy workflow to also be split up"
This reverts commit abfdf09b32.
2025-07-14 17:10:30 -05:00
fc4b34306f Remove duplicated sshd-sftp dep 2025-07-14 17:07:30 -05:00
abfdf09b32 Rewrite deploy workflow to also be split up 2025-07-14 17:01:15 -05:00
104dd2fff8 Initial commit (previously missed) 2025-07-14 16:39:25 -05:00
6ced3cbd61 Merge pull request #189 from Kingsrook/feature/circleci-improvements
minor rewrite of circleci config (mostly done by cursor).  Highlights:

- by default, mvn builds will now not output all System.out, err, logs, and stack traces (though runs in the IDE still will, and it can be opted in to via -DtestOutputToFile=false)
- in circleci, the test output files get concatted together and uploaded as artifacts, if they need reviewed.
- fixed jacoco summary stats & uncovered class reporting in circleci, w/o any external deps (e.g., to speed up builds)
- removed an explicit install of java17, since our executor already had it (!) - should speed up build ~a minute
- updated to mvn build in one step, so its loud output is segregated from test outputs (which are more useful now that it's just junit reporting success/fail, not all logs from our code)
- update to run check-middleware-api-versions in parallel with mvn verify, in theory should speed up some too.
2025-07-14 16:35:20 -05:00
384195a2c3 Update possibleValuesStandalone to accept filter as formParam. 2025-07-14 16:33:42 -05:00
79bc7dfecd Refactor new method setValueFromApiFieldInQRecord out of apiJsonObjectToQRecord for smaller use-case of similar nature 2025-07-14 16:33:42 -05:00
fc197efd74 Add method buildCrossProduct to help build cross products 2025-07-14 16:33:42 -05:00
ec697d7f0f In enrichPossibleValueSource, call getMethod instead of getDeclaredMethod, for finding the id-type of a custom-typed PVS 2025-07-14 15:56:55 -05:00
db23469c8a Pass possibleValueSourceFilter through to frontend 2025-07-14 15:56:22 -05:00
a3c5410897 Add property omitExposedJoins 2025-07-14 15:56:13 -05:00
2aa2e4643e Add method identifyJoinTablesInFilter 2025-07-14 15:55:56 -05:00
d7bb54eb16 Add QHelpContentPlugin, so that supplemental instance meta data can accept help content. also add commonmark dep and getContentAsHtml method 2025-07-08 10:49:19 -05:00
eb0c57e9a6 Add processing of subFilters to interpretValues 2025-07-07 09:44:41 -05:00
8531feef70 Add script to collect JaCoCo reports and update CircleCI config to use it 2025-07-03 15:41:09 -05:00
0aaf184962 move concatenate-test-output to its own script; switch back to verify, so we get jacoco reports; switch to no-tranffer-progress from batch-mode, so we still get color output 2025-07-03 15:24:54 -05:00
14a3dad3c8 Refactor CircleCI config to run tests and API version checks in parallel
- Split mvn_test job into separate build, test, and api_version_check jobs
- Run test and api_version_check in parallel after build completes
- Both parallel jobs use compiled outputs from build via Maven cache
- Remove unnecessary check_java_version steps
- Remove unused test-logs artifact storage
- Add jacoco:report to generate coverage reports
- Maintain sequential execution in mvn_deploy job
2025-07-03 15:13:15 -05:00
7b2b181427 split mvn verify into mvn build (which will have lots of output about downloading deps) and mvn test, to run tests (moving to test goal instead of verify, since we don't have any int-tests that would run in a verify, and we don't really need to do a packge) 2025-07-03 15:03:25 -05:00
171c73c4e4 Update CircleCI configuration to use absolute paths for test output artifacts 2025-07-03 14:49:17 -05:00
fc6f7b084d Enhance Jacoco coverage summary output to include module name and improve readability with additional separators. 2025-07-03 14:48:52 -05:00
04e64b04ab test output updates:
- by default, make tests put all their output into files (under target/surefire-reports/) - with system property -DtestOutputToFile=false to get all output on console;
- have circleci store that output as artifacts;
- run mvn in 'batch mode' in circleci, for quieter output (no download progress, no color codes)
2025-07-03 13:08:53 -05:00
4788faae7d Mark as Serializable 2025-07-03 08:10:42 -05:00
3183dd028f Add protection against ConcurrentModificationException when processing QSupplementalInstanceMetaData - for the case where enriching one adds another 2025-07-03 08:10:42 -05:00
ff1cf81315 Switch from testing for QBitComponentMetaDataProducer to use QBitComponentMetaDataProducerInterface instead 2025-07-03 08:10:42 -05:00
946e7d418b Add method get(Class,String) 2025-07-03 08:10:42 -05:00
f97a3d5097 Pass transaction through from insert/update/delete actions through DMLAuditAction into AuditAction 2025-07-03 08:10:42 -05:00
fa80daa778 Add method addAllIfNotNull 2025-07-03 07:57:37 -05:00
58dbcfd42b manual rewrite of jacoco summary reporting shell lines 2025-07-02 13:06:20 -05:00
779dfd25d0 Cursor rewrite of store-artifacts/jacoco-site 2025-07-02 13:03:06 -05:00
eca359cf69 Try not manually installing java 17, and parse jacoco outputs more directly (per Cursor) 2025-07-02 12:32:37 -05:00
b5134cd0c6 Update ApiQueryFilterUtils.manageCriteriaFields with basic support filtering by an exposed join. 2025-07-02 08:50:16 -05:00
b3f5f6bfc1 Merged feature/webhooks into feature/workflows-support 2025-06-20 08:02:12 -05:00
d7867b8d22 replace all relative program paths (e.g., cp) with absolute ones (e.g., /bin/cp), in constants (e.g., CP); 2025-06-19 14:49:07 -05:00
96217c839d Fixed test (was a copy-paste job, hadn't been finished) and fixed to filter tables in the query method 2025-06-18 16:37:27 -05:00
5c02c1fd2e Add action flags to allow sync-scheduled job processes to be omitted. 2025-06-18 15:50:40 -05:00
9b2c281431 Change the QQQTable PVS to be custom type, with permissions applied to the list of tables you see. 2025-06-18 15:50:40 -05:00
5327424cec Add QException to some methods in here. 2025-06-18 15:50:40 -05:00
4fd68f9195 Initial checkin 2025-06-18 15:37:15 -05:00
e1eb07697a Merge pull request #186 from Kingsrook/feature/search-possible-values-by-label
added labels as a param when searching for PVSs
2025-06-18 10:21:36 -05:00
cb6101d0ed Add action flags to insert, update, delete inputs 2025-06-16 09:43:34 -05:00
46bca6efb9 Merge pull request #184 from Kingsrook/183-javalin-server-fails-to-start-when-using-static-files-in-a-production-jar
Fixed loading static files from FS or Jars
2025-06-15 11:04:52 -05:00
f6859d040f Refactored to use the constructor instead of the class/static method to load properties - makes unit test runtime cleaning 2025-06-15 10:36:11 -05:00
d13fc4a863 Removed - Merged back into overall unit tests 2025-06-15 10:35:18 -05:00
eab87b9d80 Added missing jar for unit test 2025-06-15 10:01:11 -05:00
707400a8b2 Added support for loading static files from the filesystem as as from jars (based on a system property) 2025-06-14 16:07:51 -05:00
55e372a70f Increase rdbms assessor coverage; decrease its usage of stdout 2025-06-13 20:07:48 -05:00
7b190d810a Actually return (don't just log) if no scheduledJobs table in instance 2025-06-13 15:39:12 -05:00
1fb509fea1 Prevent multiple copies of enrichment & validation plugins; actually implement QSupplementalInstanceMetaData enrichment 2025-06-13 15:36:31 -05:00
786f9ba8df Add method allowedToReadRecord.
update some methods in here to take session as parameter;
2025-06-13 11:52:13 -05:00
55905d251d Better clone methods 2025-06-13 11:50:21 -05:00
d23dbac0d9 Fix assessor test that expects empty database 2025-06-13 09:00:48 -05:00
962d09b120 Add basic test for RDBMS Assessor; change h2 to not upshift all names (and backout some places where we'd previously worked around that) 2025-06-13 08:43:16 -05:00
4827669c0a Add missing 'extends BaseTest' 2025-06-13 08:02:04 -05:00
6efc34b69e Checkstyle 2025-06-12 20:45:37 -05:00
da52fccc86 Initial version of QInstanceAssessor - to compare rdbms based meta-data to the actual database. 2025-06-12 20:31:24 -05:00
efc69fee4b Initial checkin 2025-06-12 20:31:24 -05:00
1808cea5c0 Update processBasedRouters to use different handlers for processing the javalin context - with a new default implementation that makes available the request body as a string 2025-06-12 20:31:24 -05:00
a7b5e00e27 Make constants out of API_NAME_PVS_NAME and API_VERSION_PVS_NAME 2025-06-12 20:31:24 -05:00
685e747a91 Add log method 2025-06-12 20:31:24 -05:00
3d6f05e4ea avoid NPE on empty contennts 2025-06-12 20:31:24 -05:00
97883b3e43 Initial checkin 2025-06-12 20:31:24 -05:00
e11a23ccc0 Make sortMetaDataProducers a public method (qbit producer can use it); add childJoin().isOneToOne 2025-06-12 20:31:24 -05:00
12383930b0 Try to make sure values that this backend stores are of the appropriate field types. 2025-06-12 20:31:24 -05:00
cc19268132 New version of interface for QBitMetaData production 2025-06-12 20:31:24 -05:00
4883514f58 Add getDefaultBackendNameForTables 2025-06-12 20:31:24 -05:00
2ee26b14a9 Add a null check for table fields (since instance isn't validated yet) 2025-06-12 20:31:24 -05:00
e9e029d8e9 Add setRecordLinksToRecordsFromTableDynamicForPostQuery 2025-06-12 20:31:24 -05:00
ff4a0b8849 Initial checkin 2025-06-12 20:31:24 -05:00
7089ec92a6 Add instance-level pre- and post- insert and update table customizers 2025-06-12 20:31:24 -05:00
60c5c11549 Add support for one-to-one joins;
Add support for tables that aren't yet in the QInstance, but instead is in the QBitProductionContext's metadataProducerMultiOutputStack
2025-06-12 15:20:38 -05:00
3c765e9e47 Add support for one-to-one joins;
Add support for tables that aren't yet in the QInstance, but instead is in the QBitProductionContext's metadataProducerMultiOutputStack
2025-06-12 15:19:10 -05:00
5db8cf9ca1 Initial checkin of process & table customizer to help sync scheduled jobs for records in a table 2025-06-12 15:11:14 -05:00
ffca465f04 Add option to specify Comparator, for custom sorting of options [skip ci] 2025-06-05 10:59:48 -05:00
44e091a1bc Avoid NPE on empty input record 2025-06-02 14:48:59 -05:00
369d501071 Add doCheckTableApiVersion, so apps can avoid new error if table version isn't in api 2025-06-02 12:02:07 -05:00
c364b2c0be Checkstyle (default in switch) 2025-05-30 20:48:55 -05:00
00afad43bc Move QueryExecutorUtils to ApiQueryFilterUtils 2025-05-30 20:35:36 -05:00
3888aab490 Initial checkin 2025-05-30 20:32:28 -05:00
8b6aad5412 Avoid an NPE if a table doesn't have fields 2025-05-30 20:26:11 -05:00
90cc5a32ac Extract an interface from QBitComponentMetaDataProducer (which itself is still useful as a base class, so you don't have to implement get/setQBitConfig yourself), for implementations that already have a different base class. 2025-05-30 20:25:53 -05:00
395f94081f try again for stable map of properties 2025-05-29 15:27:25 -05:00
7a450eed08 Rebuild with tableMetaData example, and now stable content map for query & count 2025-05-29 12:32:20 -05:00
7b3cafe348 Test fixes 2025-05-28 08:54:34 -05:00
1c4f917939 Test fixes 2025-05-28 08:35:20 -05:00
e48d5e0965 added labels as a param when searching for PVSs 2025-05-27 17:05:37 -05:00
437448fd81 Add tryIgnore, tryCatch methods 2025-05-27 16:44:45 -05:00
d63f13bb55 Avoid a null pointer if criteria has no values 2025-05-27 16:43:13 -05:00
add41a852b Minor cleanups wrapping up app-api-version in middleware api 2025-05-27 16:42:39 -05:00
6314ad310b Use same example qinstance in meta-data and table-meta-data specs 2025-05-27 16:42:01 -05:00
35c6a13ce3 Update to make test code here available in qqq-middleware-api module 2025-05-27 16:37:54 -05:00
afd8084d45 Initial build of api aware middleware 2025-05-27 16:37:30 -05:00
23e9ac5b61 Support queryJoins and distinctCount in memory count action 2025-05-27 11:34:04 -05:00
2a76736474 Introduce QueryOrCountInputInterface 2025-05-27 11:33:45 -05:00
4b9e8e0c51 Add method to get all plugins (helpful if you want to reset them) 2025-05-27 11:33:08 -05:00
8b6f4b635a Add support for "otherValues" in possibleValuesStandalone 2025-05-27 11:31:30 -05:00
a4b1a8858d Move handleQueryNullLimit to utils class 2025-05-27 11:30:38 -05:00
78eb315558 initial build of table meta-data, query, and count specs, IO, executors 2025-05-27 11:29:54 -05:00
83684d8f2e make version for path come from the middleware spec, in support of application-versions in addition to middleware versions 2025-05-27 11:24:56 -05:00
13ef0dfdb8 Support Long values 2025-05-27 11:24:11 -05:00
6ae30f4d65 Add getFormParam and getQueryParam methods, for common use-case of single-value 2025-05-27 11:24:02 -05:00
13189f5855 Add TableMetaDataSpecV1 and TableQuerySpecV1 2025-05-23 16:42:43 -05:00
e89c837690 Update to make example instance statically (since for an api-aware middleware, this code might end up running multiple times) 2025-05-23 16:42:29 -05:00
3b1178a7c6 Update to wrap either a QFrontendFieldMetaData, or a (full) QFieldMetaData 2025-05-23 16:41:37 -05:00
565a85afd7 Add setTableVariantInSession 2025-05-23 16:40:28 -05:00
ce4cc37586 Updates per changes in base class 2025-05-23 16:40:04 -05:00
828a0ed594 add method getVersionBasePath()
add method preExecute()
2025-05-23 16:21:08 -05:00
09d8aac77f provide default implementation in handleOutput;
update newObjectFromTypeArgument to support class hierarchies of depth > 1
add AbstractMiddlewareVersion as argument to some methods
add pre-execute method
add getRequestBodyAsJsonObject
2025-05-23 15:38:04 -05:00
b84406d8ef Add support for executing table triggers beyond what the core table provides (scripts) via custom plugins (adding for workflows qbit)
also move RecordAutomationHandler to an interface (RecordAutomationHandlerInterface)
2025-05-23 15:27:50 -05:00
ca33b28f7a Add Map of "otherValues" 2025-05-23 15:03:30 -05:00
2634773b7f Improvements to UX for table triggers (require appropriate fields, only allow tables that have automations) 2025-05-23 15:02:31 -05:00
5754c940af Add validation and enrichment for QSupplementalFieldMetaData, plus option to include in frontendMetaData 2025-05-23 12:16:40 -05:00
ff7abe89f5 Add overload constructor that takes al ist of field names (e.g., in support of api-versioning) 2025-05-23 12:15:45 -05:00
1eb8b0c560 Add isHidden, and supplementalFieldMetaData 2025-05-23 12:15:20 -05:00
b16d18b8cf Add support for running in an api-Versioned manner 2025-05-23 12:14:50 -05:00
10fe644e60 Add support for omitFieldNames - by default, the join field, but also programmer-configurable. 2025-05-23 12:14:26 -05:00
37463c7676 Set tableName in toQRecord() 2025-05-23 12:05:34 -05:00
9877838c7f Add supplementalInstanceMetaData 2025-05-23 12:03:52 -05:00
802d367b94 Change to fetch the variant record - helps w/ input values being in other types, and, verifies the record exists! 2025-05-23 12:02:56 -05:00
63b956a032 Change a few exceptions to user-facing in here 2025-05-23 12:00:59 -05:00
5ad6354e15 Add isClassAvailable method (idea being to support optional maven dependencies!) 2025-05-23 12:00:36 -05:00
75fc016a4b Add support for variants to memory backend 2025-05-23 11:30:52 -05:00
5daa221ac9 Use getValueString, not cast to string. 2025-05-23 11:11:18 -05:00
89eec41ac0 Add method setDisplayValuesInRecordsIncludingPossibleValueTranslations 2025-05-23 11:06:30 -05:00
18232d5e80 Add transaction to AggregateInput and CountInput 2025-05-23 11:04:29 -05:00
ed91d3fdbe Merged dev into feature/workflows-support 2025-05-19 15:59:24 -05:00
dfb584b367 Updating to 0.26.0 2025-05-19 15:20:47 -05:00
504c53b108 Merge tag 'version-0.25.0' into dev
Tag release
2025-05-19 15:20:43 -05:00
60096dde93 Merge branch 'rel/0.25.0' 2025-05-19 15:17:15 -05:00
3395ee2146 Update for next development version 2025-05-19 15:05:04 -05:00
9949e96832 Update versions for release 2025-05-19 15:05:02 -05:00
868dcf00d7 Merged feature/string-utils-safe-equals-ignore-case into dev 2025-05-19 14:56:39 -05:00
ed6825ff05 Remove some tests that were from copy-pate 2025-05-19 14:56:26 -05:00
e33033fb05 Merged feature/qrun-support-20250313 into dev 2025-05-19 14:48:37 -05:00
32fde00b96 updates to check versions on process query params 2025-05-15 12:56:10 -05:00
2491523a6b added more whitespace behaviors (trims) 2025-05-13 10:15:41 -05:00
6d0f5d4fb3 Merge branch 'dev' into feature/string-utils-safe-equals-ignore-case 2025-05-12 15:47:09 -05:00
bc76a7f66f added whitespace behavior and test 2025-05-12 14:49:52 -05:00
5045627b18 Add initial version of javalin documentation 2025-05-12 09:17:11 -05:00
af4dd2a771 Updated to decide which javalinMetaData to use (either from this object or the QInstance) 2025-05-12 09:16:52 -05:00
595190fd8f Greatly simplified 2025-05-12 09:16:19 -05:00
b8191927e8 Remove zombie code 2025-05-11 20:33:07 -05:00
182ffe2939 Add overload of writeEnvFromSecretsWithNamePrefix w/ option to quoteValues (defaults to true, since that's what new dotenv wants) 2025-05-09 10:29:21 -05:00
7e2451dbe9 Change overrideIsEditable to be null by default (so it only actually overrides if you set a value 2025-05-08 14:47:47 -05:00
ce2ca3f413 Option to useSynchronizedCollections in RecordLookupHelper 2025-05-05 14:11:04 -05:00
625ed5209c switch InMemoryStateProvider to use synchronizedMap, to avoid ConcurrentModificationException in clean method 2025-05-05 10:59:12 -05:00
e603818c69 Merged dev into feature/qrun-support-20250313 2025-05-03 20:07:49 -05:00
f2842ba4d9 Add properties: hideSortBy, overrideIsEditable 2025-05-03 19:51:50 -05:00
1d7777076e Allow orderBy to come from widgetMetaData defaultValues 2025-05-03 19:50:33 -05:00
fa4cf8ca16 Merged feature/sftp-import-support into dev 2025-04-30 09:17:18 -05:00
e58190f15d removed unnecessary sop 2025-04-29 15:42:24 -05:00
be16d5f0cf Checkstyle! 2025-04-25 16:13:17 -05:00
e5987238e6 Add primary keys to process summary lines (and thus traces) for bulk load; better handling of errors and warnings also from bulk insert result step 2025-04-25 16:05:54 -05:00
f81b257dd4 Improving process traces built by bulk load 2025-04-21 10:58:56 -05:00
97434ebb66 Initial checkin of BasicCustomPossibleValueProvider, and migrate TablesCustomPossibleValueProvider to use it. 2025-04-18 13:57:59 -05:00
1b9d93e924 Add CUSTOM_COMPONENT widget type 2025-04-18 13:57:59 -05:00
78892b3642 Fix to allow html entities by going through a w3c DOM 2025-04-18 13:57:59 -05:00
64a405cbf8 Merge pull request #176 from Kingsrook/feature/string-utils-safe-equals-ignore-case
Feature/string utils safe equals ignore case
2025-04-17 15:34:06 -05:00
2d89dafdc1 added test cases 2025-04-15 20:09:00 -05:00
28b608c814 added utils method to do equals ignoring case safely 2025-04-15 20:03:17 -05:00
9056be056e Move scopes from hard-coded to meta-data 2025-04-10 14:50:32 -05:00
a4ffe815b5 Merged feature/filesystem-list-single-file-optimization into dev 2025-04-09 11:22:14 -05:00
3f75add3ed added non-ascii to ascii library, timer pretty print 2025-04-08 18:01:43 -05:00
6f1e9413f6 Update for use-case of Get - listing a single file - to pass that file name in, to avoid listing huge directory when not needed 2025-04-08 13:35:08 -05:00
af51641d2a And fixed a test 2025-04-05 20:51:46 -05:00
17eab1f3d4 Increase tests on ProcessBasedRouter (which of course led to some improvements!) 2025-04-05 20:45:57 -05:00
2cd96fd4bc Set output session Uuid to input uuid, in buildQSessionFromUuid 2025-04-05 19:56:51 -05:00
73aaee1960 Add call to prime test database to server startup 2025-04-05 19:40:11 -05:00
fd13b00793 Update setupSession to use sessionUUID, not idReference, in sending cookie back 2025-04-05 19:39:41 -05:00
64278e674b Merged feature/dk-misc-20250327 into dev 2025-04-03 14:24:52 -05:00
2fa829658f Merged feature/s3-table-set-content-type-on-insert into dev 2025-04-03 14:24:37 -05:00
8f751d81fe Merged feature/fix-s3-glob-pattern-bad-chars into dev 2025-04-03 14:24:27 -05:00
d42b67582a Merged feature/api-request-updates into dev 2025-04-03 14:24:06 -05:00
942134b4b0 it didn't like default as part of a case, so, moved 2025-04-01 16:52:35 -05:00
aca8436c56 Checkstyle 2025-04-01 16:45:25 -05:00
94631585ee Update for s3 tables, to allow setting content-type in aws when inserting records (files) based on file name, hard-coded value, or another field.
this involved adding table & record params to writeFile method - a @Deprecated wrapper w/o those args is provided for backward compatibility
2025-04-01 15:50:16 -05:00
96c539b323 Update content field to be 12 grid columns [skip ci] 2025-04-01 11:51:48 -05:00
235cf9e16c Bugfix for s3 utils listObjectsInBucketMatchingGlob, for file names with chars that need URL Encoding (since we're using a pathMatcher class and file:/// URIs...) update test setup to have a file that triggered this error before the fix. 2025-04-01 11:09:35 -05:00
9cf25ed45c codereview feedback 2025-03-28 16:47:06 -05:00
473cc9c0ae turned down some logging, moved getQHttpResponse into its own method in base api action utils, added override constructer to response to read bytes 2025-03-28 16:12:45 -05:00
d733ce9566 Merged dev into feature/dk-misc-20250327 2025-03-27 12:08:00 -05:00
491998ec9a Merged feature/dk-misc-20250318 into dev 2025-03-27 12:04:21 -05:00
86997528bb Merge pull request #166 from Kingsrook/feature/banners
Initial checkin of Banners under QBrandingMetaData
2025-03-27 12:03:15 -05:00
ebd9dc9c2c Add methods to work with associated records from the mainRecord 2025-03-27 11:57:37 -05:00
12e194fc2e Update all getValueXYZ methods to go through getValue method, so that subclasses behave more as expected 2025-03-27 11:57:09 -05:00
55d046cd86 Fix handling of defaultValue() in annotation 2025-03-27 11:56:00 -05:00
16cedfeb6e Update ConvertHtmlToPdfAction to use openhtmltopdf instead of flying-saucer-pdf-openpdf (gaining support for min/max-width/height 2025-03-27 11:55:36 -05:00
d0508c2568 Merge pull request #167 from Kingsrook/feature/loggly-updates-220250325
turned down some loggly messages, added utility method to value utils
2025-03-25 13:04:32 -05:00
7af23e52d6 feedback from code review 2025-03-25 12:16:48 -05:00
133e507c93 put back root log level 2025-03-25 11:23:58 -05:00
513c8f2efb turned down some loggly messages, added utility method to value utils 2025-03-25 10:08:54 -05:00
2016d0a448 Try to turn off debug logs from apache http 2025-03-24 19:53:07 -05:00
1c54a9a8ac Add 'RedirectState' table (used by oauth2 login flow); change userSession table from memory to rdbms backend 2025-03-24 19:36:41 -05:00
a95650a0ce Checkstyle 2025-03-24 19:33:29 -05:00
410175a133 checkpoint on oauth for static site
- store state + redirectUri in a table
- redirect again to get code & state out of query string
- add meta-data validation to oauth2 module
2025-03-24 09:25:53 -05:00
8f0d117b13 Checkstyle! 2025-03-19 16:51:41 -05:00
916c8c3ba6 Add support for orderBys on child-joins 2025-03-19 16:43:50 -05:00
aca199e91e Deprecated methods that take unused AbstractActionInput 2025-03-19 16:43:03 -05:00
4acc185698 Add org.apache.http Logger level of INFO; inline all empty Logger xml elements 2025-03-18 11:38:38 -05:00
d033d3f464 Add QCodeReferenceWithProperties and InitializableViaCodeReference; also, refactor QCodeLoader to eliminate most of the specialized methods - in favor of generally using getAdHoc (now that just needs a better name, lol) 2025-03-18 11:37:23 -05:00
ae4e269b88 Add static getTableName(Class) and instance.tableName() methods. 2025-03-18 10:48:15 -05:00
38cdb94876 Include process min/max input record attributes in what's sent to frontend 2025-03-18 10:47:32 -05:00
e4d52a0443 Include field maxLength attribute in what's sent to frontend 2025-03-18 10:47:12 -05:00
116a4e883b Bugfix - processing fieldAnnotation.defaultValue was throwing away the value, not actually setting it in the fieldMetaData 2025-03-18 10:46:42 -05:00
36ff5eea02 Add an openSheet(index) method 2025-03-18 10:46:09 -05:00
75fdff031a Renamed ExcelPoiStyleCustomizerInterface to ExcelPoiBasedStreamingStyleCustomizerInterface; support (by skipping) null column widths 2025-03-18 10:45:29 -05:00
14398d2c94 Open up makeQReportField to be public (as well as FieldAndJoinTable, which, in some other branch I believe was removed from this class, so, anticipate a conflict over that?) 2025-03-18 10:44:44 -05:00
9aa25b4f14 Add exportStyleCustomizer to QReportMetaData, plus clonable here and on child metadata 2025-03-18 10:43:40 -05:00
b863d62688 Add style customizer to report action, with excel poi implementation for columnWidths, more cell styles, merged ranges 2025-03-18 10:42:53 -05:00
08ed9a5aad Add style customizer to report action, with excel poi implementation for columnWidths, more cell styles, merged ranges 2025-03-18 10:18:28 -05:00
244239f053 Try to get better message in front of users if streamed ETL process is init'ed with no records 2025-03-18 10:04:52 -05:00
0f8ad2fb78 Allow a map of prepopulatedValues to be provided as an input value, to set defaultValues for fields 2025-03-18 10:04:16 -05:00
f99c39e0f6 WIP to handle login url (e.g., for static-site) - incomplete! 2025-03-18 09:50:17 -05:00
2c32c5a9fc Checkpoint on cleaning up, preparing for completion of auth + routing 2025-03-18 09:46:57 -05:00
5a5d98a3ff Merged feature/oauth2-authentication-module into feature/qrun-support-20250313 2025-03-13 08:26:22 -05:00
7d2282ebb7 Reset Unirest config and fix test assertions. 2025-03-13 07:58:22 -05:00
8e9954c909 add a ProcessBasedRouter to the sample site, and SimpleRouteAuthenticator 2025-03-12 20:19:07 -05:00
8cf53e045e Add a double-wrap of tempContexts around the example call to MetaDataAction for the example, to avoid warning about creating a system-user session w/o an instance in context. 2025-03-12 20:18:06 -05:00
955cb67a2c Working version of authentication for static & dynamic (process) route providers 2025-03-12 20:17:16 -05:00
45a6c3bcad Add validation of the code reference used for backendSteps, including support for QCodeReferenceLambda 2025-03-12 20:00:28 -05:00
d0768a6981 Initial version of QProcessPayload - like QRecordEntity, but for process values. refactoring of QRecordEntity to share logic 2025-03-12 19:59:28 -05:00
0c72210e8e update mock auth module to fail if an accessToken of 'Deny' is given; add method getLoginRedirectUrl t auth module interface 2025-03-12 19:59:28 -05:00
a2b36a10e7 Switch tests (back) to use mock authentication 2025-03-08 20:20:11 -06:00
f92ab85c8c Merged dev into feature/meta-data-loaders 2025-03-08 20:05:25 -06:00
2c976e59f4 Add oauth2-oidc-sdk; update auth0, jwks-rsa, and dotenv-java deps (for securtiy warnings) 2025-03-08 20:02:00 -06:00
23e87cd9ce Initial implementation of 0Auth2 authentication module 2025-03-07 20:36:20 -06:00
7c39372153 Initial checkin of Banners under QBrandingMetaData
- includes migration from (now deprecated) MetaDataFilterInterface to MetaDataActionCustomizerInterface (stored on the QInstance and used by MetaDataAction)
- includes migration from (now deprecated) environmentBannerText and environmentBannerColor in QBrandingMetaData to now be implemented as a banner
2025-03-07 14:39:39 -06:00
491fcd6d25 updated run backend step action to look for record id value string if no records in the input 2025-03-07 10:08:38 -06:00
e0045bb212 updated ses sender to consider adding label to from if provided 2025-03-06 16:28:51 -06:00
04e13413ef Updating to 0.25.0 2025-03-06 12:07:40 -06:00
a489808847 Merge tag 'version-0.24.0' into dev
Tag release
2025-03-06 12:07:36 -06:00
66202b9d02 Merge branch 'rel/0.24.0' 2025-03-06 12:03:51 -06:00
1a5a374c4e Update for next development version 2025-03-06 11:48:05 -06:00
51c588d2de Update versions for release 2025-03-06 11:48:03 -06:00
e3c89a80ca Update qqq-frontend-material-dashboard to 0.24.0 [skip ci] 2025-03-06 11:40:15 -06:00
bb79a31b4f Merge pull request #163 from Kingsrook/feature/CE-2260-add-ability-to-send-to-fe-and-extensive
Feature/ce 2260 add ability to send to fe and extensive
2025-03-06 10:10:26 -06:00
f49be5ff63 Switch accessToken check from != null to StringUtils.hasContent 2025-03-05 19:53:04 -06:00
83c4034d90 Merged feature/sftp-and-headless-bulk-load into dev 2025-03-05 19:40:32 -06:00
3a8bfe5f48 Minor cleanup from code review (comments, fixed a few exceptions); 2025-03-03 09:01:08 -06:00
d4d20e2b20 Fix this test that would never have worked on 3/1 of a non-leap year, i suppose 2025-02-28 19:53:18 -06:00
4cbcd0a149 better handling of some - ranges; upper-case input string to match month/day names; handle '*' day of week; day-names in , case; hour w/ AM/PM in , case; join with commas and and. 2025-02-28 19:45:01 -06:00
4b0d093a4a Add clearKey(key) 2025-02-28 19:42:40 -06:00
99e282fcdf Add sourceClass attribute to MetaDataProducerInterface 2025-02-28 19:42:25 -06:00
9fb53af0ba Checkstyle! also rename new method 2025-02-26 18:20:49 -06:00
7efd8264fa Change tables PVS to be custom type, respecting session permissions; refactor some PVS search logic to make custom implementations a little better 2025-02-26 16:56:36 -06:00
425d18e6df Remove TOOLTIP from FieldAdornment values 2025-02-26 16:03:10 -06:00
2808b3fcc4 test fixes 2025-02-26 15:22:49 -06:00
3ae5f90cc8 Checkstyle 2025-02-26 15:18:31 -06:00
92f0bd3846 Try to bubble more useful exceptions out 2025-02-26 15:15:26 -06:00
2a0bc03337 Accept storageReference (file path) as optional input 2025-02-26 15:14:47 -06:00
b87fb6bd4a Adjust inserted-ids process summary line for when only 1 record was inserted 2025-02-26 15:11:11 -06:00
1354755372 Make some of hard-coded table & field names optionally come from widget input, for more flexible usage (e.g., by sftp-data-integration qbit's report export setup) 2025-02-26 14:56:05 -06:00
2703f06b23 Add TOOLTIP type adornment; also, update url-encoding in FileDownload adornment to .replace("+", "%20") 2025-02-26 14:55:07 -06:00
428832f4ec Add discoverAndAddPluginsInPackage 2025-02-26 14:54:00 -06:00
27c816d627 Add a root exception 2025-02-26 14:53:42 -06:00
366f5d9600 Initial checkin 2025-02-26 14:47:11 -06:00
4b585cde45 convert paths starting with / to be ./ instead 2025-02-25 11:47:09 -06:00
eae24e3eba Add method pemStringToDecodedBytes 2025-02-25 08:45:48 -06:00
cdc6df2140 Removing call to remove all writeCapabilities from RenderedReport table... not entirely clear that's wanted anyway, and it's a change in behavior now, since this overload of withoutCapabilities was fixed... 2025-02-24 20:10:26 -06:00
21c4434831 Add support for public-key based authentication 2025-02-24 19:57:07 -06:00
b984959aa7 A little more flexibility in filter validation, for context w/o a joinContext 2025-02-24 14:25:30 -06:00
a0d12eade7 Make validateQueryFilter public 2025-02-24 11:14:56 -06:00
77cc272425 Initial checkin 2025-02-24 11:07:22 -06:00
80c286ab00 update setBlobValuesToDownloadUrls to not do that if the field is set to use a downloadUrlDyanmic. 2025-02-24 10:46:39 -06:00
35c4049174 Add LinkValues.TO_RECORD_FROM_TABLE_DYNAMIC and FileDownloadValues.DOWNLOAD_URL_DYNAMIC 2025-02-24 10:23:02 -06:00
cddc42db5b add testSimpleQueryForOneFile 2025-02-21 16:27:17 -06:00
2b9181b22e Remove block that was adding fileName to requestedPath, idk, wasn't good 2025-02-21 16:26:54 -06:00
46a1a77d1b Add method getProcess 2025-02-21 16:26:19 -06:00
6fe04e65df Add getValueFromRecordOrOldRecord 2025-02-21 16:26:10 -06:00
001860fc91 Initial checkin of QBits, mostly. 2025-02-21 16:24:49 -06:00
f4f2f3c80e Refactor a findProducers method out of processAllMetaDataProducersInPackage, for more flexibility (e.g., in QBitProducers) 2025-02-21 15:05:11 -06:00
0395e0d02c Add warning if input primaryKey is a filter (because that's probably not what you wanted!) 2025-02-21 15:04:23 -06:00
df530b70b8 Add static wrapper 2025-02-21 15:04:02 -06:00
693dfb2d5b Update getExistingRecordQueryFilter to convert sourceKeyList to be in the destination foreign key field's type 2025-02-21 15:02:29 -06:00
e2b81e46b9 CE-2260: fixes to oath with variants 2025-02-21 12:36:40 -06:00
b2c8c075fd CE-2260: added utility method for getting oath access key which will handle variants properly 2025-02-21 12:09:36 -06:00
3114812e34 CE-2261: qqq updates to table name 2025-02-21 09:44:29 -06:00
a659dc7a02 CE-2261: updated call to set table name to not be escaped 2025-02-20 23:25:35 -06:00
05bb0ef363 Fix withoutCapabilities(Set) - was calling with, not without :( 2025-02-20 15:35:52 -06:00
d401cc9ae1 Implement and test DeleteAction functionality
- Unified `deleteFile` API across storage modules by removing unused `QInstance` parameter.
- Added implementations for S3, SFTP, and local filesystem deleteAction.
2025-02-20 14:29:08 -06:00
44236f4309 change to not include createDate field for s3 (where it's not supported); changed file-name field used on the download adornment to be baseName by default, but configurable 2025-02-20 11:42:23 -06:00
d25eb6ee48 Simplify file listing by replacing filters with requested paths
Refactor file listing mechanisms to replace the use of complex query filters with simpler, path-based requests. Updated module-specific implementations and removed unused filtering logic. Updated tests (zombie'ing some)
2025-02-20 11:41:29 -06:00
be4f3c68f0 Update expected error message 2025-02-19 20:17:56 -06:00
2502d102d9 Better version (i hope) of using ssh & sftp client objects 2025-02-19 20:17:32 -06:00
dcf7218abf add basename field 2025-02-19 20:17:31 -06:00
bb1a43f11f Initial checkin 2025-02-19 20:02:05 -06:00
e5bdf8cd5e Move makeConnection to its own method (for use by test process); add postAction to try to close the things; add looking for 'path' criteria and adding it to readDir call 2025-02-19 20:01:30 -06:00
31a586f23e Move stripLeadingSlash up to base class 2025-02-19 19:54:58 -06:00
91aa8faca2 Add baseNameFieldName 2025-02-19 19:54:47 -06:00
154c5442af Add postAction(); move variants stuff to new BackendVariantsUtil; add baseName to ONE records; remove path criteria when filtering (assuming the listFiles method did it) 2025-02-19 19:54:37 -06:00
7ab19ca9b4 Move variant lookups to new BackendVariantsUtil 2025-02-19 19:53:14 -06:00
dc25f6b289 Explicit exception if table name is not given. 2025-02-19 19:52:49 -06:00
2fd3ed2561 add serializable 2025-02-19 19:51:05 -06:00
0005c51ecd Add capturing and reporting first & last inserted primary keys 2025-02-19 19:50:47 -06:00
143ed927fa add ability to set and trace processTracerKeyRecord in bulk load 2025-02-19 19:50:27 -06:00
8816177df8 Add optional variantRecordLookupFunction to BackendVariantsConfig and validation of same; refactor up some shared backend code into BackendVariantsUtil 2025-02-19 19:49:33 -06:00
be6d1b888f Add urlencoding to blob download urls 2025-02-19 19:07:56 -06:00
ead66385be Merge branch 'dev' into feature/CE-2261-packing-slip-template-config 2025-02-19 17:24:52 -06:00
5d2adb76e0 CE-2261: added grid widths to field metadata 2025-02-19 17:10:46 -06:00
3f8c2957d1 Swap setVariantOptionsTableTypeField for setVariantOptionsTableTypeValue re: which one sets the new config's setVariantTypeKey 2025-02-14 20:45:51 -06:00
c341708d21 Start (mostly done?) support for headless bulk-load 2025-02-14 20:30:13 -06:00
b93114a9ba Initial add of sftp filesystem module 2025-02-14 20:26:44 -06:00
5a7199495d Basic support for variants; more fields on ONE type file records (size, dates); apply skip, limit, filter, sort on listings/queries for ONE-type files; treat contents as heavy-field if so set; more try-catch (e.g., upon write file) 2025-02-14 20:24:10 -06:00
2591e6ad44 Update javadoc because i can't ever remember if inputStream or outputStream is used for writing or reading 2025-02-14 20:21:32 -06:00
72e175e1a6 Add method to work with recordEntities 2025-02-14 20:20:56 -06:00
243cf66dbd Avoid NPE on empty list of fields in setBlobValuesToDownloadUrls 2025-02-14 20:09:46 -06:00
7bd560b7a8 Initial checkin 2025-02-14 20:09:13 -06:00
bacfa57c5e New ways of working with field sections 2025-02-14 20:07:37 -06:00
4c502df328 Update to use new backendVariantConfig; removed unused session field in base api action 2025-02-14 20:01:00 -06:00
be25fc1272 Refactor setup of backend variants to use a dedicated sub-object, with more flexible "backend setting" fields as a map based with enum keys, rather than dedicated set of methods 2025-02-14 19:55:04 -06:00
f0c07caba8 Quality-of-life, add some todos for ideas 2025-02-12 14:21:02 -06:00
ab31067e11 Merge pull request #158 from Kingsrook/feature/process-tracers
Feature/process tracers
2025-02-12 15:17:54 -05:00
a18ffaa3ec CE-2261: replaced deprecated calls with actionInput 2025-02-12 09:34:03 -06:00
29e407b782 CE-2261: removed depricated calls with actionInput 2025-02-11 12:25:31 -06:00
c47c39f5e7 Move call to traceStartOrResume to be after processUUID is initialized (for case when it isn't given) 2025-02-10 13:55:22 -06:00
cd40177569 Add Long to isSupportedFieldType 2025-02-10 09:52:18 -06:00
eb8fa42fb8 Initial build of QQQProcess table - analog to QQQTable table, but for processes; refactoring of QQQTable record management into util class (out of QueryStatManager where it was originally used) 2025-02-10 09:52:06 -06:00
9072ce2426 Initial implementation of process tracers 2025-02-10 09:37:59 -06:00
ec713553b8 Merge pull request #157 from Kingsrook/feature/support-CE-2257-ice-logic
Feature/support ce 2257 ice logic
2025-02-10 09:27:31 -06:00
53f48331db Deleted pdf 2025-02-10 09:20:58 -06:00
c53f9b8fc9 Add more examples of joins 2025-02-10 09:20:29 -06:00
74e755b111 Add details about producing tableMetaData via @QMetaDataProducingEntity and customizers 2025-02-10 08:54:02 -06:00
227d22ed14 Remove a todo 2025-02-10 08:53:32 -06:00
7e50860983 Add javadoc 2025-02-09 17:31:25 -06:00
ee4f9bc209 Add ValueRangeBehavior 2025-02-09 17:28:46 -06:00
c76a5e20e8 re-add the default value for label... 2025-02-09 11:16:41 -06:00
e25ec61731 Add optional additional validation to widget meta datas; implemented at least in part for ChildRecordListWidget 2025-02-08 20:54:38 -06:00
33f3ebd4c6 Add ValueRangeBehavior - e.g., for min/max numeric value 2025-02-03 15:45:59 -06:00
036b02bb6c Add defaultValuesForNewChildRecordsFromParentFields to ChildRecordListData 2025-02-03 08:53:30 -06:00
1cec2505c9 Add auth meta-data, now that validator wants it. 2025-01-31 15:22:34 -06:00
54ff797b5d Add auth meta-data, now that validator wants it. 2025-01-31 15:07:25 -06:00
1f416fcc43 Move NotImplementedHereException inside the interface (don't love it, but fine checkstyle) 2025-01-31 14:45:27 -06:00
40b4b55bf4 Add preInsertOrUpdate, postInsertOrUpdate, and oldRecordListToMap 2025-01-31 14:32:26 -06:00
f86b3d9973 misc cleanups 2025-01-31 14:31:36 -06:00
2031e05117 Update QMetaDataProducingEntity to know how to produce table meta data; Add MetaDataCustomizers to work with producer helpers 2025-01-31 14:29:51 -06:00
38a17b2954 Merge pull request #155 from Kingsrook/feature/join-record-enhancements
Feature/join record enhancements
2025-01-31 10:54:38 -06:00
a5c65b9e67 Test coverage on new javalin routing classes 2025-01-30 20:46:33 -06:00
48fbb3d054 Update setStepList to properly fully replace both step list and map 2025-01-30 20:46:04 -06:00
bcca710316 Javalin process-based custom router; javalin meta-data to define routers 2025-01-30 19:13:32 -06:00
6d749e9df6 First version of loading process meta-data via loader (steps needed discriminating loader) 2025-01-30 19:11:39 -06:00
f0eeb260e3 Merged dev into feature/join-record-enhancements 2025-01-29 14:41:44 -06:00
d14662e2fc Update tests now that BulkLoadValueMapper removes non-valid possible-value values from record. 2025-01-29 11:34:56 -06:00
0635a9128c Remove field-values that had an error in type convertin' or possible-value lookin' up (to avoid downstream errors e.g., in pre-insert customizers) 2025-01-29 11:18:10 -06:00
81ffe1a286 checkstyle 2025-01-23 10:38:56 -06:00
6b49abb749 Checkpoint - serving static site 2025-01-23 10:11:47 -06:00
efb47b9cd6 Checkpoint - yaml-meta data and sample server 2025-01-23 10:09:03 -06:00
29f2feb321 Start support for static-file routing 2025-01-23 10:08:42 -06:00
3537d2cfd1 make QJavalinMetaData implements QSupplementalInstanceMetaData 2025-01-23 10:08:30 -06:00
634abe3822 Checkpoint on loaders tests 2025-01-23 09:51:29 -06:00
93c7fbca25 Checkpoint on loaders 2025-01-23 09:39:31 -06:00
ea40197893 more QQQApplication implementations 2025-01-23 09:37:21 -06:00
38293b81d7 Switch QSupplementalInstanceMetaData to interface instead of abstract class; remove getType in favor of getName from its base class, TopLevelMetaDataInterface; 2025-01-23 09:35:55 -06:00
7b141c3f5b Add implements QMetaDataObject 2025-01-23 09:33:34 -06:00
502095002c Add getClassesContainingNameAndOfType 2025-01-23 09:32:57 -06:00
42a8d37493 add methods: maskAndTruncate; nCopies; nCopiesWithGlue 2025-01-23 09:32:46 -06:00
ccb51be4f9 Merged feature/filter-json-field-improvements into dev 2025-01-22 16:44:53 -06:00
b6623fbed0 Merged feature/sqlite-and-rdbms-strategies into dev 2025-01-22 16:44:29 -06:00
fdebdb1095 Merged feature/entity-to-record-updates into dev 2025-01-22 16:44:15 -06:00
8e24faa975 Merged feature/process-locks-bulk into dev 2025-01-22 16:43:56 -06:00
3013e5dccd Merged feature/bulk-upload-v2 into dev 2025-01-22 16:43:32 -06:00
6725704b13 Merged dev into feature/meta-data-loaders 2025-01-17 19:12:48 -06:00
48ac6a0a4f Checkstyle 2025-01-16 19:51:57 -06:00
3f4d11b22a Checkpoint - class-detecting loader handling generic loaders; generic loader created & working; Loader registry moved to its own class; 2025-01-16 14:08:32 -06:00
c91a7903ba Haandle FORMULA type by using 'raw value' as string (seems to be the evaluated value) 2025-01-16 10:52:59 -06:00
109e390bc3 Add explicit log (Rather than NPE) for unknown table name 2025-01-16 10:24:25 -06:00
d6288eee4a Add support for CriteriaOption.CASE_INSENSITIVE 2025-01-16 10:24:01 -06:00
8c7e523e43 Add concept of criteriaOptions - ways an application & backend can add modified behavior to a criteria 2025-01-16 10:23:22 -06:00
0fffed9d31 Initial checkin 2025-01-16 10:22:09 -06:00
84d41858b2 Add method addJoinedRecordValues 2025-01-16 10:22:02 -06:00
459629b449 Promote FieldAndJoinTable up out of GenerateReportAction into top-level class, with factory method 2025-01-16 10:21:46 -06:00
64de5c9913 downgrade some logs 2025-01-15 14:30:34 -06:00
68f9bb20f7 Copyright 2025-01-14 11:07:16 -06:00
4b904471af Initial checkin - reusable FieldDisplayBehavior for fields storing a JSON-serialized queryFilter. 2025-01-14 10:54:24 -06:00
f4b54518fa Override getDefault, to return a NOOP instance 2025-01-14 10:53:53 -06:00
e012b1f090 Add properties: hidePreview, filterFieldName, columnFieldName 2025-01-14 10:52:39 -06:00
1fae1c5e2a Move enricher plugin to enrichment package 2025-01-14 10:08:42 -06:00
b8ef480804 minor grammar and typos [skip ci] 2025-01-11 20:30:56 -06:00
b397c4da08 CE-1955 Add plugins for QInstanceEnricher 2025-01-11 20:13:51 -06:00
e2c7748a4b CE-1955 Update getValueAsInstant to handle a single-digit hour, by assuming a leading 0 on it. 2025-01-10 16:11:14 -06:00
70b569c2ca CE-1955 Memoize groupByAllIndexesFromTable to avoid wasting lots of arrayLists; add todo about maybe only doing grouping if there is a mapped child table... 2025-01-10 15:43:38 -06:00
20332fa011 CE-1955 Revert splitting out records with mapping errors, to help do less spoon-feeding; also, avoid double-running customizer 2025-01-10 15:42:17 -06:00
387804acff CE-1955 Add "H" to pattern check for date-time/hours (doesn't appear in docs i can find, but does appear in a file i'm working with, so... probably valid) 2025-01-07 11:34:39 -06:00
5ad4216434 CE-1955 Replace _ with space in allCapsToMixedCase (for common use-case of an enum constant) 2025-01-07 11:30:48 -06:00
f7cbf9d1c2 CE-1955 Make sure to skip blank rows (e.g., no columns had a value) 2025-01-07 11:30:19 -06:00
bcedb566ff CE-1955 Add info summary line re: number of records processed; also return early if all records have mapping errors... this could lead to some spoon feeding, but, is working better now, so. 2025-01-07 11:24:39 -06:00
5171af1c95 CE-1955 Adjust help text re: headers 2025-01-07 11:22:33 -06:00
f54b2b79db CE-1955 Add support for value-mapping on wide-mode associated fields 2025-01-06 16:35:06 -06:00
d63cff8c5b Switch tests to use SQLiteTableBackendDetails (and update it to extend RDBMS's version) 2025-01-06 11:15:10 -06:00
32a8d65a84 Copyrights and checkstyle 2025-01-06 11:01:23 -06:00
62bf361e36 Initial checkin 2025-01-06 10:35:28 -06:00
86bf82f590 Update assembly plugin config to work for building a jar-with-deps that works for launching javalin server; update qfmd to 0.24.0 2025-01-06 08:56:01 -06:00
80b24e6dfc Merge pull request #152 from Kingsrook/feature/migrate-sample-app-to-new-javalin-server
Feature/migrate sample app to new javalin server
2025-01-06 08:50:06 -06:00
8601347d97 Update to use QApplicationJavalinServer instead of QJavalinImplementation 2025-01-06 08:40:30 -06:00
37aaea3452 Update to extend AbstractQQQApplication; set custom logo 2025-01-06 08:39:45 -06:00
719be86e94 Add guard around serving of material-dashboard-overlay, to allow server to start up without that path existing 2025-01-06 08:36:23 -06:00
009e144361 test coverage for rdbms 2025-01-03 20:42:15 -06:00
8e65255248 Delete and cleanup in QueryManger; test coverage improvements 2025-01-03 19:58:12 -06:00
2260fbde84 Initial checkin of sqlite module 2025-01-03 19:36:11 -06:00
db1269824c Refactor to use RDBMSActionStrategy 2025-01-03 16:59:09 -06:00
dc6d37aad3 Introduce the concept of RDBMSActionStrategyInterface - to use strategy pattern for refinement of how different RDBMS sub-backends may need to behave (e.g., to support SQLite, and FULLTEXT INDEX in MySQL). 2025-01-03 16:59:09 -06:00
aba5b9c5ec Move backendType/name into constant 2025-01-03 16:59:09 -06:00
b64efd0246 Add method buildConnectionString to RDBMSBackendMetaData 2025-01-03 16:59:09 -06:00
6a5f8fadad Add support for a list of "queries for new connections", to be ran when new connections are opened 2025-01-03 16:59:09 -06:00
5ecae928ac Fix path to asciidoc generataed index.html to be stored 2025-01-03 16:51:44 -06:00
8d108b671a Turn off upload of docs to (now retired server that used to host) justinsgotskinnylegs.com 2025-01-03 16:43:16 -06:00
f9cd4373aa Update RDBMS Aggregates to return INTEGER for COUNT on temporal field types 2025-01-03 16:33:50 -06:00
e9fc5f81d2 CE-1955 Add some room for a PVS search to return duplicates... room to improve here though. 2025-01-03 12:58:12 -06:00
3fda1a1eda CE-1955 Add handling for associations w/ some vs. all values coming from defaults instead of columns; 2025-01-03 12:57:49 -06:00
048ee2e332 Expand cases hit due to new idType requirement in possible values 2024-12-27 09:08:13 -06:00
21982e8f53 Remove uncommitted BackendQueryFilterUtils.setCaseSensitive 2024-12-27 08:54:22 -06:00
8b00e8c877 checkstyle 2024-12-26 19:58:09 -06:00
f57df2be86 CE-1955 change type-argument to be extends-Serializable 2024-12-26 19:12:01 -06:00
7f67eda2e3 CE-1955 do case-insensitive lookups of possible values by label 2024-12-26 19:11:41 -06:00
a4499219c8 CE-1955 Update fastexcel version; Update XlsxFileToRows to read formats, and then do a better job of handling numbers as date-time, date, int, or decimal (hopefully) 2024-12-26 19:09:41 -06:00
9cfc7fafc1 CE-1955 case-insenitiveKey map, to help with bulk load possible value case-insensitvity 2024-12-26 19:08:30 -06:00
6b7d3ac26d CE-1955 propagate errors from child (association) records up to main record 2024-12-26 18:53:27 -06:00
7e475e2c18 CE-1955 - add idType to possibleValueSource - used by bulk load possible-value mapping 2024-12-23 14:59:48 -06:00
2b0b176ced CE-1955 - only handle a single level deep of associations... 2024-12-23 14:59:48 -06:00
db526009d2 CE-1955 - more flexible handling of inbound types for looking up possible values 2024-12-23 14:59:48 -06:00
a6001af7b5 Add overload of toQRecordOnlyChangedFields that allows primary keys to be included (more useful for the update use-case) 2024-12-23 14:59:28 -06:00
f147516e45 Make tests passing 2024-12-23 11:44:55 -06:00
f3fe8a3c73 Checkstyle! 2024-12-23 11:39:09 -06:00
71dcf231db Checkstyle! 2024-12-23 11:34:22 -06:00
a20efabcf2 Initial checkin 2024-12-23 11:33:09 -06:00
00b72e0338 In enrichTable, set name in QFieldMetaData based on its key in the fields map, if it wasn't otherwise set. 2024-12-23 11:31:11 -06:00
b979e6545a Mark class as implementing QMetaDataObject 2024-12-23 11:30:27 -06:00
7982cad794 Initial build of classes to load meta-data from yaml or json files 2024-12-23 11:29:30 -06:00
891bdf68b6 Merged dev into feature/process-locks-bulk 2024-12-20 15:30:19 -06:00
b02818764b Fix heading levels 2024-12-20 12:16:46 -06:00
9e348b9817 Add section about meta-data production 2024-12-20 12:14:18 -06:00
000226c30a Make unique id on pet species enum 2024-12-20 09:38:20 -06:00
cbde8d79bd Merged feature/pagination-in-unique-key-helper into dev 2024-12-19 16:05:05 -06:00
3e69003ba7 Merged feature/file-download-callbacks into dev 2024-12-19 16:04:44 -06:00
d5ec117d1b Merged feature/meta-data-producing-annotations into dev 2024-12-19 16:04:21 -06:00
edf248c851 Add methods to ProcessLockUtils to work in bulk (both for creating and releasing locks); fix ProcessLock join to type table (had wrong joinOn field) 2024-12-19 16:03:39 -06:00
11ff517769 Do pagination, to avoid queries with, idk, 320,000 params... 2024-12-19 12:07:12 -06:00
eba6dfe1b3 CE-1772 - add call to Unirest.config().reset() 2024-12-17 11:46:19 -06:00
c5f41a8042 CE-1772 - update fileDownload adornment type to be able to specify a process name or custom code-ref, to run along with downloading a field's file. 2024-12-17 11:40:11 -06:00
23e730f566 Add an exception in PossibleValueSource.withValuesFromEnum if duplicated id values are given 2024-12-13 15:18:04 -06:00
ec74649c96 Introduce annotations that can be found by MetaDataProducerHelper, to make more meta-data, with less code. Specifically:
- PVS from PossibleValueEnum
- PVS from RecordEntity
- Joins from a parent-entity to child-entities
- ChildRecordList Widgets from a parent-entity to child-entities
2024-12-13 11:26:01 -06:00
16f931cd5c javadoc cleanup 2024-12-13 10:59:44 -06:00
d2c0ad498f add method getAssociationByName 2024-12-13 10:59:20 -06:00
5070f0a738 add method emptyToNull 2024-12-13 10:56:58 -06:00
21a5c98376 add method addIfNotNull 2024-12-13 10:56:46 -06:00
edec6d64e3 Add more validation of the join and associated table, in table associations. 2024-12-13 10:54:29 -06:00
c3c82cbd4a Checkstyle 2024-12-13 10:49:01 -06:00
6687a58bfa Add subFilterSetOperator (e.g., UNION, INTERSECT, EXCEPT) to QQueryFilter - along with implementation in RDBMS module, to generate such queries 2024-12-13 10:39:54 -06:00
96761b7162 Merge pull request #142 from Kingsrook/feature/audit-missing-security-key-logs
Update getRecordSecurityKeyValues and validateSecurityKeys to be awar…
2024-12-13 09:00:38 -06:00
7bdea734b4 Merge pull request #144 from Kingsrook/feature/hotfix-javalin-process-values-null-map-keys
Feature/hotfix javalin process values null map keys
2024-12-13 08:59:17 -06:00
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
271f2dc25b CE-1955 Add a display-value for the mappingJSON in saved bulk-load-profiles 2024-12-04 14:59:53 -06:00
c4583f16a9 CE-1955 Fix to re-set the position of the review step, upon going back 2024-12-04 14:58:34 -06:00
434d158776 CE-1955 disable until ci selenium fixed 2024-12-04 07:12:10 -06:00
eec1924113 CE-1955 add browser-tools orb, to try to fix selenium/chrome version mismatch 2024-12-03 22:03:03 -06:00
164d9e1de5 CE-1955 Checkstyle 2024-12-03 21:46:49 -06:00
131da68a38 CE-1955 Update to use new AbstractQQQApplication and QApplicationJavalinServer 2024-12-03 20:46:37 -06:00
f7bd049b81 CE-1955 Update qfmd to feature-bulk-upload-v2; add test-dep for qfmd; add slf4j simple and selenium and webdriver. 2024-12-03 20:44:29 -06:00
76d7a8a858 CE-1955 Initial checkin 2024-12-03 20:43:33 -06:00
8d37ce3c54 CE-1955 add checks for material-dashboard resources before trying to blindly serve them; add field for QJavalinMetaData; 2024-12-03 20:43:10 -06:00
7bab11ea7e CE-1955 Add support for wildcard (at start of) process names - e.g., to support bulkLoad etc processes; update to apply all helpContent to the qInstance that came in as a parameter, rather than the one in context (to work correctly for hot-swaps). 2024-12-03 20:42:02 -06:00
8157510c04 CE-1955 Add fields to bulkLoad fileMapping screen, for helpContent to be associated with 2024-12-03 20:39:38 -06:00
b5eae02fa4 CE-1955 populate association structures for record preview validation screen based on table structure associations, not actual mapping (e.g., so lines always appear on orders, even if not being used - to make that clear to user that they aren't being used) 2024-12-03 20:39:18 -06:00
1911e27cc0 CE-1955 clear out uploaded file if user goes back to this step 2024-12-03 20:38:28 -06:00
21aeac2def CE-1955 Switch fieldMetaData to use a type from in here for FieldAdornment, to include some better docs, but also to exclude new FILE_UPLOAD adornment type enum value 2024-12-03 09:51:44 -06:00
2bf12158be CE-1955 Fix to set tableName before preUpload step 2024-12-03 09:27:50 -06:00
7e3592628a CE-1955 Don't put empty-string values into records (in setValueOrDefault) - in general, we might get an empty-string from a file, but let's treat it like a non-value, null. 2024-12-03 09:27:35 -06:00
21069e2310 CE-1955 Checkstyle! 2024-12-03 09:10:00 -06:00
11db820196 CE-1955 Bulk insert updates: Add prepareFileUploadStep; make theFile field use drag&drop adornment 2024-12-03 09:03:02 -06:00
a7247b5970 CE-1955 Add method resetValidationFields - to help processes that go 'back' 2024-12-03 08:59:48 -06:00
7cd3105ee6 CE-1955 Add search-by labels - e.g., exact-matches on a single-field used as the PVS's label... definitely not perfect, but a passable first-version for bulk-load to do PVS mapping 2024-12-03 08:59:27 -06:00
86f8e24d5f CE-1955 Handle back better; put suggested mapping profile into process value under a dedicated key 2024-12-03 08:59:27 -06:00
b0cc93cbb7 CE-1955 Add FILE_UPLOAD adornment type 2024-12-03 08:59:27 -06:00
b055913fc8 CE-1955 Initial checkin 2024-12-03 08:59:27 -06:00
0e93b90270 CE-1955 Add mapping and validation of possible-values; refactor error classes some for rollup possible value errors 2024-12-03 08:59:27 -06:00
8ec6ccd691 CE-1955 added an icon for bulk-load process in example (since it has one now) 2024-11-27 15:36:36 -06:00
53ca77cde6 CE-1955 Update to use an enum-subset (excluding new BULK_LOAD components) 2024-11-27 15:36:19 -06:00
a439bffc69 Add support for OpenAPIEnumSubSet 2024-11-27 15:34:37 -06:00
8ea16db1fc CE-1955 - Checkstyle 2024-11-27 15:11:02 -06:00
61582680f3 CE-1955 - Add support for back to bulk-load process 2024-11-27 15:01:35 -06:00
8c6b4e6863 CE-1955 - Add back to processes 2024-11-27 15:01:06 -06:00
9213b8987b CE-1955 - Summarize with some examples (including rows nos) for value mapping and other validation errors 2024-11-27 12:36:35 -06:00
c88fd5b7d4 CE-1955 - Summarize with some examples (including rows nos) for value mapping and other validation errors 2024-11-27 12:36:20 -06:00
6ed9dfd498 CE-1955 - Put rows & rowNos in backend details during bulk-load. assert about those. also add tests (and fixes to mapping) for no-header use-cases 2024-11-27 12:13:15 -06:00
17fc976877 CE-1955 - Add rowNo to BulkLoadFileRow, set by FileToRowsInterface objects 2024-11-27 11:46:24 -06:00
3b24cb745c Update getRecordSecurityKeyValues and validateSecurityKeys to be aware of multiLocks 2024-11-27 08:47:58 -06:00
6672f95987 Merged dev into feature/bulk-upload-v2 2024-11-25 16:49:15 -06:00
1c2638a5c4 CE-1955 - Boosting test-coverage during bulk-load rollout 2024-11-25 11:27:44 -06:00
c883749ba9 CE-1955 - Remove bulk-insert v1 test; rename bulkInsertV2 test 2024-11-25 11:15:13 -06:00
3c06e0e589 CE-1955 - Test fixes 2024-11-25 11:10:01 -06:00
bdbb2d2d00 CE-1955 - Bulk load checkpoint - setting uploadFileArchiveTable in javalin metadata 2024-11-25 10:09:05 -06:00
58ae17bbac CE-1955 - Bulk load checkpoint:
- Switch wide format to identify associations via comma-number-indexes...
- Add suggested mappings
- use header name instead of column index for mappings
- add counts of children process summary lines
- excel value/type handling
2024-11-25 10:07:26 -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
9ad9d52634 CE-1955 Add method defineTableBulkInsertV2 (needs to not be v2 i guess) 2024-11-19 10:29:13 -06:00
07c0413277 CE-1955 Initial checkin (plus add a memory-storage table to testutils) 2024-11-19 10:25:40 -06:00
2918235f46 CE-1955 Add version field to the built BulkLoadProfile 2024-11-19 10:25:22 -06:00
07886214f5 CE-1955 Test fixes 2024-11-19 08:53:49 -06:00
22ce5acf46 CE-1955 Make filename its own path element in uploadedFile processing 2024-11-19 08:45:24 -06:00
d8ac14a756 CE-1955 Checkpoint on bulk-load backend 2024-11-19 08:44:43 -06:00
b684f2409b CE-1955 Avoid type-based exceptions checking security key values 2024-11-19 08:37:36 -06:00
c09198eed5 CE-1955 Initial checkin 2024-11-19 08:37:05 -06:00
062240a0a5 CE-1955 Add BULK_LOAD_* values 2024-11-18 20:16:09 -06:00
6aafc3d553 CE-1955 Mark Serializable 2024-11-18 20:15:47 -06:00
39b322336f CE-1955 Add transaction to validateSecurityFields 2024-11-18 16:06:38 -06:00
4b590b5653 CE-1955 make public stuff used by another test now 2024-11-12 10:02:44 -06:00
da2be57a17 CE-1955 Add fastexcel-reader (and a pinned version of commons-io, for compatibility) 2024-11-12 10:00:19 -06:00
5f081fce44 CE-1955 Checkstyle! 2024-11-12 09:48:02 -06:00
e809c773f9 CE-1955 Switch to handle uploaded files via StorageAction into the uploadFileArchive table 2024-11-12 09:44:00 -06:00
d8a0a6c68d CE-1955 Move prime-test-database into mainline, to be loaded when javalin starts 2024-11-12 09:43:17 -06:00
7ba205a262 CE-1955 Initial checkin 2024-11-12 09:16:59 -06:00
7d058530d5 CE-1955 Initial checkin 2024-11-12 09:13:34 -06:00
73200b2fd2 CE-1955 Mark as Serializable 2024-11-12 09:13:12 -06:00
703 changed files with 62552 additions and 7091 deletions

View File

@ -0,0 +1,26 @@
#!/bin/bash
############################################################################
## Script to collect all JaCoCo reports from different modules into a
## single directory for easier artifact storage in CI.
############################################################################
mkdir -p /home/circleci/jacoco-reports/
##############################################################
## Find all module directories that have target/site/jacoco ##
##############################################################
for module_dir in */; do
if [ -d "${module_dir}target/site/jacoco" ]; then
module_name=$(basename "${module_dir%/}")
target_dir="/home/circleci/jacoco-reports/${module_name}"
echo "Collecting JaCoCo reports for module: ${module_name}"
cp -r "${module_dir}target/site/jacoco" "${target_dir}"
echo "Copied JaCoCo reports for ${module_name} to ${target_dir}"
fi
done
echo "All JaCoCo reports collected to /home/circleci/jacoco-reports/"

View File

@ -0,0 +1,48 @@
#!/bin/bash
############################################################################
## Script to concatenate all .txt files in the surefire-reports directory
## into a single artifact that can be stored in CI.
############################################################################
mkdir -p /home/circleci/test-output-artifacts/
###################################################################
## Find all module directories that have target/surefire-reports ##
###################################################################
for module_dir in */; do
if [ -d "${module_dir}target/surefire-reports" ]; then
module_name=$(basename "${module_dir%/}")
output_file="/home/circleci/test-output-artifacts/${module_name}-test-output.txt"
echo "Processing module: ${module_name}"
echo "Output file: ${output_file}"
##################################################################
## Concatenate all .txt files in the surefire-reports directory ##
##################################################################
if [ -n "$(find "${module_dir}target/surefire-reports" -name "*.txt" -type f)" ]; then
echo "=== Test Output for ${module_name} ===" > "${output_file}"
echo "Generated at: $(date)" >> "${output_file}"
echo "==========================================" >> "${output_file}"
echo "" >> "${output_file}"
##############################################
## Sort files to ensure consistent ordering ##
##############################################
find "${module_dir}target/surefire-reports" -name "*.txt" -type f | sort | while read -r txt_file; do
echo "--- File: $(basename "${txt_file}") ---" >> "${output_file}"
cat "${txt_file}" >> "${output_file}"
echo "" >> "${output_file}"
echo "--- End of $(basename "${txt_file}") ---" >> "${output_file}"
echo "" >> "${output_file}"
echo "" >> "${output_file}"
echo "" >> "${output_file}"
done
echo "Concatenated test output for ${module_name} to ${output_file}"
else
echo "No .txt files found in ${module_dir}target/surefire-reports"
fi
fi
done

View File

@ -2,35 +2,29 @@ version: 2.1
orbs:
localstack: localstack/platform@2.1
browser-tools: circleci/browser-tools@1.4.7
commands:
store_jacoco_site:
parameters:
module:
type: string
steps:
- store_artifacts:
path: << parameters.module >>/target/site/jacoco/index.html
when: always
- store_artifacts:
path: << parameters.module >>/target/site/jacoco/jacoco-resources
when: always
install_java17:
mvn_build:
steps:
- checkout
- restore_cache:
keys:
- v1-dependencies-{{ checksum "pom.xml" }}
- browser-tools/install-chrome
- browser-tools/install-chromedriver
- run:
name: Install Java 17
name: Write .env
command: |
sudo apt-get update
sudo apt install -y openjdk-17-jdk
sudo rm /etc/alternatives/java
sudo ln -s /usr/lib/jvm/java-17-openjdk-amd64/bin/java /etc/alternatives/java
echo "RDBMS_PASSWORD=$RDBMS_PASSWORD" >> qqq-sample-project/.env
- run:
## used by jacoco uncovered class reporting in pom.xml
name: Install html2text
name: Run Maven Compile
command: |
sudo apt-get update
sudo apt-get install -y html2text
mvn -s .circleci/mvn-settings.xml -T4 --no-transfer-progress compile
- save_cache:
paths:
- ~/.m2
key: v1-dependencies-{{ checksum "pom.xml" }}
mvn_verify:
steps:
@ -38,34 +32,26 @@ commands:
- restore_cache:
keys:
- v1-dependencies-{{ checksum "pom.xml" }}
- run:
name: Write .env
command: |
echo "RDBMS_PASSWORD=$RDBMS_PASSWORD" >> qqq-sample-project/.env
- run:
name: Run Maven Verify
command: |
mvn -s .circleci/mvn-settings.xml -T4 verify
- store_jacoco_site:
module: qqq-backend-core
- store_jacoco_site:
module: qqq-backend-module-filesystem
- store_jacoco_site:
module: qqq-backend-module-rdbms
- store_jacoco_site:
module: qqq-backend-module-api
- store_jacoco_site:
module: qqq-middleware-api
- store_jacoco_site:
module: qqq-middleware-javalin
- store_jacoco_site:
module: qqq-middleware-picocli
- store_jacoco_site:
module: qqq-middleware-slack
- store_jacoco_site:
module: qqq-language-support-javascript
- store_jacoco_site:
module: qqq-sample-project
mvn -s .circleci/mvn-settings.xml -T4 --no-transfer-progress verify
- run:
name: Collect JaCoCo reports
command: .circleci/collect-jacoco-reports.sh
when: always
- store_artifacts:
path: /home/circleci/jacoco-reports
destination: jacoco-reports
when: always
- run:
name: Concatenate test output files
command: .circleci/concatenate-test-output.sh
when: always
- store_artifacts:
path: /home/circleci/test-output-artifacts
destination: test-output
when: always
- run:
name: Save test results
command: |
@ -74,10 +60,6 @@ commands:
when: always
- store_test_results:
path: ~/test-results
- save_cache:
paths:
- ~/.m2
key: v1-dependencies-{{ checksum "pom.xml" }}
check_middleware_api_versions:
steps:
@ -88,8 +70,8 @@ commands:
- 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
mvn -s .circleci/mvn-settings.xml -T4 --no-transfer-progress install -DskipTests
mvn -s .circleci/mvn-settings.xml -T4 --no-transfer-progress -pl qqq-middleware-javalin package appassembler:assemble -DskipTests
qqq-middleware-javalin/target/appassembler/bin/ValidateApiVersions -r $(pwd)
mvn_jar_deploy:
@ -105,7 +87,7 @@ commands:
- run:
name: Run Maven Jar Deploy
command: |
mvn -s .circleci/mvn-settings.xml -T4 flatten:flatten jar:jar deploy:deploy
mvn -s .circleci/mvn-settings.xml -T4 --no-transfer-progress flatten:flatten jar:jar deploy:deploy
- save_cache:
paths:
- ~/.m2
@ -127,29 +109,30 @@ commands:
command: |
cd docs
asciidoctor -a docinfo=shared index.adoc
upload_docs_site:
steps:
- run:
name: scp html to justinsgotskinnylegs.com
command: |
cd docs
scp index.html dkelkhoff@45.79.44.221:/mnt/first-volume/dkelkhoff/nginx/html/justinsgotskinnylegs.com/qqq-docs.html
- store_artifacts:
path: docs/index.html
when: always
jobs:
mvn_test:
build:
executor: localstack/default
steps:
- mvn_build
test:
executor: localstack/default
steps:
## - localstack/startup
- install_java17
- mvn_verify
api_version_check:
executor: localstack/default
steps:
- check_middleware_api_versions
mvn_deploy:
executor: localstack/default
steps:
## - localstack/startup
- install_java17
- mvn_build
- mvn_verify
- check_middleware_api_versions
- mvn_jar_deploy
@ -159,18 +142,35 @@ jobs:
steps:
- install_asciidoctor
- run_asciidoctor
- upload_docs_site
workflows:
test_only:
jobs:
- mvn_test:
- build:
context: [ qqq-maven-registry-credentials, build-qqq-sample-app ]
filters:
branches:
ignore: /(dev|integration.*)/
tags:
ignore: /(version|snapshot)-.*/
- test:
context: [ qqq-maven-registry-credentials, build-qqq-sample-app ]
requires:
- build
filters:
branches:
ignore: /(dev|integration.*)/
tags:
ignore: /(version|snapshot)-.*/
- api_version_check:
context: [ qqq-maven-registry-credentials, build-qqq-sample-app ]
requires:
- build
filters:
branches:
ignore: /(dev|integration.*)/
tags:
ignore: /(version|snapshot)-.*/
deploy:
jobs:

View File

@ -30,6 +30,20 @@ There are a few useful IntelliJ settings files, under `qqq-dev-tools/intellij`:
One will likely also want the [Kingsrook Commentator
Plugin](https://plugins.jetbrains.com/plugin/19325-kingsrook-commentator).
## Test Logging
By default, when ran from the command line, mvn surefire will make each test's
output (e.g., System.out, err, printStackTrace, and all logger calls) go into a
file under target/surefire-reports/${className}.txt.
The system property `-DtestOutputToFile=false` can be given on the command line
to get all of this output on the console.
In the IDE (e.g,. IntelliJ), output goes to the Console.
In CircleCI, output goes to files, and those files are concatenated together and
stored as artifacts.
## License
QQQ - Low-code Application Framework for Engineers. \
Copyright (C) 2020-2024. Kingsrook, LLC \

View File

@ -91,7 +91,7 @@ And then having a bug in the check permission logic on the _Light Bulb Inventory
No!
All of the (really important, even though application developers hate doing it) aspects of security - you don't need to write ANY code for dealing with that.
Just tell QQQ what Authentication provider you want to use (e.g., https://auth0.com/[Auth0]), and - to paraphrase the old https://www.youtube.com/watch?v=YHzM4avGrKI[iMac ad] - there's no step 2.
Just tell QQQ what Authentication provider you want to use (e.g., OAuth2 or https://auth0.com/[Auth0]), and - to paraphrase the old https://www.youtube.com/watch?v=YHzM4avGrKI[iMac ad] - there's no step 2.
QQQ just does it.
''''
@ -136,17 +136,13 @@ This speaks to the fact that this "code" is not executable code - but rather is
**** The Filter button in the Query Screen will present a menu listing all fields from the table for the user to build ad-hoc queries against the table.
The data-types specified for the fields (in the meta-data) dictate what operators QQQ allows the user to use against fields (e.g., Strings offer "contains" vs Numbers offer "greater than").
**** Values for records from the table will be formatted for presentation based on the meta-data (such as a numeric field being shown with commas if it represents a quantity, or formatted as currency).
...
[start=2]
. *Meta Data* - declarative code - java object instances (potentially which could be read from `.yaml` files or other data sources in a future version of QQQ), which tell QQQ about the backend systems, tables, processes, reports, widgets, etc, that make up the application.
For example:
* Details about the database you are using, and how to connect to it.
* A database table's name, fields, their types, its keys, and basic business rules (required fields, read-only fields, field lengths).
* The description of web API - its URL and authentication mechanism.
* A table/path within a web API, and the fields returned in the JSON at that endpoint.
* The specification of a custom workflow (process), including what screens are needed, with input & output values, and references to the custom application code for processing the data.
* Details about a chart that summarizes data from a table for presentation as a dashboard widget.
* Other kinds of information that you tell QQQ about in the form of meta-data objects includes:
** Details about the database you are using, and how to connect to it.
** A database table's name, fields, their types, its keys, and basic business rules (required fields, read-only fields, field lengths).
** The specification of a custom workflow (process), including what screens are needed, with input & output values, and references to the custom application code for processing the data.
** Details about a chart that summarizes data from a table for presentation as a dashboard widget.
** The description of web API - its URL and authentication mechanism.
** A table/path within a web API, and the fields returned in the JSON at that endpoint.
// the section below is kinda dumb. like, it says you have to write application code, but
// then it just talks about how your app code gets for-free the same shit that QQQ does.
// it should instead say more about what your custom app code is or does.
@ -164,7 +160,8 @@ For example:
// * The multi-threaded, paged producer/consumer pattern used in standard framework actions is how all custom application actions are also invoked.
// ** For example, the standard QQQ Bulk Edit action uses the same streamed-ETL process that custom application processes can use.
// Meaning your custom processes can take full advantage of the same complex frontend, middleware, and backend structural pieces, and you can just focus on your unique busines logic needs.
2. *Application code* - to customize beyond what the QQQ framework does out-of-the box, and to provide application-specific business-logic.
. *Application code* - to customize beyond what the QQQ framework does out-of-the box, and to provide application-specific business-logic.
QQQ provides its programmers the same classes that it internally uses for record access, resulting in a unified application model.
For example:

View File

@ -155,9 +155,9 @@ new QFilterOrderBy()
----
==== QueryJoin
* `joinTable` - *String, required* - Name of the table that is being joined in to the existing query.
* `joinTable` - *String, required (though inferrable)* - Name of the table that is being joined in to the existing query.
** Will be inferred from *joinMetaData*, if *joinTable* is not set when *joinMetaData* gets set.
* `baseTableOrAlias` - *String, required* - Name of a table (or an alias) already defined in the query, to which the *joinTable* will be joined.
* `baseTableOrAlias` - *String, required (though inferrable)* - Name of a table (or an alias) already defined in the query, to which the *joinTable* will be joined.
** Will be inferred from *joinMetaData*, if *baseTableOrAlias* is not set when *joinMetaData* gets set (which will only use the leftTableName from the joinMetaData - never an alias).
* `joinMetaData` - *QJoinMetaData object* - Optional specification of a {link-join} in the current QInstance.
If not set, will be looked up at runtime based on *baseTableOrAlias* and *joinTable*.
@ -165,21 +165,78 @@ If not set, will be looked up at runtime based on *baseTableOrAlias* and *joinTa
* `alias` - *String* - Optional (unless multiple instances of the same table are being joined together, when it becomes required).
Behavior based on SQL `FROM` clause aliases.
If given, must be used as the part before the dot in field name specifications throughout the rest of the query input.
* `select` - *boolean, default: false* - Specify whether fields from the *rightTable* should be selected by the query.
* `select` - *boolean, default: false* - Specify whether fields from the *joinTable* should be selected by the query.
If *true*, then the `QRecord` objects returned by this query will have values with corresponding to the (table-or-alias `.` field-name) form.
* `type` - *Enum of INNER, LEFT, RIGHT, FULL, default: INNER* - specifies the SQL-style type of join being performed.
[source,java]
.QueryJoin definition examples:
.Basic QueryJoin usage example:
----
// selecting from an "orderLine" table - then join to its corresponding "order" table
// selecting from an "orderLine" table, joined to its corresponding (parent) "order" table
queryInput.withTableName("orderLine");
queryInput.withQueryJoin(new QueryJoin("order").withSelect(true));
...
queryOutput.getRecords().get(0).getValueBigDecimal("order.grandTotal");
----
[source,java]
."V" shaped query - selecting from one parent table, and two children joined to it:
----
// TODO this needs verified for accuracy, though is a reasonable starting point as-is
// selecting from an "order" table, and two children of it, orderLine and customer
queryInput.withTableName("order");
queryInput.withQueryJoin(new QueryJoin("orderLine").withSelect(true));
queryInput.withQueryJoin(new QueryJoin("customer").withSelect(true));
...
QRecord joinedRecord = queryOutput.getRecords().get(0);
joinedRecord.getValueString("orderNo");
joinedRecord.getValueString("orderLine.sku");
joinedRecord.getValueString("customer.firstName");
----
[source,java]
."Chain" shaped query - selecting from one parent table, a child table, and a grandchild:
----
// TODO this needs verified for accuracy, though is a reasonable starting point as-is
// selecting from an "order" table, with a "customer" child table, and an "address" sub-table
queryInput.withTableName("order");
queryInput.withQueryJoin(new QueryJoin("customer").withSelect(true));
queryInput.withQueryJoin(new QueryJoin("address").withSelect(true));
...
QRecord joinedRecord = queryOutput.getRecords().get(0);
joinedRecord.getValueString("orderNo");
joinedRecord.getValueString("customer.firstName");
joinedRecord.getValueString("address.street1");
----
[source,java]
.QueryJoin usage example where two tables have two different joins between them:
----
// TODO this needs verified for accuracy, though is a reasonable starting point as-is
// here there's a "fulfillmentPlan" table, which points at the order table (many-to-one,
// as an order's plan can change over time, and we keep old plans around).
// This join is named: fulfillmentPlanJoinOrder
//
// The other join is "order" pointing at its current "fulfillmentPlan"
// This join is named: orderJoinCurrentFulfillmentPlan
// to select an order along with its current fulfillment plan:
queryInput.withTableName("order");
queryInput.withQueryJoin(new QueryJoin(instance.getJoin("orderJoinCurrentFulfillmentPlan"))
.withSelect(true));
// to select an order, and all fulfillment plans for an order (1 or more records):
queryInput.withTableName("order");
queryInput.withQueryJoin(new QueryJoin(instance.getJoin("fulfillmentPlanJoinOrder"))
.withSelect(true));
----
[source,java]
.QueryJoin usage example for table with two joins to the same child table, selecting from both:
----
// given an "order" table with 2 foreign keys to a customer table (billToCustomerId and shipToCustomerId)
// Note, we must supply the JoinMetaData to the QueryJoin, to drive what fields to join on in each case.
// we must also define an alias for each of the QueryJoins
queryInput.withTableName("order");
queryInput.withQueryJoins(List.of(
new QueryJoin(instance.getJoin("orderJoinShipToCustomer")
@ -190,11 +247,18 @@ queryInput.withQueryJoins(List.of(
.withSelect(true))));
...
record.getValueString("billToCustomer.firstName")
+ " placed an order for "
+ " paid for an order, to be sent to "
+ record.getValueString("shipToCustomer.firstName")
----
[source,java]
.Implicit QueryJoin, where unambiguous and required by QQueryFilter
----
// TODO finish and verify
queryInput.withTableName("order");
----
=== QueryOutput
* `records` - *List of QRecord* - List of 0 or more records that match the query filter.
** _Note: If a *recordPipe* was supplied to the QueryInput, then calling `queryOutput.getRecords()` will result in an `IllegalStateException` being thrown - as the records were placed into the pipe as they were fetched, and cannot all be accessed as a single list._

File diff suppressed because it is too large Load Diff

View File

@ -6,7 +6,10 @@
include::Introduction.adoc[leveloffset=+1]
== Meta Data
== Meta Data Production
include::metaData/MetaDataProduction.adoc[leveloffset=+1]
== Meta Data Types
// Organizational units
include::metaData/QInstance.adoc[leveloffset=+1]
include::metaData/Backends.adoc[leveloffset=+1]
@ -28,11 +31,9 @@ include::metaData/PermissionRules.adoc[leveloffset=+1]
== Services
include::misc/Javalin.adoc[leveloffset=+1]
include::misc/ScheduledJobs.adoc[leveloffset=+1]
=== Web server (Javalin)
#todo#
=== API server (OpenAPI)
#todo#

View File

@ -56,6 +56,37 @@ if the value in the field is longer than the `maxLength`, then one of the follow
----
===== ValueRangeBehavior
Used on Numeric fields. Specifies min and/or max allowed values for the field.
For each of min and max, the following attributes can be set:
* `minValue` / `maxValue` - the number that is the limit.
* `minAllowEqualTo` / `maxAllowEqualTo` - boolean (default true). Controls if < (>) or ≤ (≥).
* `minBehavior` / `maxBehavior` - enum of `ERROR` (default) or `CLIP`.
** If `ERROR`, then a value not within the range causes an error, and the value does not get stored.
** else if `CLIP`, then a value not within the range gets "clipped" to either be the min/max (if allowEqualTo),
or to the min/max plus/minus the clipAmount
* `minClipAmount` / `maxClipAmount` - Default 1. Used when behavior is `CLIP` (only applies when
not allowEqualTo).
[source,java]
.Examples of using ValueRangeBehavior
----
new QFieldMetaData("noOfShoes", QFieldType.INTEGER)
.withBehavior(new ValueRangeBehavior().withMinValue(0));
new QFieldMetaData("price", QFieldType.BIG_DECIMAL)
.withBehavior(new ValueRangeBehavior()
// set the min value to be >= 0, and an error if an input is < 0.
.withMinValue(BigDecimal.ZERO)
.withMinAllowEqualTo(true)
.withMinBehavior(ERROR)
// set the max value to be < 100 - but effectively, clip larger values to 99.99
// here we use the .withMax() method that takes 4 params vs. calling 4 .withMax*() methods.
.withMax(new BigDecimal("100.00"), false, CLIP, new BigDecimal("0.01"))
);
----
===== DynamicDefaultValueBehavior
Used to set a dynamic default value to a field when it is being inserted or updated.
For example, instead of having a hard-coded `defaultValue` specified in the field meta-data,

View File

@ -0,0 +1,413 @@
[#MetaDataProduction]
include::../variables.adoc[]
The first thing that an application built using QQQ needs to do is to define its meta data.
This basically means the construction of a `QInstance` object, which is populated with the
meta data objects defining the backend(s), tables, processes, possible-value sources, joins,
authentication provider, etc, that make your application.
There are various styles that can be used for how you define your meta data, and for the
most part they can be mixed and matched. They will be presented here based on the historical
evolution of how they were added to QQQ, where we generally believe that better techniques have
been added over time. So, you may wish to skip the earlier techniques, and jump straight to
the end of this section. However, it can always be instructive to learn about the past, so,
read at your own pace.
== Omni-Provider
At the most basic level, the way to populate a `QInstance` is the simple and direct approach of creating
one big class, possibly with just one big method, and just doing all the work directly inline there.
This may (clearly) violate several good engineering principles. However, it does have the benefit of
being simple - if all of your meta-data is defined in one place, it can be pretty simple to find where
that place is. So - especially in a small project, this technique may be worth continuing to consider.
Re: "doing all the work" as mentioned above - what work are we talking about? At a minimum, we need
to construct the following meta-data objects, and pass them into our `QInstance`:
* `QAuthenticationMetaData` - how (or if!) users will be authenticated to the application.
* `QBackendMeataData` - a backend data store.
* `QTableMetaData` - a table (within the backend).
* `QAppMetaData` - an organizational unit to present the other elements in a UI.
Here's what a single-method omni-provider could look like:
[source,java]
.About the simplest possible single-file meta-data provider
----
public QInstance defineQInstance()
{
QInstance qInstance = new QInstance();
qInstance.setAuthentication(new QAuthenticationMetaData()
.withName("anonymous")
.withType(QAuthenticationType.FULLY_ANONYMOUS));
qInstance.addBackend(new QBackendMetaData()
.withBackendType(MemoryBackendModule.class)
.withName("memoryBackend"));
qInstance.addTable(new QTableMetaData()
.withName("myTable")
.withPrimaryKeyField("id")
.withBackendName("memoryBackend")
.withField(new QFieldMetaData("id", QFieldType.INTEGER)));
qInstance.addApp(new QAppMetaData()
.withName("myApp")
.withSectionOfChildren(new QAppSection().withName("mySection"),
qInstance.getTable("myTable")))
return (qInstance);
}
----
== Multi-method Omni-Provider
The next evolution of meta-data production comes by just applying some basic better-engineering
principles, and splitting up from a single method that constructs all the things, to at least
using unique methods to construct each thing, then calling those methods to add their results
to the QInstance.
[source,java]
.Multi-method omni- meta-data provider
----
public QInstance defineQInstance()
{
QInstance qInstance = new QInstance();
qInstance.setAuthentication(defineAuthenticationMetaData());
qInstance.addBackend(defineBackendMetaData());
qInstance.addTable(defineMyTableMetaData());
qInstance.addApp(defineMyAppMetaData(qInstance));
return qInstance;
}
public QAuthenticationMetaData defineAuthenticationMetaData()
{
return new QAuthenticationMetaData()
.withName("anonymous")
.withType(QAuthenticationType.FULLY_ANONYMOUS);
}
public QBackendMetaData defineBackendMetaData()
{
return new QBackendMetaData()
.withBackendType(MemoryBackendModule.class)
.withName("memoryBackend");
}
// implementations of defineMyTableMetaData() and defineMyAppMetaData(qInstance)
// left as an exercise for the reader
----
== Multi-class Providers
Then the next logical evolution would be to put each of these single meta-data producing
objects into its own class, along with calls to those classes. This gets us away from the
"5000 line" single-class, and lets us stop using the word "omni":
[source,java]
.Multi-class meta-data providers
----
public QInstance defineQInstance()
{
QInstance qInstance = new QInstance();
qInstance.setAuthentication(new AuthMetaDataProvider().defineAuthenticationMetaData());
qInstance.addBackend(new BackendMetaDataProvider().defineBackendMetaData());
qInstance.addTable(new MyTableMetaDataProvider().defineTableMetaData());
qInstance.addApp(new MyAppMetaDataProvider().defineAppMetaData(qInstance));
return qInstance;
}
public class AuthMetaDataProvider
{
public QAuthenticationMetaData defineAuthenticationMetaData()
{
return new QAuthenticationMetaData()
.withName("anonymous")
.withType(QAuthenticationType.FULLY_ANONYMOUS);
}
}
public class BackendMetaDataProvider
{
public QBackendMetaData defineBackendMetaData()
{
return new QBackendMetaData()
.withBackendType(MemoryBackendModule.class)
.withName("memoryBackend");
}
}
// implementations of MyTableMetaDataProvider and MyAppMetaDataProvider
// left as an exercise for the reader
----
== MetaDataProducerInterface
As the size of your application grows, if you're doing per-object meta-data providers, you may find it
burdensome, when adding a new object to your instance, to have to write code for it in two places -
that is - a new class to produce that meta-data object, AND a single line of code to add that object
to your `QInstance`. As such, a mechanism exists to let you avoid that line-of-code for adding the object
to the `QInstance`.
This mechanism involves adding the `MetaDataProducerInterface` to all of your classes that produce a
meta-data object. This interface is generic, with a type parameter that will typically be the type of
meta-data object you are producing, such as `QTableMetaData`, `QProcessMetaData`, or `QWidgetMetaData`,
(technically, any class which implements `TopLevelMetaData`). Implementers of the interface are then
required to override just one method: `T produce(QInstance qInstance) throws QException;`
Once you have your `MetaDataProducerInterface` classes defined, then there's a one-time call needed
to add all of the objects produced by these classes to your `QInstance` - as shown here:
[source,java]
.Using MetaDataProducerInterface
----
public QInstance defineQInstance()
{
QInstance qInstance = new QInstance();
MetaDataProducerHelper.processAllMetaDataProducersInPackage(qInstance,
"com.mydomain.myapplication");
return qInstance;
}
public class AuthMetaDataProducer implements MetaDataProducerInterface<QAuthenticationMetaData>
{
@Override
public QAuthenticationMetaData produce(QInstance qInstance)
{
return new QAuthenticationMetaData()
.withName("anonymous")
.withType(QAuthenticationType.FULLY_ANONYMOUS);
}
}
public class BackendMetaDataProducer implements MetaDataProducerInterface<QBackendMetaData>
{
@Override
public QBackendMetaData defineBackendMetaData()
{
return new QBackendMetaData()
.withBackendType(MemoryBackendModule.class)
.withName("memoryBackend");
}
}
// implementations of MyTableMetaDataProvider and MyAppMetaDataProvider
// left as an exercise for the reader
----
=== MetaDataProducerMultiOutput
It is worth mentioning, that sometimes it might feel like a bridge that's a bit too far, to make
every single one of your meta-data objects require its own class. Some may argue that it's best
to do it that way - single responsibility principle, etc. But, if you're producing, say, 5 widgets
that are all related, and it's only a handful of lines of code for each one, maybe you'd rather
produce them all in the same class. Or maybe when you define a table, you'd like to define its
joins and widgets at the same time.
This approach can be accomplished by making the type argument for your `MetaDataProducerInterface` be
`MetaDataProducerMultiOutput` - a simple class that just wraps a list of other `MetaDataProducerOutput`
objects.
[source,java]
.Returning a MetaDataProducerMultiOutput
----
public class MyMultiProducer implements MetaDataProducerInterface<MetaDataProducerMultiOutput>
{
@Override
public MetaDataProducerMultiOutput produce(QInstance qInstance)
{
MetaDataProducerMultiOutput output = new MetaDataProducerMultiOutput();
output.add(new QPossibleValueSource()...);
output.add(new QJoinMetaData()...);
output.add(new QJoinMetaData()...);
output.add(new QWidgetMetaData()...);
output.add(new QTableMetaData()...);
return (output);
}
}
----
== Aside: TableMetaData with RecordEntities
At this point, let's take a brief aside to dig deeper into the creation of a `QTableMeta` object.
Tables, being probably the most important meta-data type in QQQ, have a lot of information that can
be specified in their meta-data object.
At the same time, if you're writing any custom code in your QQQ application
(e.g., any processes or table customizers), where you're working with records from tables, you may
prefer being able to work with entity beans (e.g., java classes with typed getter & setter methods),
rather than the default object type that QQQ's ORM actions return, the `QRecord`, which carries all
of its values in a `Map` (where you don't get compile-time checks of field names or data types).
QQQ has a mechanism for dealing with this - in the form of the `QRecordEntity` class.
So - if you want to build your application using entity beans (which is recommended, for the compile-time
safety that they provide in custom code), you will be writing a `QRecordEntity` class for each of your tables,
which will look like:
[source,java]
.QRecordEntity example
----
public class MyTable extends QRecordEntity
{
public static final String TABLE_NAME = "myTable";
@QField(isEditable = false, isPrimaryKey = true)
private Integer id;
@QField()
private String name;
// no-arg constructor and constructor that takes a QRecord
// getters & setters (and optional fluent setters)
}
----
The point of introducing this topic here and now is, that a `QRecordEntity` can be used to shortcut to
defining some of the attributes in a `QTableMetaData` object. Specifically, in a `MetaDataProducer<QTableMetaData>`
you may say:
[source,java]
.QTableMetaDataProducer using a QRecordEntity
----
public QTableMetaData produce(QInstance qInstance) throws QExcpetion
{
return new QTableMetaData()
.withName(MyTable.TABLE_NAME)
.withFieldsFromEntity(MyTable.class)
.withBackendName("memoryBackend");
}
----
That `withFieldsFromEntity` call is one of the biggest benefits of this technique. It allows you to avoid defining
all of the fields in you table in two places (the entity and the table meta-data).
== MetaData Producing Annotations for Entities
If you are using `QRecordEntity` classes that correspond to your tables, then you can take advantage of some
additional annotations on those classes, to produce more related meta-data objects associated with those tables.
The point of this is to eliminate boilerplate, and simplify / speed up the process of getting a new table
built and deployed in your application.
Furthermore, the case can be made that it is beneficial to keep the meta-data definition for a table as close
as possible to the entity that corresponds to the table. This enables modifications to the table (e.g., adding
a new field/column) to only require edits in one java source file, rather than necessarily requiring edits
in two files.
=== @QMetaDataProducingEntity
This is an annotation meant to be placed on a `QRecordEntity` subclass, which you would like to be
processed by an invocation of `MetaDataProducerHelper`, to automatically produce some meta-data
objects.
This annotation supports:
* Creating table meta-data for the corresponding record entity table. Enabled by setting `produceTableMetaData=true`.
** One may customize the table meta data that is produced automatically by supplying a class that extends
`MetaDataCustomizerInterface` in the annotation attribute `tableMetaDataCustomizer`.
** In addition to (or as an alternative to) the per-table `MetaDataCustomizerInterface` that can be specified
in `@QMetaDataProducingEntity.tableMetaDataCustomzier`, when an application calls
`MetaDataProducerHelper.processAllMetaDataProducersInPackage`, an additional `MetaDataCustomizerInterface` can be
given, to apply a common set of adjustments to all tales being generated by the call.
* Making a possible-value-source out of the table. Enabled by setting `producePossibleValueSource=true`.
* Processing child tables to create joins and childRecordList widgets
=== @ChildTable
This is an annotation used as a value that goes inside a `@QMetadataProducingEntity` annotation, to define
child-tables, e.g., for producing joins and childRecordList widgets related to the table defined in the entity class.
==== @ChildJoin
This is an annotation used as a value inside a `@ChildTable` inside a `@QMetadataProducingEntity` annotation,
to control the generation of a `QJoinMetaData`, as a `ONE_TO_MANY` type join from the table represented by
the annotated entity, to the table referenced in the `@ChildTable` annotation.
==== @ChildRecordListWidget
This is an annotation used as a value that goes inside a `@QMetadataProducingEntity` annotation, to control
the generation of a QWidgetMetaData - for a ChildRecordList widget.
[source,java]
.QRecordEntity with meta-data producing annotations and a table MetaDataCustomizer
----
@QMetaDataProducingEntity(
produceTableMetaData = true,
tableMetaDataCustomizer = MyTable.TableMetaDataCustomizer.class,
producePossibleValueSource = true,
childTables = {
@ChildTable(
childTableEntityClass = MyChildTable.class,
childJoin = @ChildJoin(enabled = true),
childRecordListWidget = @ChildRecordListWidget(enabled = true, label = "Children"))
}
)
public class MyTable extends QRecordEntity
{
public static final String TABLE_NAME = "myTable";
public static class TableMetaDataCustomizer implements MetaDataCustomizerInterface<QTableMetaData>
{
@Override
public QTableMetaData customizeMetaData(QInstance qInstance, QTableMetaData table) throws QException
{
String childJoinName = QJoinMetaData.makeInferredJoinName(TABLE_NAME, MyChildTable.TABLE_NAME);
table
.withUniqueKey(new UniqueKey("name"))
.withIcon(new QIcon().withName("table_bar"))
.withRecordLabelFormat("%s")
.withRecordLabelFields("name")
.withSection(new QFieldSection("identity", new QIcon().withName("badge"), Tier.T1,
List.of("id", "name")))
// todo additional sections for other fields
.withSection(new QFieldSection("children", new QIcon().withName("account_tree"), Tier.T2)
.withWidgetName(childJoinName))
.withExposedJoin(new ExposedJoin()
.withLabel("Children")
.withJoinPath(List.of(childJoinName))
.withJoinTable(MyChildTable.TABLE_NAME));
return (table);
}
}
@QField(isEditable = false, isPrimaryKey = true)
private Integer id;
// remaining fields, constructors, getters & setters left as an exercise for the reader and/or the IDE
}
----
The class given in the example above, if processed by the `MetaDataProducerHelper`, would add the following
meta-data objects to your `QInstance`:
* A `QTableMetaData` named `myTable`, with all fields annotated as `@QField` from the `QRecordEntity` class,
and with additional attributes as set in the `TableMetaDataCustomizer` inner class.
* A `QPossibleValueSource` named `myTable`, of type `TABLE`, with `myTable` as its backing table.
* A `QJoinMetaData` named `myTableJoinMyChildTable`, as a `ONE_TO_MANY` type, between those two tables.
* A `QWidgetMetaData` named `myTableJoinMyChildTable`, as a `CHILD_RECORD_LIST` type, that will show a list of
records from `myChildTable` as a widget, when viewing a record from `myTable`.
== Other MetaData Producing Annotations
Similar to these annotations for a `RecordEntity`, a similar one exists for a `PossibleValueEnum` class,
to automatically write the meta-data to use that enum as a possible value source in your application:
=== @QMetaDataProducingPossibleValueEnum
This is an annotation to go on a `PossibleValueEnum` class, which you would like to be
processed by MetaDataProducerHelper, to automatically produce a PossibleValueSource meta-data
based on the enum.
[source,java]
.PossibleValueEnum with meta-data producing annotation
----
@QMetaDataProducingPossibleValueEnum(producePossibleValueSource = true)
public enum MyOptionsEnum implements PossibleValueEnum<Integer>
{
// values and methods left as exercise for reader
}
----
The enum given in the example above, if processed by the `MetaDataProducerHelper`, would add the following
meta-data object to your `QInstance`:
* A `QPossibleValueSource` named `MyOptionsEnum`, of type `ENUM`, with `MyOptionsEnum` as its backing enum.

View File

@ -38,6 +38,13 @@ See {link-permissionRules} for details.
*** 1) by a single call to `.withStepList(List<QStepMetaData>)`, which internally adds each step into the `steps` map.
*** 2) by multiple calls to `.addStep(QStepMetaData)`, which adds a step to both the `stepList` and `steps` map.
** If a process also needs optional steps (for a <<_custom_process_flow>>), they should be added by a call to `.addOptionalStep(QStepMetaData)`, which only places them in the `steps` map.
* `stepFlow` - *enum, default LINEAR* - specifies the the flow-control logic between steps. Possible values are:
** `LINEAR` - steps are executed in-order, through the `stepList`.
A backend step _can_ customize the `nextStepName` or re-order the `stepList`, if needed.
In a frontend step, a user may be given the option to go _back_ to a previous step as well.
** `STATE_MACHINE` - steps are executed as a Fine State Machine, starting with the first step in `stepList`,
but then proceeding based on the `nextStepName` specified by the previous step.
Thus allowing much more flexible flows.
* `schedule` - *<<QScheduleMetaData>>* - set up the process to run automatically on the specified schedule.
See below for details.
* `minInputRecords` - *Integer* - #not used...#
@ -67,6 +74,11 @@ For processes with a user-interface, they must define one or more "screens" in t
* `formFields` - *List of String* - list of field names used by the screen as form-inputs.
* `viewFields` - *List of String* - list of field names used by the screen as visible outputs.
* `recordListFields` - *List of String* - list of field names used by the screen in a record listing.
* `format` - *Optional String* - directive for a frontend to use specialized formatting for the display of the process.
** Consult frontend documentation for supported values and their meanings.
* `backStepName` - *Optional String* - For processes using `LINEAR` flow, if this value is given,
then the frontend should offer a control that the user can take (e.g., a button) to move back to an
earlier step in the process.
==== QFrontendComponentMetaData
@ -90,10 +102,13 @@ Expects a process value named `html`.
Expects process values named `downloadFileName` and `serverFilePath`.
** `GOOGLE_DRIVE_SELECT_FOLDER` - Special form that presents a UI from Google Drive, where the user can select a folder (e.g., as a target for uploading files in a subsequent backend step).
** `BULK_EDIT_FORM` - For use by the standard QQQ Bulk Edit process.
** `BULK_LOAD_FILE_MAPPING_FORM`, `BULK_LOAD_VALUE_MAPPING_FORM`, or `BULK_LOAD_PROFILE_FORM` - For use by the standard QQQ Bulk Load process.
** `VALIDATION_REVIEW_SCREEN` - For use by the QQQ Streamed ETL With Frontend process family of processes.
Displays a component prompting the user to run full validation or to skip it, or, if full validation has been ran, then showing the results of that validation.
** `PROCESS_SUMMARY_RESULTS` - For use by the QQQ Streamed ETL With Frontend process family of processes.
Displays the summary results of running the process.
** `WIDGET` - Render a QQQ Widget.
Requires that `widgetName` be given as a value for the component.
** `RECORD_LIST` - _Deprecated.
Showed a grid with a list of records as populated by the process._
* `values` - *Map of String → Serializable* - Key=value pairs, with different expectations based on the component's `type`.
@ -116,6 +131,27 @@ It can be used, however, for example, to cause a `defaultValue` to be applied to
It can also be used to cause the process to throw an error, if a field is marked as `isRequired`, but a value is not present.
** `recordListMetaData` - *RecordListMetaData object* - _Not used at this time._
==== QStateMachineStep
Processes that use `flow = STATE_MACHINE` should use process steps of type `QStateMachineStep`.
A common pattern seen in state-machine processes, is that they will present a frontend-step to a user,
then always run a given backend-step in response to that screen which the user submitted.
Inside that backend-step, custom application logic will determine the next state to go to,
which is typically another frontend-step (which would then submit data to its corresponding backend-step,
and continue the FSM).
To help facilitate this pattern, factory methods exist on `QStateMachineStep`,
for constructing the commonly-expected types of state-machine steps:
* `frontendThenBackend(name, frontendStep, backendStep)` - for the frontend-then-backend pattern described above.
* `backendOnly(name, backendStep)` - for a state that only has a backend step.
This might be useful as a “reset” step, to run before restarting a state-loop.
* `frontendOnly(name, frontendStep)` - for a state that only has a frontend step,
which would always be followed by another state, which must be specified as the `defaultNextStepName`
on the `QStateMachineStep`.
==== BasepullConfiguration
A "Basepull" process is a common pattern where an application needs to perform some action on all new (or updated) records from a particular data source.
@ -218,12 +254,10 @@ But for some cases, doing page-level transactions can reduce long-transactions a
* `withSchedule(QScheduleMetaData schedule)` - Add a <<QScheduleMetaData>> to the process.
[#_custom_process_flow]
==== Custom Process Flow
As referenced in the definition of the <<_QProcessMetaData_Properties,QProcessMetaData Properties>>, by default, a process
will execute each of its steps in-order, as defined in the `stepList` property.
However, a Backend Step can customize this flow #todo - write more clearly here...
There are generally 2 method to call (in a `BackendStep`) to do a dynamic flow:
==== How to customize a Linear process flow
As referenced in the definition of the <<_QProcessMetaData_Properties,QProcessMetaData Properties>>, by default,
(with `flow = LINEAR`) a process will execute each of its steps in-order, as defined in the `stepList` property.
However, a Backend Step can customize this flow as follows:
* `RunBackendStepOutput.setOverrideLastStepName(String stepName)`
** QQQ's `RunProcessAction` keeps track of which step it "last" ran, e.g., to tell it which one to run next.
@ -239,7 +273,7 @@ does need to be found in the new `stepNameList` - otherwise, the framework will
for figuring out where to go next.
[source,java]
.Example of a defining process that can use a flexible flow:
.Example of a defining process that can use a customized linear flow:
----
// for a case like this, it would be recommended to define all step names in constants:
public final static String STEP_START = "start";
@ -324,4 +358,21 @@ public static class StartStep implements BackendStep
}
----
[#_process_back]
==== How to allow a process to go back
The simplest option to allow a process to present a "Back" button to users,
thus allowing them to move backward through a process
(e.g., from a review screen back to an earlier input screen), is to set the property `backStepName`
on a `QFrontendStepMetaData`.
If the step that is executed after the user hits "Back" is a backend step, then within that
step, `runBackendStepInput.getIsStepBack()` will return `true` (but ONLY within that first step after
the user hits "Back"). It may be necessary within individual processes to be aware that the user
has chosen to go back, to reset certain values in the process's state.
Alternatively, if a frontend step's "Back" behavior needs to be dynamic (e.g., sometimes not available,
or sometimes targeting different steps in the process), then in a backend step that runs before the
frontend step, a call to `runBackendStepOutput.getProcessState().setBackStepName()` can be made,
to customize the value which would otherwise come from the `QFrontendStepMetaData`.

View File

@ -29,11 +29,21 @@ service.routes(qJavalinImplementation.getRoutes());
service.start();
----
*QBackendMetaData Setup Methods:*
*QInstance Setup:*
These are the methods that one is most likely to use when setting up (defining) a `QInstance` object:
* asdf
* `add(TopLevelMetaDataInterface metaData)` - Generic method that takes most of the meta-data subtypes that can be added
to an instance, such as `QBackendMetaData`, `QTableMetaData`, `QProcessMetaData`, etc.
There are also type-specific methods (e.g., `addTable`, `addProcess`, etc), which one can call instead - this would just
be a matter of personal preference.
*QBackendMetaData Usage Methods:*
*QInstance Usage:*
Generally you will set up a `QInstance` in your application's startup flow, and then place it in the server (e.g., javalin).
But, if, during application-code runtime, you need access to any of the meta-data in the instance, you access it
via the `QContext` object's static `getInstance()` method. This can be useful, for example, to get a list of the defined
tables in the application, or fields in a table, or details about a field, etc.
It is generally considered risky and/or not a good idea at all to modify the `QInstance` after it has been validated and
a server is running. Future versions of QQQ may in fact restrict modifications to the instance after validation.

109
docs/misc/Javalin.adoc Normal file
View File

@ -0,0 +1,109 @@
== QQQ Middleware: Javalin web server
include::../variables.adoc[]
QQQ provides a standard implementation of a middleware layer - that is - code that exists between the
QQQ backend and user interface. This implementation is a web server built using the https://javalin.io/[Javalin framework],
packaged and deployed in the `qqq-middleware-javalin` maven module
The de facto way to create a QQQ application server is to write a class which uses an instance of one of the
subclasses of `QApplicationJavalinServer`.
For example, if your application metadata is defined in a directory of yaml files, your server class could be implemented as:
[source,java]
.ConfigFileBasedQQQApplication usage example
----
public static void main(String[] args)
{
try
{
String path = "src/main/resources/metadata";
ConfigFilesBasedQQQApplication application = new ConfigFilesBasedQQQApplication(path);
QApplicationJavalinServer javalinServer = new QApplicationJavalinServer(application);
javalinServer.start();
}
catch(Exception e)
{
LOG.error("Failed to start javalin server. See stack trace for details.", e);
}
}
----
A similar class exists if your metadata is produced by a package of Java MetaDataProducer objects: `MetaDataProducerBasedQQQApplication`.
=== QApplicationJavalinServer
This class provides the bridge between your QQQ Application (e.g., your metadata) and the QQQ Middleware layer
served by a Javalin web server. It has several properties to control behaviors:
* `Integer port` - (default `8000`) - port to use for serving HTTP.
* `boolean serveFrontendMaterialDashboard` - (default `true`) whether to serve the javascript frontend provided
in the maven artifact `qqq-frontend-material-dashboard`.
* `boolean serveLegacyUnversionedMiddlewareAPI` - (default `true`) whether to serve a version the original implementation
of the QQQ middleware, which current version of `qqq-frontend-material-dashboard` are compatible with.
* `List<AbstractMiddlewareVersion> middlewareVersionList` - (default contains `MiddlewareVersionV1`) - list of
newer, formally versioned implementations of the QQQ middleware interface to be served.
* `Consumer<Javalin> javalinConfigurationCustomizer` - (default `null`) - optional hook to customize the
javalin service object before it is started.
* `List<QJavalinRouteProviderInterface> additionalRouteProviders` - (default `null`) - list of fully custom
implementations of `QJavalinRouteProviderInterface`, to add additional endpoints to the javalin server.
** _Note, you may first want to consider using JavalinRouteProviderMetaData instead - see below._
* `QJavalinMetaData javalinMetaData` - (default `null`) - optional alternative place to define `JavalinMetaData` (vs.
defining it in the `QInstance`). _Note that if it is set in both places, the one in the QApplicationJavalinServer
is used._
=== JavalinMetaData
Certain behaviors of a QQQ Javalin server are configured in a declarative manner by adding a `QJavalinMetaData`
object to the `supplementalMetaData` in your `QInstance` (or, as mentioned above, by setting it directly on the
`QApplicationJavalinServer`):
* `List<JavalinRouteProviderMetaData> routeProviders` - (default `null`) optional list of custom route providers to
add to the Javalin server. See below for details.
* `String uploadedFileArchiveTableName` - (default `null`) - reference to a QQQ Table in your application instance,
needed to support the Bulk Load process, as well as any other processes which need to accept an uploaded file
as input.
* `boolean loggerDisabled` - (default `false`)
* `Function<QJavalinAccessLogger.LogEntry, Boolean> logFilter` - (default `null`)
* `boolean queryWithoutLimitAllowed` - (default `false`)
* `Integer queryWithoutLimitDefault` - (default `1000`)
* `Level queryWithoutLimitLogLevel` - (default `INFO`)
==== JavalinRouteProviderMetaData
This type of metadata allows you to add additional http route providers to your Javalin instance, e.g., for
serving static files or for running custom code from your application (in the form of QQQ Processes) to respond
to HTTP requests.
* `String hostedPath` - (required)
* `String fileSystemPath` - (required for a static router)
* `String processName` - required for a dynamic, process-based router. Must be a process name within the QQQ Instance.
See below for additional details
* `List<String> methods` - required list of HTTP methods (verbs) that are served by the route provider
* `QCodeReference routeAuthenticator - Optional reference to a class that implements `RouteAuthenticatorInterface`,
to provide security authentication to all requests handled by the route provider.
** A default implementation is provided as `SimpleRouteAuthenticator`, which requires that a user session be present
to access paths served by the route provider.
===== Process-based route provider processes
If you define a `JavalinRouteProviderMetaData` with a `processName` (e.g., to serve dynamic HTTP responses from your javalin
server), the process that you implement will be called to respond to any HTTP requests received by the javalin
server which match the `hostedPath` and `methods` that are specified in the metadata.
The QQQ javalin server will marshal request data from the javalin context into the process's payload, conforming to
the shape of the `ProcessBasedRouterPayload` class. Similarly, the http response will be built by taking values from
the process's output/state conforming to the fields in that class. As such, it is recommended to use a
`ProcessBasedRouterPayload` instance, as show in this example:
[source,java]
.Process-based router usage example (including ProcessBasedRouterPayload)
----
public class MyDynamicSiteProcessStep implements BackendStep
{
@Override
public void run(RunBackendStepInput input, RunBackendStepOutput output) throws QException
{
ProcessBasedRouterPayload payload = input.getProcessPayload(ProcessBasedRouterPayload.class);
String path = payload.getPath();
payload.setResponseString("You requested: " + path);
output.setProcessPayload(payload);
}
}
----

35
pom.xml
View File

@ -34,6 +34,7 @@
<module>qqq-backend-module-api</module>
<module>qqq-backend-module-filesystem</module>
<module>qqq-backend-module-rdbms</module>
<module>qqq-backend-module-sqlite</module>
<module>qqq-backend-module-mongodb</module>
<module>qqq-language-support-javascript</module>
<module>qqq-openapi</module>
@ -47,7 +48,7 @@
</modules>
<properties>
<revision>0.23.0-SNAPSHOT</revision>
<revision>0.26.0-SNAPSHOT</revision>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
@ -58,6 +59,7 @@
<coverage.instructionCoveredRatioMinimum>0.80</coverage.instructionCoveredRatioMinimum>
<coverage.classCoveredRatioMinimum>0.95</coverage.classCoveredRatioMinimum>
<plugin.shade.phase>none</plugin.shade.phase>
<testOutputToFile>true</testOutputToFile>
</properties>
<profiles>
@ -140,6 +142,8 @@
<configuration>
<!-- Sets the VM argument line used when integration tests are run. -->
<argLine>@{jaCoCoArgLine}</argLine>
<!-- Reduce console output for cleaner JUnit output -->
<redirectTestOutputToFile>${testOutputToFile}</redirectTestOutputToFile>
</configuration>
</plugin>
<plugin>
@ -243,30 +247,29 @@ if [ ! -e target/site/jacoco/index.html ]; then
fi
echo
echo "Jacoco coverage summary report:"
echo "Jacoco coverage summary report for module: ${project.artifactId}"
echo " See also target/site/jacoco/index.html"
echo " and https://www.jacoco.org/jacoco/trunk/doc/counters.html"
echo "------------------------------------------------------------"
if which xpath > /dev/null 2>&1; then
echo "Element\nInstructions Missed\nInstruction Coverage\nBranches Missed\nBranch Coverage\nComplexity Missed\nComplexity Hit\nLines Missed\nLines Hit\nMethods Missed\nMethods Hit\nClasses Missed\nClasses Hit\n" > /tmp/$$.headers
xpath -n -q -e '/html/body/table/tfoot/tr[1]/td/text()' target/site/jacoco/index.html > /tmp/$$.values
# Parse Jacoco HTML coverage summary
if [ -f target/site/jacoco/index.html ]; then
echo -e "Instructions Missed\nInstruction Coverage\nBranches Missed\nBranch Coverage\nComplexity Missed\nComplexity Hit\nLines Missed\nLines Hit\nMethods Missed\nMethods Hit\nClasses Missed\nClasses Hit\n" > /tmp/$$.headers
sed 's/<\/\w\+>/&\n/g' target/site/jacoco/index.html | grep -A 12 '<tfoot>' | grep '<td' | sed 's/<td class="\w\+\d*">\([^<]*\)<\/td>/\1/' | grep -v Total > /tmp/$$.values
paste /tmp/$$.headers /tmp/$$.values | tail +2 | awk -v FS='\t' '{printf("%-20s %s\n",$1,$2)}'
rm /tmp/$$.headers /tmp/$$.values
else
echo "xpath is not installed. Jacoco coverage summary will not be produced here...";
echo "Jacoco coverage summary was not found.";
fi
echo "-----------------------------"
echo
if which html2text > /dev/null 2>&1; then
echo "Untested classes, per Jacoco:"
echo "-----------------------------"
for i in target/site/jacoco/*/index.html; do
html2text -width 500 -nobs $i | sed '1,/^Total/d;' | grep -v Created | sed 's/ \+/ /g' | sed 's/ [[:digit:]]$//' | grep -v 0$ | cut -d' ' -f1;
done;
echo
else
echo "html2text is not installed. Untested classes from Jacoco will not be printed here...";
fi
echo "Untested classes, per Jacoco for module: ${project.artifactId}"
echo "-----------------------------"
# Parse Jacoco XML reports directly to find classes with 0% coverage
sed 's/<classs .*\?>/&\n/g;s/<\/class>/&\n/g' target/site/jacoco/jacoco.xml | grep -v 'counter type="CLASS" missed="0"' | sed 's/>.*//;s/.*\///;s/".*//'
echo "-----------------------------"
echo
]]>
</argument>

View File

@ -65,7 +65,11 @@
<artifactId>aws-java-sdk-secretsmanager</artifactId>
<version>1.12.385</version>
</dependency>
<dependency>
<groupId>com.ibm.icu</groupId>
<artifactId>icu4j</artifactId>
<version>77.1</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
@ -100,7 +104,12 @@
<dependency>
<groupId>org.dhatim</groupId>
<artifactId>fastexcel</artifactId>
<version>0.12.15</version>
<version>0.18.4</version>
</dependency>
<dependency>
<groupId>org.dhatim</groupId>
<artifactId>fastexcel-reader</artifactId>
<version>0.18.4</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
@ -112,10 +121,29 @@
<artifactId>poi-ooxml</artifactId>
<version>5.2.5</version>
</dependency>
<dependency>
<groupId>org.commonmark</groupId>
<artifactId>commonmark</artifactId>
<version>0.25.0</version>
</dependency>
<!-- adding to help FastExcel -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.16.0</version>
</dependency>
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>oauth2-oidc-sdk</artifactId>
<version>11.23.1</version>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>auth0</artifactId>
<version>2.1.0</version>
<version>2.18.0</version>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
@ -125,12 +153,12 @@
<dependency>
<groupId>com.auth0</groupId>
<artifactId>jwks-rsa</artifactId>
<version>0.22.0</version>
<version>0.22.1</version>
</dependency>
<dependency>
<groupId>io.github.cdimascio</groupId>
<artifactId>java-dotenv</artifactId>
<version>5.2.2</version>
<artifactId>dotenv-java</artifactId>
<version>3.2.0</version>
</dependency>
<dependency>
<groupId>org.apache.velocity</groupId>
@ -138,16 +166,21 @@
<version>2.3</version>
</dependency>
<!-- the next 2 deps are for html to pdf - per https://www.baeldung.com/java-html-to-pdf -->
<!-- the next 3 deps are for html to pdf -->
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.15.3</version>
</dependency>
<dependency>
<groupId>org.xhtmlrenderer</groupId>
<artifactId>flying-saucer-pdf-openpdf</artifactId>
<version>9.1.22</version>
<groupId>com.openhtmltopdf</groupId>
<artifactId>openhtmltopdf-core</artifactId>
<version>1.0.10</version>
</dependency>
<dependency>
<groupId>com.openhtmltopdf</groupId>
<artifactId>openhtmltopdf-pdfbox</artifactId>
<version>1.0.10</version>
</dependency>
<!-- the next 3 deps are being added for google drive support -->

View File

@ -186,7 +186,7 @@ public class AsyncRecordPipeLoop
if(recordCount > 0)
{
LOG.info("End of job summary", logPair("recordCount", recordCount), logPair("jobName", jobName), logPair("millis", endTime - jobStartTime), logPair("recordsPerSecond", 1000d * (recordCount / (.001d + (endTime - jobStartTime)))));
LOG.debug("End of job summary", logPair("recordCount", recordCount), logPair("jobName", jobName), logPair("millis", endTime - jobStartTime), logPair("recordsPerSecond", 1000d * (recordCount / (.001d + (endTime - jobStartTime)))));
}
return (recordCount);

View File

@ -44,6 +44,7 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.security.MultiRecordSecurityLock;
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLockFilters;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
@ -173,26 +174,53 @@ public class AuditAction extends AbstractQActionFunction<AuditInput, AuditOutput
Map<String, Serializable> securityKeyValues = new HashMap<>();
for(RecordSecurityLock recordSecurityLock : RecordSecurityLockFilters.filterForReadLocks(CollectionUtils.nonNullList(table.getRecordSecurityLocks())))
{
Serializable keyValue = record == null ? null : record.getValue(recordSecurityLock.getFieldName());
if(keyValue == null && oldRecord.isPresent())
{
LOG.debug("Table with a securityLock, but value not found in field", logPair("table", table.getName()), logPair("field", recordSecurityLock.getFieldName()));
keyValue = oldRecord.get().getValue(recordSecurityLock.getFieldName());
}
if(keyValue == null)
{
LOG.debug("Table with a securityLock, but value not found in field", logPair("table", table.getName()), logPair("field", recordSecurityLock.getFieldName()), logPair("oldRecordIsPresent", oldRecord.isPresent()));
}
securityKeyValues.put(recordSecurityLock.getSecurityKeyType(), keyValue);
getRecordSecurityKeyValues(table, record, oldRecord, recordSecurityLock, securityKeyValues);
}
return securityKeyValues;
}
/***************************************************************************
** recursive implementation of getRecordSecurityKeyValues, for dealing with
** multi-locks
***************************************************************************/
private static void getRecordSecurityKeyValues(QTableMetaData table, QRecord record, Optional<QRecord> oldRecord, RecordSecurityLock recordSecurityLock, Map<String, Serializable> securityKeyValues)
{
//////////////////////////////////////////////////////
// special case with recursive call for multi-locks //
//////////////////////////////////////////////////////
if(recordSecurityLock instanceof MultiRecordSecurityLock multiRecordSecurityLock)
{
for(RecordSecurityLock subLock : RecordSecurityLockFilters.filterForReadLocks(CollectionUtils.nonNullList(multiRecordSecurityLock.getLocks())))
{
getRecordSecurityKeyValues(table, record, oldRecord, subLock, securityKeyValues);
}
return;
}
///////////////////////////////////////////
// by default, deal with non-multi locks //
///////////////////////////////////////////
Serializable keyValue = record == null ? null : record.getValue(recordSecurityLock.getFieldName());
if(keyValue == null && oldRecord.isPresent())
{
LOG.debug("Table with a securityLock, but value not found in field", logPair("table", table.getName()), logPair("field", recordSecurityLock.getFieldName()));
keyValue = oldRecord.get().getValue(recordSecurityLock.getFieldName());
}
if(keyValue == null)
{
LOG.debug("Table with a securityLock, but value not found in field", logPair("table", table.getName()), logPair("field", recordSecurityLock.getFieldName()), logPair("oldRecordIsPresent", oldRecord.isPresent()));
}
securityKeyValues.put(recordSecurityLock.getSecurityKeyType(), keyValue);
}
/*******************************************************************************
**
*******************************************************************************/
@ -218,21 +246,16 @@ public class AuditAction extends AbstractQActionFunction<AuditInput, AuditOutput
throw (new QException("Requested audit for an unrecognized table name: " + auditSingleInput.getAuditTableName()));
}
///////////////////////////////////////////////////
// validate security keys on the table are given //
///////////////////////////////////////////////////
for(RecordSecurityLock recordSecurityLock : RecordSecurityLockFilters.filterForReadLocks(CollectionUtils.nonNullList(table.getRecordSecurityLocks())))
///////////////////////////////////////////////////////
// validate security keys on the table are given //
// originally, this case threw... //
// but i think it's better to record the audit, just //
// missing its security key value, then to fail... //
// but, maybe should be configurable, etc... //
///////////////////////////////////////////////////////
if(!validateSecurityKeys(auditSingleInput, table))
{
if(auditSingleInput.getSecurityKeyValues() == null || !auditSingleInput.getSecurityKeyValues().containsKey(recordSecurityLock.getSecurityKeyType()))
{
///////////////////////////////////////////////////////
// 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()));
}
LOG.debug("Missing securityKeyValue in audit request", logPair("table", auditSingleInput.getAuditTableName()), logPair("auditMessage", auditSingleInput.getMessage()), logPair("recordId", auditSingleInput.getRecordId()));
}
////////////////////////////////////////////////
@ -268,6 +291,7 @@ public class AuditAction extends AbstractQActionFunction<AuditInput, AuditOutput
/////////////////////////////
InsertInput insertInput = new InsertInput();
insertInput.setTableName("audit");
insertInput.setTransaction(input.getTransaction());
insertInput.setRecords(auditRecords);
InsertOutput insertOutput = new InsertAction().execute(insertInput);
@ -295,6 +319,7 @@ public class AuditAction extends AbstractQActionFunction<AuditInput, AuditOutput
{
insertInput = new InsertInput();
insertInput.setTableName("auditDetail");
insertInput.setTransaction(input.getTransaction());
insertInput.setRecords(auditDetailRecords);
new InsertAction().execute(insertInput);
}
@ -310,6 +335,70 @@ public class AuditAction extends AbstractQActionFunction<AuditInput, AuditOutput
/***************************************************************************
**
***************************************************************************/
static boolean validateSecurityKeys(AuditSingleInput auditSingleInput, QTableMetaData table)
{
boolean allAreValid = true;
for(RecordSecurityLock recordSecurityLock : RecordSecurityLockFilters.filterForReadLocks(CollectionUtils.nonNullList(table.getRecordSecurityLocks())))
{
boolean lockIsValid = validateSecurityKeysForLock(auditSingleInput, recordSecurityLock);
if(!lockIsValid)
{
allAreValid = false;
}
}
return (allAreValid);
}
/***************************************************************************
**
***************************************************************************/
private static boolean validateSecurityKeysForLock(AuditSingleInput auditSingleInput, RecordSecurityLock recordSecurityLock)
{
if(recordSecurityLock instanceof MultiRecordSecurityLock multiRecordSecurityLock)
{
boolean allSubLocksAreValid = true;
boolean anySubLocksAreValid = false;
for(RecordSecurityLock lock : RecordSecurityLockFilters.filterForReadLocks(CollectionUtils.nonNullList(multiRecordSecurityLock.getLocks())))
{
boolean subLockIsValid = validateSecurityKeysForLock(auditSingleInput, lock);
if(subLockIsValid)
{
anySubLocksAreValid = true;
}
else
{
allSubLocksAreValid = false;
}
}
if(multiRecordSecurityLock.getOperator().equals(MultiRecordSecurityLock.BooleanOperator.OR))
{
return (anySubLocksAreValid);
}
else if(multiRecordSecurityLock.getOperator().equals(MultiRecordSecurityLock.BooleanOperator.AND))
{
return (allSubLocksAreValid);
}
}
else
{
if(auditSingleInput.getSecurityKeyValues() == null || !auditSingleInput.getSecurityKeyValues().containsKey(recordSecurityLock.getSecurityKeyType()))
{
return (false);
}
}
return (true);
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -124,6 +124,7 @@ public class DMLAuditAction extends AbstractQActionFunction<DMLAuditInput, DMLAu
String contextSuffix = getContentSuffix(input);
AuditInput auditInput = new AuditInput();
auditInput.setTransaction(input.getTransaction());
if(auditLevel.equals(AuditLevel.RECORD) || (auditLevel.equals(AuditLevel.FIELD) && !dmlType.supportsFields))
{
////////////////////////////////////////////////////////////////////////////////////////////////////////////

View File

@ -0,0 +1,38 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. 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.automation;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.automation.RecordAutomationInput;
/*******************************************************************************
** interface to be implemented by one that wishes to execute custom table triggers
*******************************************************************************/
public interface CustomTableTriggerRecordAutomationHandler extends RecordAutomationHandlerInterface
{
/***************************************************************************
**
***************************************************************************/
boolean handlesThisInput(RecordAutomationInput recordAutomationInput) throws QException;
}

View File

@ -22,19 +22,11 @@
package com.kingsrook.qqq.backend.core.actions.automation;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.automation.RecordAutomationInput;
/*******************************************************************************
** Base class for custom-codes to run as an automation action
*******************************************************************************/
public abstract class RecordAutomationHandler
@Deprecated(since = "0.26.0 - when RecordAutomationHandlerInterface was introduced")
public abstract class RecordAutomationHandler implements RecordAutomationHandlerInterface
{
/*******************************************************************************
**
*******************************************************************************/
public abstract void execute(RecordAutomationInput recordAutomationInput) throws QException;
}

View File

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

View File

@ -0,0 +1,89 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.automation;
import java.util.LinkedHashMap;
import java.util.Map;
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.automation.RecordAutomationInput;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
** RecordAutomationHandler implementation that is called by automation runner
** that doesn't know to deal with a TableTrigger record that it received.
**
** e.g., if an app has altered that table (e.g., workflows-qbit).
*******************************************************************************/
public class RunCustomTableTriggerRecordAutomationHandler implements RecordAutomationHandlerInterface
{
private static final QLogger LOG = QLogger.getLogger(RunCustomTableTriggerRecordAutomationHandler.class);
private static Map<String, QCodeReference> handlers = new LinkedHashMap<>();
/***************************************************************************
**
***************************************************************************/
public static void registerHandler(String name, QCodeReference codeReference)
{
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if there's already a value mapped for this name, warn about it (unless it's for the same code reference) //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
if(handlers.containsKey(name))
{
if(handlers.get(name).getName().equals(codeReference.getName()))
{
LOG.warn("Registering a CustomTableTriggerRecordAutomationHandler for a name that is already registered", logPair("name", name));
}
}
handlers.put(name, codeReference);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void execute(RecordAutomationInput recordAutomationInput) throws QException
{
for(QCodeReference codeReference : handlers.values())
{
CustomTableTriggerRecordAutomationHandler customHandler = QCodeLoader.getAdHoc(CustomTableTriggerRecordAutomationHandler.class, codeReference);
if(customHandler.handlesThisInput(recordAutomationInput))
{
customHandler.execute(recordAutomationInput);
return;
}
}
throw (new QException("No custom record automation handler was found for " + recordAutomationInput));
}
}

View File

@ -51,7 +51,7 @@ import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
**
*******************************************************************************/
public class RunRecordScriptAutomationHandler extends RecordAutomationHandler
public class RunRecordScriptAutomationHandler implements RecordAutomationHandlerInterface
{
private static final QLogger LOG = QLogger.getLogger(RunRecordScriptAutomationHandler.class);
@ -83,7 +83,7 @@ public class RunRecordScriptAutomationHandler extends RecordAutomationHandler
}
QRecord scriptRevision = queryOutput.getRecords().get(0);
LOG.info("Running script against records", logPair("scriptRevisionId", scriptRevision.getValue("id")), logPair("scriptId", scriptRevision.getValue("scriptIdd")));
LOG.debug("Running script against records", logPair("scriptRevisionId", scriptRevision.getValue("id")), logPair("scriptId", scriptRevision.getValue("scriptIdd")));
RunAdHocRecordScriptInput input = new RunAdHocRecordScriptInput();
input.setCodeReference(new AdHocScriptCodeReference().withScriptRevisionRecord(scriptRevision));

View File

@ -33,8 +33,9 @@ import java.util.function.Supplier;
import java.util.stream.Collectors;
import com.kingsrook.qqq.backend.core.actions.async.AsyncRecordPipeLoop;
import com.kingsrook.qqq.backend.core.actions.automation.AutomationStatus;
import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationHandler;
import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationHandlerInterface;
import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationStatusUpdater;
import com.kingsrook.qqq.backend.core.actions.automation.RunCustomTableTriggerRecordAutomationHandler;
import com.kingsrook.qqq.backend.core.actions.automation.RunRecordScriptAutomationHandler;
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
import com.kingsrook.qqq.backend.core.actions.processes.QProcessCallback;
@ -442,15 +443,33 @@ public class PollingAutomationPerTableRunner implements Runnable
}
}
rs.add(new TableAutomationAction()
.withName("Script:" + tableTrigger.getScriptId())
TableAutomationAction tableAutomationAction = new TableAutomationAction()
.withFilter(filter)
.withTriggerEvent(triggerEvent)
.withPriority(tableTrigger.getPriority())
.withCodeReference(new QCodeReference(RunRecordScriptAutomationHandler.class))
.withValues(MapBuilder.of("scriptId", tableTrigger.getScriptId()))
.withIncludeRecordAssociations(true)
);
.withIncludeRecordAssociations(true);
///////////////////////////////////////////////////////////////////////////////////////////////////////
// if the table trigger has a script id on it, then we know how to run that here in qqq-backend-core //
///////////////////////////////////////////////////////////////////////////////////////////////////////
if(tableTrigger.getScriptId() != null)
{
rs.add(tableAutomationAction
.withName("Script:" + tableTrigger.getScriptId())
.withValues(MapBuilder.of("scriptId", tableTrigger.getScriptId()))
.withCodeReference(new QCodeReference(RunRecordScriptAutomationHandler.class)));
}
else
{
////////////////////////////////////////////////////////////////////////////////////////////////
// but - the app may have added an extension to the TableTrigger table (e.g., workflows qbit) //
// so, defer to RunCustomRecordAutomationHandler for unrecognized triggers //
////////////////////////////////////////////////////////////////////////////////////////////////
rs.add(tableAutomationAction
.withName("Custom Trigger:" + tableTrigger.getScriptId())
.withValues(MapBuilder.of("tableTriggerId", tableTrigger.getId()))
.withCodeReference(new QCodeReference(RunCustomTableTriggerRecordAutomationHandler.class)));
}
}
catch(Exception e)
{
@ -526,7 +545,7 @@ public class PollingAutomationPerTableRunner implements Runnable
// note - this method - will re-query the objects, so we should have confidence that their data is fresh... //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
List<QRecord> matchingQRecords = getRecordsMatchingActionFilter(table, records, action);
LOG.debug("Of the [" + records.size() + "] records that were pending automations, [" + matchingQRecords.size() + "] of them match the filter on the action:" + action);
LOG.debug("Of the [" + records.size() + "] records that were pending automations, [" + matchingQRecords.size() + "] of them match the filter on the action:" + action);
if(CollectionUtils.nullSafeHasContents(matchingQRecords))
{
LOG.debug(" Processing " + matchingQRecords.size() + " records in " + table + " for action " + action);
@ -649,7 +668,7 @@ public class PollingAutomationPerTableRunner implements Runnable
input.setRecordList(records);
input.setAction(action);
RecordAutomationHandler recordAutomationHandler = QCodeLoader.getRecordAutomationHandler(action);
RecordAutomationHandlerInterface recordAutomationHandler = QCodeLoader.getAdHoc(RecordAutomationHandlerInterface.class, action.getCodeReference());
recordAutomationHandler.execute(input);
}
}

View File

@ -0,0 +1,226 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. 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.customizers;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.QueryOrGetInputInterface;
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
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.code.InitializableViaCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReferenceWithProperties;
import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder;
/*******************************************************************************
** Implementation of TableCustomizerInterface that runs multiple other customizers
*******************************************************************************/
public class MultiCustomizer implements InitializableViaCodeReference, TableCustomizerInterface
{
private static final String KEY_CODE_REFERENCES = "codeReferences";
private List<TableCustomizerInterface> customizers = new ArrayList<>();
/***************************************************************************
* Factory method that builds a {@link QCodeReferenceWithProperties} that will
* allow this multi-customizer to be assigned to a table, and to track
* in that code ref's properties, the "sub" QCodeReferences to be used.
*
* Added to a table as in:
* <pre>
* table.withCustomizer(TableCustomizers.POST_INSERT_RECORD,
* MultiCustomizer.of(QCodeReference(x), QCodeReference(y)));
* </pre>
*
* @param codeReferences
* one or more {@link QCodeReference objects} to run when this customizer
* runs. note that they will run in the order provided in this list.
***************************************************************************/
public static QCodeReferenceWithProperties of(QCodeReference... codeReferences)
{
ArrayList<QCodeReference> list = new ArrayList<>(Arrays.stream(codeReferences).toList());
return (new QCodeReferenceWithProperties(MultiCustomizer.class, MapBuilder.of(KEY_CODE_REFERENCES, list)));
}
/***************************************************************************
* Add an additional table customizer code reference to an existing
* codeReference, e.g., constructed by the `of` factory method.
*
* @see #of(QCodeReference...)
***************************************************************************/
public static void addTableCustomizer(QCodeReferenceWithProperties existingMultiCustomizerCodeReference, QCodeReference codeReference)
{
ArrayList<QCodeReference> list = (ArrayList<QCodeReference>) existingMultiCustomizerCodeReference.getProperties().computeIfAbsent(KEY_CODE_REFERENCES, key -> new ArrayList<>());
list.add(codeReference);
}
/***************************************************************************
* When this class is instantiated by the QCodeLoader, initialize the
* sub-customizer objects.
***************************************************************************/
@Override
public void initialize(QCodeReference codeReference)
{
if(codeReference instanceof QCodeReferenceWithProperties codeReferenceWithProperties)
{
Serializable codeReferencesPropertyValue = codeReferenceWithProperties.getProperties().get(KEY_CODE_REFERENCES);
if(codeReferencesPropertyValue instanceof List<?> list)
{
for(Object o : list)
{
if(o instanceof QCodeReference reference)
{
TableCustomizerInterface customizer = QCodeLoader.getAdHoc(TableCustomizerInterface.class, reference);
customizers.add(customizer);
}
}
}
else
{
LOG.warn("Property KEY_CODE_REFERENCES [" + KEY_CODE_REFERENCES + "] must be a List<QCodeReference>.");
}
}
if(customizers.isEmpty())
{
LOG.info("No TableCustomizers were specified for MultiCustomizer.");
}
}
/***************************************************************************
* run postQuery method over all sub-customizers
***************************************************************************/
@Override
public List<QRecord> postQuery(QueryOrGetInputInterface queryInput, List<QRecord> records) throws QException
{
for(TableCustomizerInterface customizer : customizers)
{
records = customizer.postQuery(queryInput, records);
}
return records;
}
/***************************************************************************
* run preInsert method over all sub-customizers
***************************************************************************/
@Override
public List<QRecord> preInsert(InsertInput insertInput, List<QRecord> records, boolean isPreview) throws QException
{
for(TableCustomizerInterface customizer : customizers)
{
records = customizer.preInsert(insertInput, records, isPreview);
}
return records;
}
/***************************************************************************
* run postInsert method over all sub-customizers
***************************************************************************/
@Override
public List<QRecord> postInsert(InsertInput insertInput, List<QRecord> records) throws QException
{
for(TableCustomizerInterface customizer : customizers)
{
records = customizer.postInsert(insertInput, records);
}
return records;
}
/***************************************************************************
* run preUpdate method over all sub-customizers
***************************************************************************/
@Override
public List<QRecord> preUpdate(UpdateInput updateInput, List<QRecord> records, boolean isPreview, Optional<List<QRecord>> oldRecordList) throws QException
{
for(TableCustomizerInterface customizer : customizers)
{
records = customizer.preUpdate(updateInput, records, isPreview, oldRecordList);
}
return records;
}
/***************************************************************************
* run postUpdate method over all sub-customizers
***************************************************************************/
@Override
public List<QRecord> postUpdate(UpdateInput updateInput, List<QRecord> records, Optional<List<QRecord>> oldRecordList) throws QException
{
for(TableCustomizerInterface customizer : customizers)
{
records = customizer.postUpdate(updateInput, records, oldRecordList);
}
return records;
}
/***************************************************************************
* run preDelete method over all sub-customizers
***************************************************************************/
@Override
public List<QRecord> preDelete(DeleteInput deleteInput, List<QRecord> records, boolean isPreview) throws QException
{
for(TableCustomizerInterface customizer : customizers)
{
records = customizer.preDelete(deleteInput, records, isPreview);
}
return records;
}
/***************************************************************************
* run postDelete method over all sub-customizers
***************************************************************************/
@Override
public List<QRecord> postDelete(DeleteInput deleteInput, List<QRecord> records) throws QException
{
for(TableCustomizerInterface customizer : customizers)
{
records = customizer.postDelete(deleteInput, records);
}
return records;
}
}

View File

@ -0,0 +1,88 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. 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.customizers;
import java.io.Serializable;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import com.kingsrook.qqq.backend.core.utils.collections.TypeTolerantKeyMap;
/*******************************************************************************
** utility class to help table customizers working with the oldRecordList.
** Usage is just 2 lines:
** outside of loop-over-records:
** - OldRecordHelper oldRecordHelper = new OldRecordHelper(updateInput.getTableName(), oldRecordList);
** then inside the record loop:
** - Optional<QRecord> oldRecord = oldRecordHelper.getOldRecord(record);
*******************************************************************************/
public class OldRecordHelper
{
private String primaryKeyField;
private QFieldType primaryKeyType;
private Optional<List<QRecord>> oldRecordList;
private Map<Serializable, QRecord> oldRecordMap;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public OldRecordHelper(String tableName, Optional<List<QRecord>> oldRecordList)
{
this.primaryKeyField = QContext.getQInstance().getTable(tableName).getPrimaryKeyField();
this.primaryKeyType = QContext.getQInstance().getTable(tableName).getField(primaryKeyField).getType();
this.oldRecordList = oldRecordList;
}
/***************************************************************************
**
***************************************************************************/
public Optional<QRecord> getOldRecord(QRecord record)
{
if(oldRecordMap == null)
{
if(oldRecordList.isPresent())
{
oldRecordMap = new TypeTolerantKeyMap<>(primaryKeyType);
oldRecordList.get().forEach(r -> oldRecordMap.put(r.getValue(primaryKeyField), r));
}
else
{
oldRecordMap = Collections.emptyMap();
}
}
return (Optional.ofNullable(oldRecordMap.get(record.getValue(primaryKeyField))));
}
}

View File

@ -24,17 +24,11 @@ package com.kingsrook.qqq.backend.core.actions.customizers;
import java.lang.reflect.Constructor;
import java.util.Optional;
import java.util.function.Function;
import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationHandler;
import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
import com.kingsrook.qqq.backend.core.actions.values.QCustomPossibleValueProvider;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.metadata.code.InitializableViaCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.TableAutomationAction;
import com.kingsrook.qqq.backend.core.utils.lambdas.UnsafeFunction;
import com.kingsrook.qqq.backend.core.utils.memoization.Memoization;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
@ -43,9 +37,7 @@ import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
** Utility to load code for running QQQ customizers.
**
** TODO - redo all to go through method that memoizes class & constructor
** lookup. That memoziation causes 1,000,000 such calls to go from ~500ms
** to ~100ms.
** That memoization causes 1,000,000 such calls to go from ~500ms to ~100ms.
*******************************************************************************/
public class QCodeLoader
{
@ -70,84 +62,6 @@ public class QCodeLoader
/*******************************************************************************
**
*******************************************************************************/
@SuppressWarnings("unchecked")
public static <T, R> Function<T, R> getFunction(QCodeReference codeReference)
{
if(codeReference == null)
{
return (null);
}
if(!codeReference.getCodeType().equals(QCodeType.JAVA))
{
///////////////////////////////////////////////////////////////////////////////////////
// todo - 1) support more languages, 2) wrap them w/ java Functions here, 3) profit! //
///////////////////////////////////////////////////////////////////////////////////////
throw (new IllegalArgumentException("Only JAVA customizers are supported at this time."));
}
try
{
Class<?> customizerClass = Class.forName(codeReference.getName());
return ((Function<T, R>) customizerClass.getConstructor().newInstance());
}
catch(Exception e)
{
LOG.error("Error initializing customizer", e, logPair("codeReference", codeReference));
//////////////////////////////////////////////////////////////////////////////////////////////////////////
// return null here - under the assumption that during normal run-time operations, we'll never hit here //
// as we'll want to validate all functions in the instance validator at startup time (and IT will throw //
// if it finds an invalid code reference //
//////////////////////////////////////////////////////////////////////////////////////////////////////////
return (null);
}
}
/*******************************************************************************
**
*******************************************************************************/
@SuppressWarnings("unchecked")
public static <T extends BackendStep> T getBackendStep(Class<T> expectedType, QCodeReference codeReference)
{
if(codeReference == null)
{
return (null);
}
if(!codeReference.getCodeType().equals(QCodeType.JAVA))
{
///////////////////////////////////////////////////////////////////////////////////////
// todo - 1) support more languages, 2) wrap them w/ java Functions here, 3) profit! //
///////////////////////////////////////////////////////////////////////////////////////
throw (new IllegalArgumentException("Only JAVA BackendSteps are supported at this time."));
}
try
{
Class<?> customizerClass = Class.forName(codeReference.getName());
return ((T) customizerClass.getConstructor().newInstance());
}
catch(Exception e)
{
LOG.error("Error initializing customizer", e, logPair("codeReference", codeReference));
//////////////////////////////////////////////////////////////////////////////////////////////////////////
// return null here - under the assumption that during normal run-time operations, we'll never hit here //
// as we'll want to validate all functions in the instance validator at startup time (and IT will throw //
// if it finds an invalid code reference //
//////////////////////////////////////////////////////////////////////////////////////////////////////////
return (null);
}
}
/*******************************************************************************
**
*******************************************************************************/
@ -177,7 +91,17 @@ public class QCodeLoader
if(constructor.isPresent())
{
return ((T) constructor.get().newInstance());
T t = (T) constructor.get().newInstance();
////////////////////////////////////////////////////////////////
// if the object is initializable, then, well, initialize it! //
////////////////////////////////////////////////////////////////
if(t instanceof InitializableViaCodeReference initializableViaCodeReference)
{
initializableViaCodeReference.initialize(codeReference);
}
return t;
}
else
{
@ -187,7 +111,7 @@ public class QCodeLoader
}
catch(Exception e)
{
LOG.error("Error initializing customizer", e, logPair("codeReference", codeReference));
LOG.error("Error initializing codeReference", e, logPair("codeReference", codeReference));
//////////////////////////////////////////////////////////////////////////////////////////////////////////
// return null here - under the assumption that during normal run-time operations, we'll never hit here //
@ -198,67 +122,4 @@ public class QCodeLoader
}
}
/*******************************************************************************
**
*******************************************************************************/
public static RecordAutomationHandler getRecordAutomationHandler(TableAutomationAction action) throws QException
{
try
{
QCodeReference codeReference = action.getCodeReference();
if(!codeReference.getCodeType().equals(QCodeType.JAVA))
{
///////////////////////////////////////////////////////////////////////////////////////
// todo - 1) support more languages, 2) wrap them w/ java Functions here, 3) profit! //
///////////////////////////////////////////////////////////////////////////////////////
throw (new IllegalArgumentException("Only JAVA customizers are supported at this time."));
}
Class<?> codeClass = Class.forName(codeReference.getName());
Object codeObject = codeClass.getConstructor().newInstance();
if(!(codeObject instanceof RecordAutomationHandler recordAutomationHandler))
{
throw (new QException("The supplied code [" + codeClass.getName() + "] is not an instance of RecordAutomationHandler"));
}
return (recordAutomationHandler);
}
catch(QException qe)
{
throw (qe);
}
catch(Exception e)
{
throw (new QException("Error getting record automation handler for action [" + action.getName() + "]", e));
}
}
/*******************************************************************************
**
*******************************************************************************/
public static QCustomPossibleValueProvider getCustomPossibleValueProvider(QPossibleValueSource possibleValueSource) throws QException
{
try
{
Class<?> codeClass = Class.forName(possibleValueSource.getCustomCodeReference().getName());
Object codeObject = codeClass.getConstructor().newInstance();
if(!(codeObject instanceof QCustomPossibleValueProvider customPossibleValueProvider))
{
throw (new QException("The supplied code [" + codeClass.getName() + "] is not an instance of QCustomPossibleValueProvider"));
}
return (customPossibleValueProvider);
}
catch(QException qe)
{
throw (qe);
}
catch(Exception e)
{
throw (new QException("Error getting custom possible value provider for PVS [" + possibleValueSource.getName() + "]", e));
}
}
}

View File

@ -27,6 +27,7 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
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;
@ -160,4 +161,18 @@ public interface RecordCustomizerUtilityInterface
return (oldRecordMap);
}
/***************************************************************************
**
***************************************************************************/
static <T extends Serializable> T getValueFromRecordOrOldRecord(String fieldName, QRecord record, Serializable primaryKey, Optional<Map<Serializable, QRecord>> oldRecordMap)
{
T value = (T) record.getValue(fieldName);
if(value == null && primaryKey != null && oldRecordMap.isPresent() && oldRecordMap.get().containsKey(primaryKey))
{
value = (T) oldRecordMap.get().get(primaryKey).getValue(fieldName);
}
return value;
}
}

View File

@ -22,15 +22,19 @@
package com.kingsrook.qqq.backend.core.actions.customizers;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.QueryOrGetInputInterface;
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
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.utils.CollectionUtils;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
@ -47,7 +51,6 @@ public interface TableCustomizerInterface
{
QLogger LOG = QLogger.getLogger(TableCustomizerInterface.class);
/*******************************************************************************
** custom actions to run after a query (or get!) takes place.
**
@ -77,8 +80,15 @@ public interface TableCustomizerInterface
*******************************************************************************/
default List<QRecord> preInsert(InsertInput insertInput, List<QRecord> records, boolean isPreview) throws QException
{
LOG.info("A default implementation of preInsert is running... Probably not expected!", logPair("tableName", insertInput.getTableName()));
return (records);
try
{
return (preInsertOrUpdate(insertInput, records, isPreview, Optional.empty()));
}
catch(NotImplementedHereException e)
{
LOG.info("A default implementation of preInsert is running... Probably not expected!", logPair("tableName", insertInput.getTableName()));
return (records);
}
}
@ -104,8 +114,15 @@ public interface TableCustomizerInterface
*******************************************************************************/
default List<QRecord> postInsert(InsertInput insertInput, List<QRecord> records) throws QException
{
LOG.info("A default implementation of postInsert is running... Probably not expected!", logPair("tableName", insertInput.getTableName()));
return (records);
try
{
return (postInsertOrUpdate(insertInput, records, Optional.empty()));
}
catch(NotImplementedHereException e)
{
LOG.info("A default implementation of postInsert is running... Probably not expected!", logPair("tableName", insertInput.getTableName()));
return (records);
}
}
@ -130,8 +147,15 @@ public interface TableCustomizerInterface
*******************************************************************************/
default List<QRecord> preUpdate(UpdateInput updateInput, List<QRecord> records, boolean isPreview, Optional<List<QRecord>> oldRecordList) throws QException
{
LOG.info("A default implementation of preUpdate is running... Probably not expected!", logPair("tableName", updateInput.getTableName()));
return (records);
try
{
return (preInsertOrUpdate(updateInput, records, isPreview, oldRecordList));
}
catch(NotImplementedHereException e)
{
LOG.info("A default implementation of preUpdate is running... Probably not expected!", logPair("tableName", updateInput.getTableName()));
return (records);
}
}
@ -151,8 +175,15 @@ public interface TableCustomizerInterface
*******************************************************************************/
default List<QRecord> postUpdate(UpdateInput updateInput, List<QRecord> records, Optional<List<QRecord>> oldRecordList) throws QException
{
LOG.info("A default implementation of postUpdate is running... Probably not expected!", logPair("tableName", updateInput.getTableName()));
return (records);
try
{
return (postInsertOrUpdate(updateInput, records, oldRecordList));
}
catch(NotImplementedHereException e)
{
LOG.info("A default implementation of postUpdate is running... Probably not expected!", logPair("tableName", updateInput.getTableName()));
return (records);
}
}
@ -199,4 +230,59 @@ public interface TableCustomizerInterface
return (records);
}
/***************************************************************************
** Optional method to override in a customizer that does the same thing for
** both preInsert & preUpdate.
***************************************************************************/
default List<QRecord> preInsertOrUpdate(AbstractActionInput input, List<QRecord> records, boolean isPreview, Optional<List<QRecord>> oldRecordList) throws QException
{
throw NotImplementedHereException.instance;
}
/***************************************************************************
** Optional method to override in a customizer that does the same thing for
** both postInsert & postUpdate.
***************************************************************************/
default List<QRecord> postInsertOrUpdate(AbstractActionInput input, List<QRecord> records, Optional<List<QRecord>> oldRecordList) throws QException
{
throw NotImplementedHereException.instance;
}
/***************************************************************************
**
***************************************************************************/
default Optional<Map<Serializable, QRecord>> oldRecordListToMap(String primaryKeyField, Optional<List<QRecord>> oldRecordList)
{
if(oldRecordList.isPresent())
{
return (Optional.of(CollectionUtils.listToMap(oldRecordList.get(), r -> r.getValue(primaryKeyField))));
}
else
{
return (Optional.empty());
}
}
/***************************************************************************
**
***************************************************************************/
class NotImplementedHereException extends QException
{
private static NotImplementedHereException instance = new NotImplementedHereException();
/***************************************************************************
**
***************************************************************************/
private NotImplementedHereException()
{
super("Not implemented here");
}
}
}

View File

@ -161,7 +161,7 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer
public static String linkTableCreateWithDefaultValues(RenderWidgetInput input, String tableName, Map<String, Serializable> defaultValues) throws QException
{
String tablePath = QContext.getQInstance().getTablePath(tableName);
return (tablePath + "/create#defaultValues=" + URLEncoder.encode(JsonUtils.toJson(defaultValues), Charset.defaultCharset()));
return (tablePath + "/create#defaultValues=" + URLEncoder.encode(JsonUtils.toJson(defaultValues), StandardCharsets.UTF_8));
}
@ -229,7 +229,7 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer
}
filter = QQueryFilterDeduper.dedupeFilter(filter);
return (tablePath + "?filter=" + URLEncoder.encode(JsonUtils.toJson(filter), Charset.defaultCharset()));
return (tablePath + "?filter=" + URLEncoder.encode(JsonUtils.toJson(filter), StandardCharsets.UTF_8));
}
@ -290,7 +290,18 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer
/*******************************************************************************
**
*******************************************************************************/
@Deprecated(since = "call one that doesn't take input param")
public static String linkRecordEdit(AbstractActionInput input, String tableName, Serializable recordId) throws QException
{
return linkRecordEdit(tableName, recordId);
}
/*******************************************************************************
**
*******************************************************************************/
public static String linkRecordEdit(String tableName, Serializable recordId) throws QException
{
String tablePath = QContext.getQInstance().getTablePath(tableName);
return (tablePath + "/" + recordId + "/edit");
@ -317,7 +328,17 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer
/*******************************************************************************
**
*******************************************************************************/
@Deprecated(since = "call one that doesn't take input param")
public static String linkProcessForFilter(AbstractActionInput input, String processName, QQueryFilter filter) throws QException
{
return linkProcessForFilter(processName, filter);
}
/*******************************************************************************
**
*******************************************************************************/
public static String linkProcessForFilter(String processName, QQueryFilter filter) throws QException
{
QProcessMetaData process = QContext.getQInstance().getProcess(processName);
if(process == null)
@ -337,10 +358,21 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer
/*******************************************************************************
**
*******************************************************************************/
@Deprecated(since = "call one that doesn't take input param")
public static String linkProcessForRecord(AbstractActionInput input, String processName, Serializable recordId) throws QException
{
return linkProcessForRecord(processName, recordId);
}
/*******************************************************************************
**
*******************************************************************************/
public static String linkProcessForRecord(String processName, Serializable recordId) throws QException
{
QProcessMetaData process = QContext.getQInstance().getProcess(processName);
String tableName = process.getTableName();

View File

@ -25,11 +25,14 @@ package com.kingsrook.qqq.backend.core.actions.dashboard.widgets;
import java.io.Serializable;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import com.google.gson.reflect.TypeToken;
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;
@ -37,12 +40,15 @@ 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.instances.QInstanceValidator;
import com.kingsrook.qqq.backend.core.instances.validation.plugins.QInstanceValidatorPluginInterface;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.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.QueryOutput;
@ -51,15 +57,19 @@ import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetOutput;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.ChildRecordListData;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.WidgetType;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.AbstractWidgetMetaDataBuilder;
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaDataInterface;
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn;
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import com.kingsrook.qqq.backend.core.utils.collections.MutableList;
import org.apache.commons.lang.BooleanUtils;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
@ -83,7 +93,9 @@ public class ChildRecordListRenderer extends AbstractWidgetRenderer
.withIsCard(true)
.withCodeReference(new QCodeReference(ChildRecordListRenderer.class))
.withType(WidgetType.CHILD_RECORD_LIST.getType())
.withDefaultValue("joinName", join.getName())));
.withDefaultValue("joinName", join.getName())
.withValidatorPlugin(new ChildRecordListWidgetValidator())
));
}
@ -168,6 +180,19 @@ public class ChildRecordListRenderer extends AbstractWidgetRenderer
widgetMetaData.withDefaultValue("manageAssociationName", manageAssociationName);
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
public Builder withOmitFieldNames(List<String> omitFieldNames)
{
ArrayList<String> arrayList = CollectionUtils.useOrWrap(omitFieldNames, new TypeToken<>() {});
widgetMetaData.withDefaultValue("omitFieldNames", arrayList);
return (this);
}
}
@ -187,14 +212,25 @@ public class ChildRecordListRenderer extends AbstractWidgetRenderer
QTableMetaData leftTable = QContext.getQInstance().getTable(join.getLeftTable());
QTableMetaData rightTable = QContext.getQInstance().getTable(join.getRightTable());
Map<String, Serializable> widgetMetaDataDefaultValues = input.getWidgetMetaData().getDefaultValues();
List<String> omitFieldNames = (List<String>) widgetMetaDataDefaultValues.get("omitFieldNames");
if(omitFieldNames == null)
{
omitFieldNames = new ArrayList<>();
}
else
{
omitFieldNames = new MutableList<>(omitFieldNames);
}
Integer maxRows = null;
if(StringUtils.hasContent(input.getQueryParams().get("maxRows")))
{
maxRows = ValueUtils.getValueAsInteger(input.getQueryParams().get("maxRows"));
}
else if(input.getWidgetMetaData().getDefaultValues().containsKey("maxRows"))
else if(widgetMetaDataDefaultValues.containsKey("maxRows"))
{
maxRows = ValueUtils.getValueAsInteger(input.getWidgetMetaData().getDefaultValues().containsKey("maxRows"));
maxRows = ValueUtils.getValueAsInteger(widgetMetaDataDefaultValues.get("maxRows"));
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@ -226,8 +262,19 @@ public class ChildRecordListRenderer extends AbstractWidgetRenderer
for(JoinOn joinOn : join.getJoinOns())
{
filter.addCriteria(new QFilterCriteria(joinOn.getRightField(), QCriteriaOperator.EQUALS, List.of(primaryRecord.getValue(joinOn.getLeftField()))));
omitFieldNames.add(joinOn.getRightField());
}
filter.setOrderBys(join.getOrderBys());
Serializable orderBy = widgetMetaDataDefaultValues.get("orderBy");
if(orderBy instanceof List orderByList && !orderByList.isEmpty() && orderByList.get(0) instanceof QFilterOrderBy)
{
filter.setOrderBys(orderByList);
}
else
{
filter.setOrderBys(join.getOrderBys());
}
filter.setLimit(maxRows);
QueryInput queryInput = new QueryInput();
@ -254,9 +301,10 @@ public class ChildRecordListRenderer extends AbstractWidgetRenderer
}
String tablePath = QContext.getQInstance().getTablePath(rightTable.getName());
String viewAllLink = tablePath == null ? null : (tablePath + "?filter=" + URLEncoder.encode(JsonUtils.toJson(filter), Charset.defaultCharset()));
String viewAllLink = tablePath == null ? null : (tablePath + "?filter=" + URLEncoder.encode(JsonUtils.toJson(filter), StandardCharsets.UTF_8));
ChildRecordListData widgetData = new ChildRecordListData(widgetLabel, queryOutput, rightTable, tablePath, viewAllLink, totalRows);
widgetData.setOmitFieldNames(omitFieldNames);
if(BooleanUtils.isTrue(ValueUtils.getValueAsBoolean(input.getQueryParams().get("canAddChildRecord"))))
{
@ -276,11 +324,10 @@ public class ChildRecordListRenderer extends AbstractWidgetRenderer
widgetData.setDefaultValuesForNewChildRecords(defaultValuesForNewChildRecords);
Map<String, Serializable> widgetValues = input.getWidgetMetaData().getDefaultValues();
if(widgetValues.containsKey("disabledFieldsForNewChildRecords"))
if(widgetMetaDataDefaultValues.containsKey("disabledFieldsForNewChildRecords"))
{
@SuppressWarnings("unchecked")
Set<String> disabledFieldsForNewChildRecords = (Set<String>) widgetValues.get("disabledFieldsForNewChildRecords");
Set<String> disabledFieldsForNewChildRecords = (Set<String>) widgetMetaDataDefaultValues.get("disabledFieldsForNewChildRecords");
widgetData.setDisabledFieldsForNewChildRecords(disabledFieldsForNewChildRecords);
}
else
@ -299,6 +346,13 @@ public class ChildRecordListRenderer extends AbstractWidgetRenderer
}
}
}
if(widgetMetaDataDefaultValues.containsKey("defaultValuesForNewChildRecordsFromParentFields"))
{
@SuppressWarnings("unchecked")
Map<String, String> defaultValuesForNewChildRecordsFromParentFields = (Map<String, String>) widgetMetaDataDefaultValues.get("defaultValuesForNewChildRecordsFromParentFields");
widgetData.setDefaultValuesForNewChildRecordsFromParentFields(defaultValuesForNewChildRecordsFromParentFields);
}
}
widgetData.setAllowRecordEdit(BooleanUtils.isTrue(ValueUtils.getValueAsBoolean(input.getQueryParams().get("allowRecordEdit"))));
@ -313,4 +367,68 @@ public class ChildRecordListRenderer extends AbstractWidgetRenderer
}
}
/***************************************************************************
**
***************************************************************************/
private static class ChildRecordListWidgetValidator implements QInstanceValidatorPluginInterface<QWidgetMetaDataInterface>
{
/***************************************************************************
**
***************************************************************************/
@Override
public void validate(QWidgetMetaDataInterface widgetMetaData, QInstance qInstance, QInstanceValidator qInstanceValidator)
{
String prefix = "Widget " + widgetMetaData.getName() + ": ";
//////////////////////////////////
// make sure join name is given //
//////////////////////////////////
String joinName = ValueUtils.getValueAsString(CollectionUtils.nonNullMap(widgetMetaData.getDefaultValues()).get("joinName"));
if(qInstanceValidator.assertCondition(StringUtils.hasContent(joinName), prefix + "defaultValue for joinName must be given"))
{
///////////////////////////
// make sure join exists //
///////////////////////////
QJoinMetaData join = qInstance.getJoin(joinName);
if(qInstanceValidator.assertCondition(join != null, prefix + "No join named " + joinName + " exists in the instance"))
{
//////////////////////////////////////////////////////////////////////////////////
// if there's a manageAssociationName, make sure the table has that association //
//////////////////////////////////////////////////////////////////////////////////
String manageAssociationName = ValueUtils.getValueAsString(widgetMetaData.getDefaultValues().get("manageAssociationName"));
if(StringUtils.hasContent(manageAssociationName))
{
validateAssociationName(prefix, manageAssociationName, join, qInstance, qInstanceValidator);
}
}
}
}
/***************************************************************************
**
***************************************************************************/
private void validateAssociationName(String prefix, String manageAssociationName, QJoinMetaData join, QInstance qInstance, QInstanceValidator qInstanceValidator)
{
///////////////////////////////////
// make sure join's table exists //
///////////////////////////////////
QTableMetaData table = qInstance.getTable(join.getLeftTable());
if(table == null)
{
qInstanceValidator.getErrors().add(prefix + "Unable to validate manageAssociationName, as table [" + join.getLeftTable() + "] on left-side table of join [" + join.getName() + "] does not exist.");
}
else
{
if(CollectionUtils.nonNullList(table.getAssociations()).stream().noneMatch(a -> manageAssociationName.equals(a.getName())))
{
qInstanceValidator.getErrors().add(prefix + "an association named [" + manageAssociationName + "] does not exist on table [" + join.getLeftTable() + "]");
}
}
}
}
}

View File

@ -0,0 +1,252 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.dashboard.widgets;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import com.kingsrook.qqq.backend.core.actions.tables.CountAction;
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.instances.QInstanceValidator;
import com.kingsrook.qqq.backend.core.instances.validation.plugins.QInstanceValidatorPluginInterface;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.FilterUseCase;
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.actions.widgets.RenderWidgetInput;
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetOutput;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.ChildRecordListData;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.WidgetType;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.AbstractWidgetMetaDataBuilder;
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaDataInterface;
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.JsonUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
** Generic widget to display a list of records.
**
** Note, closely related to (and copied from ChildRecordListRenderer.
** opportunity to share more code with that in the future??
*******************************************************************************/
public class RecordListWidgetRenderer extends AbstractWidgetRenderer
{
private static final QLogger LOG = QLogger.getLogger(RecordListWidgetRenderer.class);
/*******************************************************************************
**
*******************************************************************************/
public static Builder widgetMetaDataBuilder(String widgetName)
{
return (new Builder(new QWidgetMetaData()
.withName(widgetName)
.withIsCard(true)
.withCodeReference(new QCodeReference(RecordListWidgetRenderer.class))
.withType(WidgetType.CHILD_RECORD_LIST.getType())
.withValidatorPlugin(new RecordListWidgetValidator())
));
}
/*******************************************************************************
**
*******************************************************************************/
public static class Builder extends AbstractWidgetMetaDataBuilder
{
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public Builder(QWidgetMetaData widgetMetaData)
{
super(widgetMetaData);
}
/*******************************************************************************
**
*******************************************************************************/
public Builder withLabel(String label)
{
widgetMetaData.setLabel(label);
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
public Builder withMaxRows(Integer maxRows)
{
widgetMetaData.withDefaultValue("maxRows", maxRows);
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
public Builder withTableName(String tableName)
{
widgetMetaData.withDefaultValue("tableName", tableName);
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
public Builder withFilter(QQueryFilter filter)
{
widgetMetaData.withDefaultValue("filter", filter);
return (this);
}
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public RenderWidgetOutput render(RenderWidgetInput input) throws QException
{
try
{
Integer maxRows = null;
if(StringUtils.hasContent(input.getQueryParams().get("maxRows")))
{
maxRows = ValueUtils.getValueAsInteger(input.getQueryParams().get("maxRows"));
}
else if(input.getWidgetMetaData().getDefaultValues().containsKey("maxRows"))
{
maxRows = ValueUtils.getValueAsInteger(input.getWidgetMetaData().getDefaultValues().get("maxRows"));
}
QQueryFilter filter = ((QQueryFilter) input.getWidgetMetaData().getDefaultValues().get("filter")).clone();
filter.interpretValues(new HashMap<>(input.getQueryParams()), FilterUseCase.DEFAULT);
filter.setLimit(maxRows);
String tableName = ValueUtils.getValueAsString(input.getWidgetMetaData().getDefaultValues().get("tableName"));
QTableMetaData table = QContext.getQInstance().getTable(tableName);
QueryInput queryInput = new QueryInput();
queryInput.setTableName(tableName);
queryInput.setShouldTranslatePossibleValues(true);
queryInput.setShouldGenerateDisplayValues(true);
queryInput.setFilter(filter);
QueryOutput queryOutput = new QueryAction().execute(queryInput);
QValueFormatter.setBlobValuesToDownloadUrls(table, queryOutput.getRecords());
int totalRows = queryOutput.getRecords().size();
if(maxRows != null && (queryOutput.getRecords().size() == maxRows))
{
/////////////////////////////////////////////////////////////////////////////////////
// if the input said to only do some max, and the # of results we got is that max, //
// then do a count query, for displaying 1-n of <count> //
/////////////////////////////////////////////////////////////////////////////////////
CountInput countInput = new CountInput();
countInput.setTableName(tableName);
countInput.setFilter(filter);
totalRows = new CountAction().execute(countInput).getCount();
}
String tablePath = QContext.getQInstance().getTablePath(tableName);
String viewAllLink = tablePath == null ? null : (tablePath + "?filter=" + URLEncoder.encode(JsonUtils.toJson(filter), StandardCharsets.UTF_8));
ChildRecordListData widgetData = new ChildRecordListData(input.getQueryParams().get("widgetLabel"), queryOutput, table, tablePath, viewAllLink, totalRows);
return (new RenderWidgetOutput(widgetData));
}
catch(Exception e)
{
LOG.warn("Error rendering record list widget", e, logPair("widgetName", () -> input.getWidgetMetaData().getName()));
throw (e);
}
}
/***************************************************************************
**
***************************************************************************/
private static class RecordListWidgetValidator implements QInstanceValidatorPluginInterface<QWidgetMetaDataInterface>
{
/***************************************************************************
**
***************************************************************************/
@Override
public void validate(QWidgetMetaDataInterface widgetMetaData, QInstance qInstance, QInstanceValidator qInstanceValidator)
{
String prefix = "Widget " + widgetMetaData.getName() + ": ";
//////////////////////////////////////////////
// make sure table name is given and exists //
//////////////////////////////////////////////
QTableMetaData table = null;
String tableName = ValueUtils.getValueAsString(CollectionUtils.nonNullMap(widgetMetaData.getDefaultValues()).get("tableName"));
if(qInstanceValidator.assertCondition(StringUtils.hasContent(tableName), prefix + "defaultValue for tableName must be given"))
{
////////////////////////////
// make sure table exists //
////////////////////////////
table = qInstance.getTable(tableName);
qInstanceValidator.assertCondition(table != null, prefix + "No table named " + tableName + " exists in the instance");
}
////////////////////////////////////////////////////////////////////////////////////
// make sure filter is given and is valid (only check that if table is given too) //
////////////////////////////////////////////////////////////////////////////////////
QQueryFilter filter = ((QQueryFilter) widgetMetaData.getDefaultValues().get("filter"));
if(qInstanceValidator.assertCondition(filter != null, prefix + "defaultValue for filter must be given") && table != null)
{
qInstanceValidator.validateQueryFilter(qInstance, prefix, table, filter, null);
}
}
}
}

View File

@ -33,6 +33,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
/*******************************************************************************
** a default implementation of MetaDataFilterInterface, that allows all the things
*******************************************************************************/
@Deprecated(since = "migrated to metaDataCustomizer")
public class AllowAllMetaDataFilter implements MetaDataFilterInterface
{

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 is all noop.
*******************************************************************************/
public class DefaultNoopMetaDataActionCustomizer implements MetaDataActionCustomizerInterface
{
/***************************************************************************
**
***************************************************************************/
@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

@ -23,10 +23,12 @@ package com.kingsrook.qqq.backend.core.actions.metadata;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
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;
@ -34,6 +36,7 @@ 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.exceptions.QUserFacingException;
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;
@ -65,7 +68,7 @@ public class MetaDataAction
{
private static final QLogger LOG = QLogger.getLogger(MetaDataAction.class);
private static Memoization<QInstance, MetaDataFilterInterface> metaDataFilterMemoization = new Memoization<>();
private static Memoization<QInstance, MetaDataActionCustomizerInterface> metaDataActionCustomizerMemoization = new Memoization<>();
@ -79,7 +82,7 @@ public class MetaDataAction
MetaDataOutput metaDataOutput = new MetaDataOutput();
Map<String, AppTreeNode> treeNodes = new LinkedHashMap<>();
MetaDataFilterInterface filter = getMetaDataFilter();
MetaDataActionCustomizerInterface customizer = getMetaDataActionCustomizer();
/////////////////////////////////////
// map tables to frontend metadata //
@ -90,7 +93,7 @@ public class MetaDataAction
String tableName = entry.getKey();
QTableMetaData table = entry.getValue();
if(!filter.allowTable(metaDataInput, table))
if(!customizer.allowTable(metaDataInput, table))
{
continue;
}
@ -119,7 +122,7 @@ public class MetaDataAction
String processName = entry.getKey();
QProcessMetaData process = entry.getValue();
if(!filter.allowProcess(metaDataInput, process))
if(!customizer.allowProcess(metaDataInput, process))
{
continue;
}
@ -144,7 +147,7 @@ public class MetaDataAction
String reportName = entry.getKey();
QReportMetaData report = entry.getValue();
if(!filter.allowReport(metaDataInput, report))
if(!customizer.allowReport(metaDataInput, report))
{
continue;
}
@ -169,7 +172,7 @@ public class MetaDataAction
String widgetName = entry.getKey();
QWidgetMetaDataInterface widget = entry.getValue();
if(!filter.allowWidget(metaDataInput, widget))
if(!customizer.allowWidget(metaDataInput, widget))
{
continue;
}
@ -206,7 +209,7 @@ public class MetaDataAction
continue;
}
if(!filter.allowApp(metaDataInput, app))
if(!customizer.allowApp(metaDataInput, app))
{
continue;
}
@ -292,11 +295,24 @@ public class MetaDataAction
metaDataOutput.setBranding(QContext.getQInstance().getBranding());
}
metaDataOutput.setEnvironmentValues(QContext.getQInstance().getEnvironmentValues());
metaDataOutput.setEnvironmentValues(Objects.requireNonNullElse(QContext.getQInstance().getEnvironmentValues(), Collections.emptyMap()));
metaDataOutput.setHelpContents(QContext.getQInstance().getHelpContent());
metaDataOutput.setHelpContents(Objects.requireNonNullElse(QContext.getQInstance().getHelpContent(), Collections.emptyMap()));
// todo post-customization - can do whatever w/ the result if you want?
metaDataOutput.setSupplementalInstanceMetaData(QContext.getQInstance().getSupplementalMetaData());
try
{
customizer.postProcess(metaDataOutput);
}
catch(QUserFacingException e)
{
LOG.debug("User-facing exception thrown in meta-data customizer post-processing", e);
}
catch(Exception e)
{
LOG.warn("Unexpected error thrown in meta-data customizer post-processing", e);
}
return metaDataOutput;
}
@ -306,26 +322,38 @@ public class MetaDataAction
/***************************************************************************
**
***************************************************************************/
private MetaDataFilterInterface getMetaDataFilter()
private MetaDataActionCustomizerInterface getMetaDataActionCustomizer()
{
return metaDataFilterMemoization.getResult(QContext.getQInstance(), i ->
return metaDataActionCustomizerMemoization.getResult(QContext.getQInstance(), i ->
{
MetaDataFilterInterface filter = null;
QCodeReference metaDataFilterReference = QContext.getQInstance().getMetaDataFilter();
if(metaDataFilterReference != null)
MetaDataActionCustomizerInterface actionCustomizer = null;
QCodeReference metaDataActionCustomizerReference = QContext.getQInstance().getMetaDataActionCustomizer();
if(metaDataActionCustomizerReference != null)
{
filter = QCodeLoader.getAdHoc(MetaDataFilterInterface.class, metaDataFilterReference);
LOG.debug("Using new meta-data filter of type: " + filter.getClass().getSimpleName());
actionCustomizer = QCodeLoader.getAdHoc(MetaDataActionCustomizerInterface.class, metaDataActionCustomizerReference);
}
if(filter == null)
if(actionCustomizer == null)
{
filter = new AllowAllMetaDataFilter();
LOG.debug("Using new default (allow-all) meta-data filter");
/////////////////////////////////////////////////////////////////////////////////////
// check if QInstance is still using the now-deprecated getMetaDataFilter approach //
/////////////////////////////////////////////////////////////////////////////////////
@SuppressWarnings("deprecation")
QCodeReference metaDataFilterReference = QContext.getQInstance().getMetaDataFilter();
if(metaDataFilterReference != null)
{
LOG.warn("QInstance.metaDataFilter is deprecated in favor of metaDataActionCustomizer.");
actionCustomizer = QCodeLoader.getAdHoc(MetaDataActionCustomizerInterface.class, metaDataFilterReference);
}
}
return (filter);
}).orElseThrow(() -> new QRuntimeException("Error getting metaDataFilter"));
if(actionCustomizer == null)
{
actionCustomizer = new DefaultNoopMetaDataActionCustomizer();
}
return (actionCustomizer);
}).orElseThrow(() -> new QRuntimeException("Error getting MetaDataActionCustomizer"));
}

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.actions.metadata;
import com.kingsrook.qqq.backend.core.exceptions.QException;
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.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;
/*******************************************************************************
** Interface for customizations that can be injected by an application into
** the MetaDataAction - e.g., loading applicable meta-data for a user into a
** frontend.
*******************************************************************************/
public interface MetaDataActionCustomizerInterface
{
/***************************************************************************
**
***************************************************************************/
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);
/***************************************************************************
**
***************************************************************************/
default void postProcess(MetaDataOutput metaDataOutput) throws QException
{
/////////////////////
// noop by default //
/////////////////////
}
}

View File

@ -22,43 +22,11 @@
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
@Deprecated(since = "migrated to metaDataCustomizer")
public interface MetaDataFilterInterface extends MetaDataActionCustomizerInterface
{
/***************************************************************************
**
***************************************************************************/
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

@ -36,6 +36,8 @@ import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
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.QueryOutput;
@ -46,6 +48,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaD
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionInputMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
@ -188,21 +191,40 @@ public class RunBackendStepAction
{
if(CollectionUtils.nullSafeIsEmpty(runBackendStepInput.getRecords()))
{
QueryInput queryInput = new QueryInput();
queryInput.setTableName(inputMetaData.getRecordListMetaData().getTableName());
QTableMetaData table = QContext.getQInstance().getTable(inputMetaData.getRecordListMetaData().getTableName());
QueryInput queryInput = new QueryInput();
queryInput.setTableName(table.getName());
// todo - handle this being async (e.g., http)
// seems like it just needs to throw, breaking this flow, and to send a response to the frontend, directing it to prompt the user for the needed data
// then this step can re-run, hopefully with the needed data.
QProcessCallback callback = runBackendStepInput.getCallback();
if(callback == null)
//////////////////////////////////////////////////
// look for record ids in the input data values //
//////////////////////////////////////////////////
String recordIds = runBackendStepInput.getValueString("recordIds");
if(recordIds == null)
{
throw (new QUserFacingException("Missing input records.",
new QException("Function is missing input records, but no callback was present to request fields from a user")));
recordIds = runBackendStepInput.getValueString("recordId");
}
queryInput.setFilter(callback.getQueryFilter());
///////////////////////////////////////////////////////////
// if records were found, add as criteria to query input //
///////////////////////////////////////////////////////////
if(recordIds != null)
{
queryInput.setFilter(new QQueryFilter(new QFilterCriteria(table.getPrimaryKeyField(), QCriteriaOperator.IN, recordIds.split(","))));
}
else
{
// todo - handle this being async (e.g., http)
// seems like it just needs to throw, breaking this flow, and to send a response to the frontend, directing it to prompt the user for the needed data
// then this step can re-run, hopefully with the needed data.
QProcessCallback callback = runBackendStepInput.getCallback();
if(callback == null)
{
throw (new QUserFacingException("Missing input records.",
new QException("Function is missing input records, but no callback was present to request fields from a user")));
}
queryInput.setFilter(callback.getQueryFilter());
}
//////////////////////////////////////////////////////////////////////////////////////////
// if process has a max-no of records, set a limit on the process of that number plus 1 //
@ -210,7 +232,7 @@ public class RunBackendStepAction
//////////////////////////////////////////////////////////////////////////////////////////
if(process.getMaxInputRecords() != null)
{
if(callback.getQueryFilter() == null)
if(queryInput.getFilter() == null)
{
queryInput.setFilter(new QQueryFilter());
}

View File

@ -32,6 +32,7 @@ import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
import com.kingsrook.qqq.backend.core.actions.dashboard.widgets.NoCodeWidgetRenderer;
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
@ -53,6 +54,7 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
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.QBackendMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.NoCodeWidgetFrontendComponentMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData;
@ -63,6 +65,7 @@ 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;
import com.kingsrook.qqq.backend.core.processes.tracing.ProcessTracerInterface;
import com.kingsrook.qqq.backend.core.state.InMemoryStateProvider;
import com.kingsrook.qqq.backend.core.state.StateProviderInterface;
import com.kingsrook.qqq.backend.core.state.StateType;
@ -71,6 +74,7 @@ import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import org.apache.commons.lang.BooleanUtils;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
@ -87,12 +91,16 @@ public class RunProcessAction
public static final String BASEPULL_TIMESTAMP_FIELD = "basepullTimestampField";
public static final String BASEPULL_CONFIGURATION = "basepullConfiguration";
public static final String PROCESS_TRACER_CODE_REFERENCE_FIELD = "processTracerCodeReference";
////////////////////////////////////////////////////////////////////////////////////////////////
// indicator that the timestamp field should be updated - e.g., the execute step is finished. //
////////////////////////////////////////////////////////////////////////////////////////////////
public static final String BASEPULL_READY_TO_UPDATE_TIMESTAMP_FIELD = "basepullReadyToUpdateTimestamp";
public static final String BASEPULL_DID_QUERY_USING_TIMESTAMP_FIELD = "basepullDidQueryUsingTimestamp";
private ProcessTracerInterface processTracer;
/*******************************************************************************
@ -119,9 +127,17 @@ public class RunProcessAction
}
runProcessOutput.setProcessUUID(runProcessInput.getProcessUUID());
traceStartOrResume(runProcessInput, process);
UUIDAndTypeStateKey stateKey = new UUIDAndTypeStateKey(UUID.fromString(runProcessInput.getProcessUUID()), StateType.PROCESS_STATUS);
ProcessState processState = primeProcessState(runProcessInput, stateKey, process);
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
// these should always be clear when we're starting a run - so make sure they haven't leaked from previous //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
processState.clearNextStepName();
processState.clearBackStepName();
/////////////////////////////////////////////////////////
// if process is 'basepull' style, keep track of 'now' //
/////////////////////////////////////////////////////////
@ -160,6 +176,7 @@ public class RunProcessAction
////////////////////////////////////////////////////////////
// upon exception (e.g., one thrown by a step), throw it. //
////////////////////////////////////////////////////////////
traceBreakOrFinish(runProcessInput, runProcessOutput, qe);
throw (qe);
}
catch(Exception e)
@ -167,6 +184,7 @@ public class RunProcessAction
////////////////////////////////////////////////////////////
// upon exception (e.g., one thrown by a step), throw it. //
////////////////////////////////////////////////////////////
traceBreakOrFinish(runProcessInput, runProcessOutput, e);
throw (new QException("Error running process", e));
}
finally
@ -177,6 +195,8 @@ public class RunProcessAction
runProcessOutput.setProcessState(processState);
}
traceBreakOrFinish(runProcessInput, runProcessOutput, null);
return (runProcessOutput);
}
@ -188,14 +208,35 @@ public class RunProcessAction
private void runLinearStepLoop(QProcessMetaData process, ProcessState processState, UUIDAndTypeStateKey stateKey, RunProcessInput runProcessInput, RunProcessOutput runProcessOutput) throws Exception
{
String lastStepName = runProcessInput.getStartAfterStep();
String startAtStep = runProcessInput.getStartAtStep();
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. //
// deal with if we were told, from the input, to start After a step, or start At a step. //
///////////////////////////////////////////////////////////////////////////////////////////////////////
List<QStepMetaData> stepList = getAvailableStepList(processState, process, lastStepName);
List<QStepMetaData> stepList;
if(startAtStep == null)
{
stepList = getAvailableStepList(processState, process, lastStepName, false);
}
else
{
stepList = getAvailableStepList(processState, process, startAtStep, true);
///////////////////////////////////////////////////////////////////////////////////
// clear this field - so after we run a step, we'll then loop in last-step mode. //
///////////////////////////////////////////////////////////////////////////////////
startAtStep = null;
///////////////////////////////////////////////////////////////////////////////////
// if we're going to run a backend step now, let it see that this is a step-back //
///////////////////////////////////////////////////////////////////////////////////
processState.setIsStepBack(true);
}
if(stepList.isEmpty())
{
break;
@ -232,7 +273,18 @@ public class RunProcessAction
//////////////////////////////////////////////////
throw (new QException("Unsure how to run a step of type: " + step.getClass().getName()));
}
////////////////////////////////////////////////////////////////////////////////////////
// only let this value be set for the original back step - don't let it stick around. //
// if a process wants to keep track of this itself, it can, but in a different slot. //
////////////////////////////////////////////////////////////////////////////////////////
processState.setIsStepBack(false);
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// in case we broke from the loop above (e.g., by going directly into a frontend step), once again make sure to lower this flag. //
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
processState.setIsStepBack(false);
}
@ -264,6 +316,12 @@ public class RunProcessAction
processFrontendStepFieldDefaultValues(processState, step);
processFrontendComponents(processState, step);
processState.setNextStepName(step.getName());
if(StringUtils.hasContent(step.getBackStepName()) && processState.getBackStepName().isEmpty())
{
processState.setBackStepName(step.getBackStepName());
}
return LoopTodo.BREAK;
}
case SKIP ->
@ -317,6 +375,7 @@ public class RunProcessAction
// else run the given lastStepName //
/////////////////////////////////////
processState.clearNextStepName();
processState.clearBackStepName();
step = process.getStep(lastStepName);
if(step == null)
{
@ -398,6 +457,7 @@ public class RunProcessAction
// its sub-steps, or, to fall out of the loop and end the process. //
//////////////////////////////////////////////////////////////////////////////////////////////////////
processState.clearNextStepName();
processState.clearBackStepName();
runStateMachineStep(nextStepName.get(), process, processState, stateKey, runProcessInput, runProcessOutput, stackDepth + 1);
return;
}
@ -584,6 +644,7 @@ public class RunProcessAction
runBackendStepInput.setCallback(runProcessInput.getCallback());
runBackendStepInput.setFrontendStepBehavior(runProcessInput.getFrontendStepBehavior());
runBackendStepInput.setAsyncJobCallback(runProcessInput.getAsyncJobCallback());
runBackendStepInput.setProcessTracer(processTracer);
runBackendStepInput.setTableName(process.getTableName());
if(!StringUtils.hasContent(runBackendStepInput.getTableName()))
@ -605,9 +666,13 @@ public class RunProcessAction
runBackendStepInput.setBasepullLastRunTime((Instant) runProcessInput.getValues().get(BASEPULL_LAST_RUNTIME_KEY));
}
traceStepStart(runBackendStepInput);
RunBackendStepOutput runBackendStepOutput = new RunBackendStepAction().execute(runBackendStepInput);
storeState(stateKey, runBackendStepOutput.getProcessState());
traceStepFinish(runBackendStepInput, runBackendStepOutput);
if(runBackendStepOutput.getException() != null)
{
runProcessOutput.setException(runBackendStepOutput.getException());
@ -621,8 +686,10 @@ public class RunProcessAction
/*******************************************************************************
** Get the list of steps which are eligible to run.
**
** lastStep will be included in the list, or not, based on includeLastStep.
*******************************************************************************/
private List<QStepMetaData> getAvailableStepList(ProcessState processState, QProcessMetaData process, String lastStep) throws QException
static List<QStepMetaData> getAvailableStepList(ProcessState processState, QProcessMetaData process, String lastStep, boolean includeLastStep) throws QException
{
if(lastStep == null)
{
@ -649,6 +716,10 @@ public class RunProcessAction
if(stepName.equals(lastStep))
{
foundLastStep = true;
if(includeLastStep)
{
validStepNames.add(stepName);
}
}
}
return (stepNamesToSteps(process, validStepNames));
@ -660,7 +731,7 @@ public class RunProcessAction
/*******************************************************************************
**
*******************************************************************************/
private List<QStepMetaData> stepNamesToSteps(QProcessMetaData process, List<String> stepNames) throws QException
private static List<QStepMetaData> stepNamesToSteps(QProcessMetaData process, List<String> stepNames) throws QException
{
List<QStepMetaData> result = new ArrayList<>();
@ -744,13 +815,14 @@ public class RunProcessAction
{
QSession session = QContext.getQSession();
QBackendMetaData backendMetaData = QContext.getQInstance().getBackend(process.getVariantBackend());
if(session.getBackendVariants() == null || !session.getBackendVariants().containsKey(backendMetaData.getVariantOptionsTableTypeValue()))
String variantTypeKey = backendMetaData.getBackendVariantsConfig().getVariantTypeKey();
if(session.getBackendVariants() == null || !session.getBackendVariants().containsKey(variantTypeKey))
{
LOG.warn("Could not find Backend Variant information for Backend '" + backendMetaData.getName() + "'");
}
else
{
basepullKeyValue += "-" + session.getBackendVariants().get(backendMetaData.getVariantOptionsTableTypeValue());
basepullKeyValue += "-" + session.getBackendVariants().get(variantTypeKey);
}
}
@ -879,4 +951,153 @@ public class RunProcessAction
runProcessInput.getValues().put(BASEPULL_TIMESTAMP_FIELD, basepullConfiguration.getTimestampField());
runProcessInput.getValues().put(BASEPULL_CONFIGURATION, basepullConfiguration);
}
/***************************************************************************
**
***************************************************************************/
private void setupProcessTracer(RunProcessInput runProcessInput, QProcessMetaData process)
{
try
{
if(process.getProcessTracerCodeReference() != null)
{
processTracer = QCodeLoader.getAdHoc(ProcessTracerInterface.class, process.getProcessTracerCodeReference());
}
Serializable processTracerCodeReference = runProcessInput.getValue(PROCESS_TRACER_CODE_REFERENCE_FIELD);
if(processTracerCodeReference != null)
{
if(processTracerCodeReference instanceof QCodeReference codeReference)
{
processTracer = QCodeLoader.getAdHoc(ProcessTracerInterface.class, codeReference);
}
}
}
catch(Exception e)
{
LOG.warn("Error setting up processTracer", e, logPair("processName", runProcessInput.getProcessName()));
}
}
/***************************************************************************
**
***************************************************************************/
private void traceStartOrResume(RunProcessInput runProcessInput, QProcessMetaData process)
{
setupProcessTracer(runProcessInput, process);
try
{
if(processTracer != null)
{
if(StringUtils.hasContent(runProcessInput.getStartAfterStep()) || StringUtils.hasContent(runProcessInput.getStartAtStep()))
{
processTracer.handleProcessResume(runProcessInput);
}
else
{
processTracer.handleProcessStart(runProcessInput);
}
}
}
catch(Exception e)
{
LOG.info("Error in traceStart", e, logPair("processName", runProcessInput.getProcessName()));
}
}
/***************************************************************************
**
***************************************************************************/
private void traceBreakOrFinish(RunProcessInput runProcessInput, RunProcessOutput runProcessOutput, Exception processException)
{
try
{
if(processTracer != null)
{
ProcessState processState = runProcessOutput.getProcessState();
boolean isBreak = true;
/////////////////////////////////////////////////////////////
// if there's no next step, that means the process is done //
/////////////////////////////////////////////////////////////
if(processState.getNextStepName().isEmpty())
{
isBreak = false;
}
else
{
/////////////////////////////////////////////////////////////////
// or if the next step is the last index, then we're also done //
/////////////////////////////////////////////////////////////////
String nextStepName = processState.getNextStepName().get();
int nextStepIndex = processState.getStepList().indexOf(nextStepName);
if(nextStepIndex == processState.getStepList().size() - 1)
{
isBreak = false;
}
}
if(isBreak)
{
processTracer.handleProcessBreak(runProcessInput, runProcessOutput, processException);
}
else
{
processTracer.handleProcessFinish(runProcessInput, runProcessOutput, processException);
}
}
}
catch(Exception e)
{
LOG.info("Error in traceProcessFinish", e, logPair("processName", runProcessInput.getProcessName()));
}
}
/***************************************************************************
**
***************************************************************************/
private void traceStepStart(RunBackendStepInput runBackendStepInput)
{
try
{
if(processTracer != null)
{
processTracer.handleStepStart(runBackendStepInput);
}
}
catch(Exception e)
{
LOG.info("Error in traceStepFinish", e, logPair("processName", runBackendStepInput.getProcessName()), logPair("stepName", runBackendStepInput.getStepName()));
}
}
/***************************************************************************
**
***************************************************************************/
private void traceStepFinish(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput)
{
try
{
if(processTracer != null)
{
processTracer.handleStepFinish(runBackendStepInput, runBackendStepOutput);
}
}
catch(Exception e)
{
LOG.info("Error in traceStepFinish", e, logPair("processName", runBackendStepInput.getProcessName()), logPair("stepName", runBackendStepInput.getStepName()));
}
}
}

View File

@ -108,7 +108,7 @@ public class CsvExportStreamer implements ExportStreamerInterface
}
catch(Exception e)
{
throw (new QReportingException("Error starting CSV report"));
throw (new QReportingException("Error starting CSV report", e));
}
}

View File

@ -54,6 +54,14 @@ public interface ExportStreamerInterface
// noop in base class
}
/***************************************************************************
**
***************************************************************************/
default void setExportStyleCustomizer(ExportStyleCustomizerInterface exportStyleCustomizer)
{
// noop in base class
}
/*******************************************************************************
** Called once per sheet, before any rows are available. Meant to write a
** header, for example.

View File

@ -0,0 +1,35 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. 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;
/*******************************************************************************
** interface for classes that can be used to customize visual style aspects of
** exports/reports.
**
** Anticipates very different sub-interfaces based on the file type being generated,
** and the capabilities of each. e.g., excel (bolds, fonts, cell merging) vs
** json (different structure of objects).
*******************************************************************************/
public interface ExportStyleCustomizerInterface
{
}

View File

@ -71,6 +71,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.QueryJoin;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldAndJoinTable;
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.reporting.QReportDataSource;
@ -162,6 +163,17 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
reportStreamer = reportFormat.newReportStreamer();
}
if(reportInput.getExportStyleCustomizer() != null)
{
ExportStyleCustomizerInterface styleCustomizer = QCodeLoader.getAdHoc(ExportStyleCustomizerInterface.class, reportInput.getExportStyleCustomizer());
reportStreamer.setExportStyleCustomizer(styleCustomizer);
}
else if(report.getExportStyleCustomizer() != null)
{
ExportStyleCustomizerInterface styleCustomizer = QCodeLoader.getAdHoc(ExportStyleCustomizerInterface.class, report.getExportStyleCustomizer());
reportStreamer.setExportStyleCustomizer(styleCustomizer);
}
reportStreamer.preRun(reportInput.getReportDestination(), views);
////////////////////////////////////////////////////////////////////////////////////////////////
@ -210,7 +222,8 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
/////////////////////////////////////////////////////////////////////////////////////////
if(dataSourceTableView.getViewCustomizer() != null)
{
Function<QReportView, QReportView> viewCustomizerFunction = QCodeLoader.getFunction(dataSourceTableView.getViewCustomizer());
@SuppressWarnings("unchecked")
Function<QReportView, QReportView> viewCustomizerFunction = QCodeLoader.getAdHoc(Function.class, dataSourceTableView.getViewCustomizer());
if(viewCustomizerFunction instanceof ReportViewCustomizer reportViewCustomizer)
{
reportViewCustomizer.setReportInput(reportInput);
@ -567,7 +580,7 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
// all pivotFields that are possible value sources are implicitly translated //
///////////////////////////////////////////////////////////////////////////////
QTableMetaData mainTable = QContext.getQInstance().getTable(dataSource.getSourceTable());
FieldAndJoinTable fieldAndJoinTable = getFieldAndJoinTable(mainTable, summaryFieldName);
FieldAndJoinTable fieldAndJoinTable = FieldAndJoinTable.get(mainTable, summaryFieldName);
if(fieldAndJoinTable.field().getPossibleValueSourceName() != null)
{
fieldsToTranslatePossibleValues.add(summaryFieldName);
@ -580,32 +593,6 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
/*******************************************************************************
**
*******************************************************************************/
public static FieldAndJoinTable getFieldAndJoinTable(QTableMetaData mainTable, String fieldName) throws QException
{
if(fieldName.indexOf('.') > -1)
{
String joinTableName = fieldName.replaceAll("\\..*", "");
String joinFieldName = fieldName.replaceAll(".*\\.", "");
QTableMetaData joinTable = QContext.getQInstance().getTable(joinTableName);
if(joinTable == null)
{
throw (new QException("Unrecognized join table name: " + joinTableName));
}
return new FieldAndJoinTable(joinTable.getField(joinFieldName), joinTable);
}
else
{
return new FieldAndJoinTable(mainTable.getField(fieldName), mainTable);
}
}
/*******************************************************************************
**
*******************************************************************************/
@ -685,7 +672,7 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if any fields are 'showPossibleValueLabel', then move display values for them into the record's values map //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
for(QReportField column : tableView.getColumns())
for(QReportField column : CollectionUtils.nonNullList(tableView.getColumns()))
{
if(column.getShowPossibleValueLabel())
{
@ -756,7 +743,7 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
SummaryKey key = new SummaryKey();
for(String summaryFieldName : view.getSummaryFields())
{
FieldAndJoinTable fieldAndJoinTable = getFieldAndJoinTable(table, summaryFieldName);
FieldAndJoinTable fieldAndJoinTable = FieldAndJoinTable.get(table, summaryFieldName);
Serializable summaryValue = record.getValue(summaryFieldName);
if(fieldAndJoinTable.field().getPossibleValueSourceName() != null)
{
@ -811,7 +798,7 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
//////////////////////////////////////////////////////
// todo - memoize this, if we ever need to optimize //
//////////////////////////////////////////////////////
FieldAndJoinTable fieldAndJoinTable = getFieldAndJoinTable(table, fieldName);
FieldAndJoinTable fieldAndJoinTable = FieldAndJoinTable.get(table, fieldName);
field = fieldAndJoinTable.field();
}
catch(Exception e)
@ -956,7 +943,7 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
List<QFieldMetaData> fields = new ArrayList<>();
for(String summaryFieldName : view.getSummaryFields())
{
FieldAndJoinTable fieldAndJoinTable = getFieldAndJoinTable(table, summaryFieldName);
FieldAndJoinTable fieldAndJoinTable = FieldAndJoinTable.get(table, summaryFieldName);
fields.add(new QFieldMetaData(summaryFieldName, fieldAndJoinTable.field().getType()).withLabel(fieldAndJoinTable.field().getLabel())); // todo do we need the type? if so need table as input here
}
for(QReportField column : view.getColumns())
@ -1208,27 +1195,4 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
{
}
/*******************************************************************************
**
*******************************************************************************/
public record FieldAndJoinTable(QFieldMetaData field, QTableMetaData joinTable)
{
/*******************************************************************************
**
*******************************************************************************/
public String getLabel(QTableMetaData mainTable)
{
if(mainTable.getName().equals(joinTable.getName()))
{
return (field.getLabel());
}
else
{
return (joinTable.getLabel() + ": " + field.getLabel());
}
}
}
}

View File

@ -46,6 +46,7 @@ import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;
import com.kingsrook.qqq.backend.core.actions.reporting.ExportStreamerInterface;
import com.kingsrook.qqq.backend.core.actions.reporting.ExportStyleCustomizerInterface;
import com.kingsrook.qqq.backend.core.actions.reporting.ReportUtils;
import com.kingsrook.qqq.backend.core.exceptions.QReportingException;
import com.kingsrook.qqq.backend.core.instances.QInstanceEnricher;
@ -77,6 +78,7 @@ import org.apache.poi.xssf.usermodel.XSSFPivotTable;
import org.apache.poi.xssf.usermodel.XSSFRow;
import org.apache.poi.xssf.usermodel.XSSFSheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
@ -112,9 +114,10 @@ public class ExcelPoiBasedStreamingExportStreamer implements ExportStreamerInter
public static final String EXCEL_DATE_FORMAT = "yyyy-MM-dd";
public static final String EXCEL_DATE_TIME_FORMAT = "yyyy-MM-dd H:mm:ss";
private PoiExcelStylerInterface poiExcelStylerInterface = getStylerInterface();
private ExcelPoiBasedStreamingStyleCustomizerInterface styleCustomizerInterface;
private Map<String, String> excelCellFormats;
private Map<String, XSSFCellStyle> styles = new HashMap<>();
private Map<String, XSSFCellStyle> styles = new HashMap<>();
private int rowNo = 0;
private int sheetIndex = 1;
@ -402,6 +405,7 @@ public class ExcelPoiBasedStreamingExportStreamer implements ExportStreamerInter
dateTimeStyle.setDataFormat(createHelper.createDataFormat().getFormat(EXCEL_DATE_TIME_FORMAT));
styles.put("datetime", dateTimeStyle);
PoiExcelStylerInterface poiExcelStylerInterface = getStylerInterface();
styles.put("title", poiExcelStylerInterface.createStyleForTitle(workbook, createHelper));
styles.put("header", poiExcelStylerInterface.createStyleForHeader(workbook, createHelper));
styles.put("footer", poiExcelStylerInterface.createStyleForFooter(workbook, createHelper));
@ -413,6 +417,11 @@ public class ExcelPoiBasedStreamingExportStreamer implements ExportStreamerInter
XSSFCellStyle footerDateTimeStyle = poiExcelStylerInterface.createStyleForFooter(workbook, createHelper);
footerDateTimeStyle.setDataFormat(createHelper.createDataFormat().getFormat(EXCEL_DATE_TIME_FORMAT));
styles.put("footer-datetime", footerDateTimeStyle);
if(styleCustomizerInterface != null)
{
styleCustomizerInterface.customizeStyles(styles, workbook, createHelper);
}
}
@ -458,7 +467,7 @@ public class ExcelPoiBasedStreamingExportStreamer implements ExportStreamerInter
}
else
{
sheetWriter.beginSheet();
sheetWriter.beginSheet(view, styleCustomizerInterface);
////////////////////////////////////////////////
// put the title and header rows in the sheet //
@ -560,6 +569,16 @@ public class ExcelPoiBasedStreamingExportStreamer implements ExportStreamerInter
/***************************************************************************
**
***************************************************************************/
public static void setStyleForField(QRecord record, String fieldName, String styleName)
{
record.setDisplayValue(fieldName + ":excelStyle", styleName);
}
/*******************************************************************************
**
*******************************************************************************/
@ -567,12 +586,12 @@ public class ExcelPoiBasedStreamingExportStreamer implements ExportStreamerInter
{
sheetWriter.insertRow(rowNo++);
int styleIndex = -1;
int baseStyleIndex = -1;
int dateStyleIndex = styles.get("date").getIndex();
int dateTimeStyleIndex = styles.get("datetime").getIndex();
if(isFooter)
{
styleIndex = styles.get("footer").getIndex();
baseStyleIndex = styles.get("footer").getIndex();
dateStyleIndex = styles.get("footer-date").getIndex();
dateTimeStyleIndex = styles.get("footer-datetime").getIndex();
}
@ -582,6 +601,13 @@ public class ExcelPoiBasedStreamingExportStreamer implements ExportStreamerInter
{
Serializable value = qRecord.getValue(field.getName());
String overrideStyleName = qRecord.getDisplayValue(field.getName() + ":excelStyle");
int styleIndex = baseStyleIndex;
if(overrideStyleName != null)
{
styleIndex = styles.get(overrideStyleName).getIndex();
}
if(value != null)
{
if(value instanceof String s)
@ -706,7 +732,7 @@ public class ExcelPoiBasedStreamingExportStreamer implements ExportStreamerInter
{
if(!ReportType.PIVOT.equals(currentView.getType()))
{
sheetWriter.endSheet();
sheetWriter.endSheet(currentView, styleCustomizerInterface);
}
activeSheetWriter.flush();
@ -815,7 +841,29 @@ public class ExcelPoiBasedStreamingExportStreamer implements ExportStreamerInter
*******************************************************************************/
protected PoiExcelStylerInterface getStylerInterface()
{
if(styleCustomizerInterface != null)
{
return styleCustomizerInterface.getExcelStyler();
}
return (new PlainPoiExcelStyler());
}
/***************************************************************************
**
***************************************************************************/
@Override
public void setExportStyleCustomizer(ExportStyleCustomizerInterface exportStyleCustomizer)
{
if(exportStyleCustomizer instanceof ExcelPoiBasedStreamingStyleCustomizerInterface poiExcelStylerInterface)
{
this.styleCustomizerInterface = poiExcelStylerInterface;
}
else
{
LOG.debug("Supplied export style customizer is not an instance of ExcelPoiStyleCustomizerInterface, so will not be used for an excel export", logPair("exportStyleCustomizerClass", exportStyleCustomizer.getClass().getSimpleName()));
}
}
}

View File

@ -0,0 +1,81 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. 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.excel.poi;
import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.actions.reporting.ExportStyleCustomizerInterface;
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportView;
import org.apache.poi.ss.usermodel.CreationHelper;
import org.apache.poi.xssf.usermodel.XSSFCellStyle;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
/*******************************************************************************
** style customization points for Excel files generated via our streaming POI.
*******************************************************************************/
public interface ExcelPoiBasedStreamingStyleCustomizerInterface extends ExportStyleCustomizerInterface
{
/***************************************************************************
** slightly legacy way we did excel styles - but get an instance of object
** that defaults "default" styles (header, footer, etc).
***************************************************************************/
default PoiExcelStylerInterface getExcelStyler()
{
return (new PlainPoiExcelStyler());
}
/***************************************************************************
** either change "default" styles put in the styles map, or create new ones
** which can then be applied to row/field values (cells) via:
** ExcelPoiBasedStreamingExportStreamer.setStyleForField(row, fieldName, styleName);
***************************************************************************/
default void customizeStyles(Map<String, XSSFCellStyle> styles, XSSFWorkbook workbook, CreationHelper createHelper)
{
//////////////////
// noop default //
//////////////////
}
/***************************************************************************
** for a given view (sheet), return a list of custom column widths.
** any nulls in the list are ignored (so default width is used).
***************************************************************************/
default List<Integer> getColumnWidthsForView(QReportView view)
{
return (null);
}
/***************************************************************************
** for a given view (sheet), return a list of any ranges which should be
** merged, as in "A1:C1" (first three cells in first row).
***************************************************************************/
default List<String> getMergedRangesForView(QReportView view)
{
return (null);
}
}

View File

@ -25,7 +25,10 @@ package com.kingsrook.qqq.backend.core.actions.reporting.excel.poi;
import java.io.IOException;
import java.io.Writer;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportView;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import org.apache.poi.ss.util.CellReference;
@ -53,13 +56,33 @@ public class StreamedSheetWriter
/*******************************************************************************
**
*******************************************************************************/
public void beginSheet() throws IOException
public void beginSheet(QReportView view, ExcelPoiBasedStreamingStyleCustomizerInterface styleCustomizerInterface) throws IOException
{
writer.write("""
<?xml version="1.0" encoding="UTF-8"?>
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
<sheetData>""");
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">""");
if(styleCustomizerInterface != null && view != null)
{
List<Integer> columnWidths = styleCustomizerInterface.getColumnWidthsForView(view);
if(CollectionUtils.nullSafeHasContents(columnWidths))
{
writer.write("<cols>");
for(int i = 0; i < columnWidths.size(); i++)
{
Integer width = columnWidths.get(i);
if(width != null)
{
writer.write("""
<col min="%d" max="%d" width="%d" customWidth="1"/>
""".formatted(i + 1, i + 1, width));
}
}
writer.write("</cols>");
}
}
writer.write("<sheetData>");
}
@ -67,11 +90,25 @@ public class StreamedSheetWriter
/*******************************************************************************
**
*******************************************************************************/
public void endSheet() throws IOException
public void endSheet(QReportView view, ExcelPoiBasedStreamingStyleCustomizerInterface styleCustomizerInterface) throws IOException
{
writer.write("""
</sheetData>
</worksheet>""");
writer.write("</sheetData>");
if(styleCustomizerInterface != null && view != null)
{
List<String> mergedRanges = styleCustomizerInterface.getMergedRangesForView(view);
if(CollectionUtils.nullSafeHasContents(mergedRanges))
{
writer.write(String.format("<mergeCells count=\"%d\">", mergedRanges.size()));
for(String range : mergedRanges)
{
writer.write(String.format("<mergeCell ref=\"%s\"/>", range));
}
writer.write("</mergeCells>");
}
}
writer.write("</worksheet>");
}
@ -151,7 +188,7 @@ public class StreamedSheetWriter
{
rs.append("&quot;");
}
else if (c < 32 && c != '\t' && c != '\n')
else if(c < 32 && c != '\t' && c != '\n')
{
rs.append(' ');
}

View File

@ -53,7 +53,8 @@ public class QJavaExecutor implements QCodeExecutor
Serializable output;
try
{
Function<Map<String, Object>, Serializable> function = QCodeLoader.getFunction(codeReference);
@SuppressWarnings("unchecked")
Function<Map<String, Object>, Serializable> function = QCodeLoader.getAdHoc(Function.class, codeReference);
output = function.apply(context);
}
catch(Exception e)

View File

@ -32,6 +32,7 @@ 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.count.CountInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.querystats.QueryStat;
@ -82,6 +83,22 @@ public class CountAction
/*******************************************************************************
** shorthand way to call for the most common use-case, when you just want the
** count to be returned, and you just want to pass in a table name and filter.
*******************************************************************************/
public static Integer execute(String tableName, QQueryFilter filter) throws QException
{
CountAction countAction = new CountAction();
CountInput countInput = new CountInput();
countInput.setTableName(tableName);
countInput.setFilter(filter);
CountOutput countOutput = countAction.execute(countInput);
return (countOutput.getCount());
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -82,6 +82,11 @@ public class DeleteAction
{
ActionHelper.validateSession(deleteInput);
if(deleteInput.getTableName() == null)
{
throw (new QException("Table name was not specified in delete input"));
}
QTableMetaData table = deleteInput.getTable();
String primaryKeyFieldName = table.getPrimaryKeyField();
QFieldMetaData primaryKeyField = table.getField(primaryKeyFieldName);
@ -241,6 +246,7 @@ public class DeleteAction
{
DMLAuditInput dmlAuditInput = new DMLAuditInput()
.withTableActionInput(deleteInput)
.withTransaction(deleteInput.getTransaction())
.withAuditContext(deleteInput.getAuditContext());
oldRecordList.ifPresent(l -> dmlAuditInput.setRecordList(l));
new DMLAuditAction().execute(dmlAuditInput);
@ -320,7 +326,7 @@ public class DeleteAction
QTableMetaData table = deleteInput.getTable();
List<QRecord> primaryKeysNotFound = validateRecordsExistAndCanBeAccessed(deleteInput, oldRecordList.get());
ValidateRecordSecurityLockHelper.validateSecurityFields(table, oldRecordList.get(), ValidateRecordSecurityLockHelper.Action.DELETE);
ValidateRecordSecurityLockHelper.validateSecurityFields(table, oldRecordList.get(), ValidateRecordSecurityLockHelper.Action.DELETE, deleteInput.getTransaction());
///////////////////////////////////////////////////////////////////////////
// after all validations, run the pre-delete customizer, if there is one //
@ -396,6 +402,7 @@ public class DeleteAction
if(CollectionUtils.nullSafeHasContents(associatedKeys))
{
DeleteInput nextLevelDeleteInput = new DeleteInput();
nextLevelDeleteInput.setFlags(deleteInput.getFlags());
nextLevelDeleteInput.setTransaction(deleteInput.getTransaction());
nextLevelDeleteInput.setTableName(association.getAssociatedTableName());
nextLevelDeleteInput.setPrimaryKeys(associatedKeys);

View File

@ -238,6 +238,11 @@ public class GetAction
*******************************************************************************/
public static QRecord execute(String tableName, Serializable primaryKey) throws QException
{
if(primaryKey instanceof QQueryFilter)
{
LOG.warn("Unexpected use of QQueryFilter instead of primary key in GetAction call");
}
GetAction getAction = new GetAction();
GetInput getInput = new GetInput(tableName).withPrimaryKey(primaryKey);
return getAction.executeForRecord(getInput);

View File

@ -34,7 +34,6 @@ import java.util.Set;
import java.util.stream.Collectors;
import com.kingsrook.qqq.backend.core.actions.AbstractQActionFunction;
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
import com.kingsrook.qqq.backend.core.actions.audits.DMLAuditAction;
import com.kingsrook.qqq.backend.core.actions.automation.AutomationStatus;
import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationStatusUpdater;
@ -54,6 +53,7 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn;
@ -67,6 +67,7 @@ import com.kingsrook.qqq.backend.core.model.statusmessages.QWarningMessage;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
@ -110,6 +111,12 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
public InsertOutput execute(InsertInput insertInput) throws QException
{
ActionHelper.validateSession(insertInput);
if(!StringUtils.hasContent(insertInput.getTableName()))
{
throw (new QException("Table name was not specified in insert input"));
}
QTableMetaData table = insertInput.getTable();
if(table == null)
@ -122,7 +129,7 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
/////////////////////////////
// run standard validators //
/////////////////////////////
performValidations(insertInput, false);
performValidations(insertInput, false, false);
//////////////////////////////////////////////////////
// use the backend module to actually do the insert //
@ -150,7 +157,7 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
//////////////////////////////////////////////////
// insert any associations in the input records //
//////////////////////////////////////////////////
manageAssociations(table, insertOutput.getRecords(), insertInput.getTransaction());
manageAssociations(table, insertOutput.getRecords(), insertInput);
//////////////////
// do the audit //
@ -163,13 +170,26 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
{
new DMLAuditAction().execute(new DMLAuditInput()
.withTableActionInput(insertInput)
.withTransaction(insertInput.getTransaction())
.withAuditContext(insertInput.getAuditContext())
.withRecordList(insertOutput.getRecords()));
}
//////////////////////////////////////////////////////////////
// finally, run the post-insert customizer, if there is one //
//////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////
// finally, run the post-insert customizers, if there are any //
////////////////////////////////////////////////////////////////
runPostInsertCustomizers(insertInput, table, insertOutput);
return insertOutput;
}
/***************************************************************************
**
***************************************************************************/
private static void runPostInsertCustomizers(InsertInput insertInput, QTableMetaData table, InsertOutput insertOutput)
{
Optional<TableCustomizerInterface> postInsertCustomizer = QCodeLoader.getTableCustomizer(table, TableCustomizers.POST_INSERT_RECORD.getRole());
if(postInsertCustomizer.isPresent())
{
@ -186,7 +206,25 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
}
}
return insertOutput;
///////////////////////////////////////////////
// run all of the instance-level customizers //
///////////////////////////////////////////////
List<QCodeReference> tableCustomizerCodes = QContext.getQInstance().getTableCustomizers(TableCustomizers.POST_INSERT_RECORD);
for(QCodeReference tableCustomizerCode : tableCustomizerCodes)
{
try
{
TableCustomizerInterface tableCustomizer = QCodeLoader.getAdHoc(TableCustomizerInterface.class, tableCustomizerCode);
insertOutput.setRecords(tableCustomizer.postInsert(insertInput, insertOutput.getRecords()));
}
catch(Exception e)
{
for(QRecord record : insertOutput.getRecords())
{
record.addWarning(new QWarningMessage("An error occurred after the insert: " + e.getMessage()));
}
}
}
}
@ -225,7 +263,7 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
/*******************************************************************************
**
*******************************************************************************/
public void performValidations(InsertInput insertInput, boolean isPreview) throws QException
public void performValidations(InsertInput insertInput, boolean isPreview, boolean didAlreadyRunCustomizer) throws QException
{
if(CollectionUtils.nullSafeIsEmpty(insertInput.getRecords()))
{
@ -237,12 +275,10 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
///////////////////////////////////////////////////////////////////
// load the pre-insert customizer and set it up, if there is one //
// then we'll run it based on its WhenToRun value //
// note - if we already ran it, then don't re-run it! //
///////////////////////////////////////////////////////////////////
Optional<TableCustomizerInterface> preInsertCustomizer = QCodeLoader.getTableCustomizer(table, TableCustomizers.PRE_INSERT_RECORD.getRole());
if(preInsertCustomizer.isPresent())
{
runPreInsertCustomizerIfItIsTime(insertInput, isPreview, preInsertCustomizer, AbstractPreInsertCustomizer.WhenToRun.BEFORE_ALL_VALIDATIONS);
}
Optional<TableCustomizerInterface> preInsertCustomizer = didAlreadyRunCustomizer ? Optional.empty() : QCodeLoader.getTableCustomizer(table, TableCustomizers.PRE_INSERT_RECORD.getRole());
runPreInsertCustomizerIfItIsTime(insertInput, isPreview, preInsertCustomizer, AbstractPreInsertCustomizer.WhenToRun.BEFORE_ALL_VALIDATIONS);
setDefaultValuesInRecords(table, insertInput.getRecords());
@ -258,7 +294,7 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
}
runPreInsertCustomizerIfItIsTime(insertInput, isPreview, preInsertCustomizer, AbstractPreInsertCustomizer.WhenToRun.BEFORE_SECURITY_CHECKS);
ValidateRecordSecurityLockHelper.validateSecurityFields(insertInput.getTable(), insertInput.getRecords(), ValidateRecordSecurityLockHelper.Action.INSERT);
ValidateRecordSecurityLockHelper.validateSecurityFields(insertInput.getTable(), insertInput.getRecords(), ValidateRecordSecurityLockHelper.Action.INSERT, insertInput.getTransaction());
runPreInsertCustomizerIfItIsTime(insertInput, isPreview, preInsertCustomizer, AbstractPreInsertCustomizer.WhenToRun.AFTER_ALL_VALIDATIONS);
}
@ -303,6 +339,19 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
insertInput.setRecords(preInsertCustomizer.get().preInsert(insertInput, insertInput.getRecords(), isPreview));
}
}
///////////////////////////////////////////////
// run all of the instance-level customizers //
///////////////////////////////////////////////
List<QCodeReference> tableCustomizerCodes = QContext.getQInstance().getTableCustomizers(TableCustomizers.PRE_INSERT_RECORD);
for(QCodeReference tableCustomizerCode : tableCustomizerCodes)
{
TableCustomizerInterface tableCustomizer = QCodeLoader.getAdHoc(TableCustomizerInterface.class, tableCustomizerCode);
if(whenToRun.equals(tableCustomizer.whenToRunPreInsert(insertInput, isPreview)))
{
insertInput.setRecords(tableCustomizer.preInsert(insertInput, insertInput.getRecords(), isPreview));
}
}
}
@ -337,7 +386,7 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
/*******************************************************************************
**
*******************************************************************************/
private void manageAssociations(QTableMetaData table, List<QRecord> insertedRecords, QBackendTransaction transaction) throws QException
private void manageAssociations(QTableMetaData table, List<QRecord> insertedRecords, InsertInput insertInput) throws QException
{
for(Association association : CollectionUtils.nonNullList(table.getAssociations()))
{
@ -370,7 +419,8 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
if(CollectionUtils.nullSafeHasContents(nextLevelInserts))
{
InsertInput nextLevelInsertInput = new InsertInput();
nextLevelInsertInput.setTransaction(transaction);
nextLevelInsertInput.withFlags(insertInput.getFlags());
nextLevelInsertInput.setTransaction(insertInput.getTransaction());
nextLevelInsertInput.setTableName(association.getAssociatedTableName());
nextLevelInsertInput.setRecords(nextLevelInserts);
InsertOutput nextLevelInsertOutput = new InsertAction().execute(nextLevelInsertInput);

View File

@ -126,6 +126,7 @@ public class ReplaceAction extends AbstractQActionFunction<ReplaceInput, Replace
InsertInput insertInput = new InsertInput();
insertInput.setTableName(table.getName());
insertInput.setRecords(insertList);
insertInput.withFlags(input.getFlags());
insertInput.setTransaction(transaction);
insertInput.setOmitDmlAudit(input.getOmitDmlAudit());
InsertOutput insertOutput = new InsertAction().execute(insertInput);
@ -135,6 +136,7 @@ public class ReplaceAction extends AbstractQActionFunction<ReplaceInput, Replace
UpdateInput updateInput = new UpdateInput();
updateInput.setTableName(table.getName());
updateInput.setRecords(updateList);
updateInput.withFlags(input.getFlags());
updateInput.setTransaction(transaction);
updateInput.setOmitDmlAudit(input.getOmitDmlAudit());
UpdateOutput updateOutput = new UpdateAction().execute(updateInput);
@ -151,6 +153,7 @@ public class ReplaceAction extends AbstractQActionFunction<ReplaceInput, Replace
DeleteInput deleteInput = new DeleteInput();
deleteInput.setTableName(table.getName());
deleteInput.setQueryFilter(deleteFilter);
deleteInput.withFlags(input.getFlags());
deleteInput.setTransaction(transaction);
deleteInput.setOmitDmlAudit(input.getOmitDmlAudit());
DeleteOutput deleteOutput = new DeleteAction().execute(deleteInput);

View File

@ -47,7 +47,8 @@ public class StorageAction
{
/*******************************************************************************
**
** create an output stream in the storage backend - that can be written to,
** for the purpose of inserting or writing a file into storage.
*******************************************************************************/
public OutputStream createOutputStream(StorageInput storageInput) throws QException
{
@ -59,7 +60,8 @@ public class StorageAction
/*******************************************************************************
**
** create an input stream in the storage backend - that can be read from,
** for the purpose of getting or reading a file from storage.
*******************************************************************************/
public InputStream getInputStream(StorageInput storageInput) throws QException
{

View File

@ -57,6 +57,7 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.fields.DynamicDefaultValueBehavior;
import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldBehavior;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
@ -74,6 +75,7 @@ import com.kingsrook.qqq.backend.core.model.statusmessages.QWarningMessage;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import org.apache.commons.lang.BooleanUtils;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
@ -118,6 +120,11 @@ public class UpdateAction
{
ActionHelper.validateSession(updateInput);
if(!StringUtils.hasContent(updateInput.getTableName()))
{
throw (new QException("Table name was not specified in update input"));
}
QTableMetaData table = updateInput.getTable();
//////////////////////////////////////////////////////
@ -183,6 +190,7 @@ public class UpdateAction
else
{
DMLAuditInput dmlAuditInput = new DMLAuditInput()
.withTransaction(updateInput.getTransaction())
.withTableActionInput(updateInput)
.withRecordList(updateOutput.getRecords())
.withAuditContext(updateInput.getAuditContext());
@ -193,6 +201,18 @@ public class UpdateAction
//////////////////////////////////////////////////////////////
// finally, run the post-update customizer, if there is one //
//////////////////////////////////////////////////////////////
runPostUpdateCustomizers(updateInput, table, updateOutput, oldRecordList);
return updateOutput;
}
/***************************************************************************
**
***************************************************************************/
private static void runPostUpdateCustomizers(UpdateInput updateInput, QTableMetaData table, UpdateOutput updateOutput, Optional<List<QRecord>> oldRecordList)
{
Optional<TableCustomizerInterface> postUpdateCustomizer = QCodeLoader.getTableCustomizer(table, TableCustomizers.POST_UPDATE_RECORD.getRole());
if(postUpdateCustomizer.isPresent())
{
@ -209,7 +229,49 @@ public class UpdateAction
}
}
return updateOutput;
///////////////////////////////////////////////
// run all of the instance-level customizers //
///////////////////////////////////////////////
List<QCodeReference> tableCustomizerCodes = QContext.getQInstance().getTableCustomizers(TableCustomizers.POST_UPDATE_RECORD);
for(QCodeReference tableCustomizerCode : tableCustomizerCodes)
{
try
{
TableCustomizerInterface tableCustomizer = QCodeLoader.getAdHoc(TableCustomizerInterface.class, tableCustomizerCode);
updateOutput.setRecords(tableCustomizer.postUpdate(updateInput, updateOutput.getRecords(), oldRecordList));
}
catch(Exception e)
{
for(QRecord record : updateOutput.getRecords())
{
record.addWarning(new QWarningMessage("An error occurred after the update: " + e.getMessage()));
}
}
}
}
/***************************************************************************
**
***************************************************************************/
private static void runPreUpdateCustomizers(UpdateInput updateInput, QTableMetaData table, Optional<List<QRecord>> oldRecordList, boolean isPreview) throws QException
{
Optional<TableCustomizerInterface> preUpdateCustomizer = QCodeLoader.getTableCustomizer(table, TableCustomizers.PRE_UPDATE_RECORD.getRole());
if(preUpdateCustomizer.isPresent())
{
updateInput.setRecords(preUpdateCustomizer.get().preUpdate(updateInput, updateInput.getRecords(), isPreview, oldRecordList));
}
///////////////////////////////////////////////
// run all of the instance-level customizers //
///////////////////////////////////////////////
List<QCodeReference> tableCustomizerCodes = QContext.getQInstance().getTableCustomizers(TableCustomizers.PRE_UPDATE_RECORD);
for(QCodeReference tableCustomizerCode : tableCustomizerCodes)
{
TableCustomizerInterface tableCustomizer = QCodeLoader.getAdHoc(TableCustomizerInterface.class, tableCustomizerCode);
updateInput.setRecords(tableCustomizer.preUpdate(updateInput, updateInput.getRecords(), isPreview, oldRecordList));
}
}
@ -261,7 +323,7 @@ public class UpdateAction
}
else
{
ValidateRecordSecurityLockHelper.validateSecurityFields(table, updateInput.getRecords(), ValidateRecordSecurityLockHelper.Action.UPDATE);
ValidateRecordSecurityLockHelper.validateSecurityFields(table, updateInput.getRecords(), ValidateRecordSecurityLockHelper.Action.UPDATE, updateInput.getTransaction());
}
if(updateInput.getInputSource().shouldValidateRequiredFields())
@ -272,11 +334,7 @@ public class UpdateAction
///////////////////////////////////////////////////////////////////////////
// after all validations, run the pre-update customizer, if there is one //
///////////////////////////////////////////////////////////////////////////
Optional<TableCustomizerInterface> preUpdateCustomizer = QCodeLoader.getTableCustomizer(table, TableCustomizers.PRE_UPDATE_RECORD.getRole());
if(preUpdateCustomizer.isPresent())
{
updateInput.setRecords(preUpdateCustomizer.get().preUpdate(updateInput, updateInput.getRecords(), isPreview, oldRecordList));
}
runPreUpdateCustomizers(updateInput, table, oldRecordList, isPreview);
}
@ -374,7 +432,7 @@ public class UpdateAction
}
}
ValidateRecordSecurityLockHelper.validateSecurityFields(table, updateInput.getRecords(), ValidateRecordSecurityLockHelper.Action.UPDATE);
ValidateRecordSecurityLockHelper.validateSecurityFields(table, updateInput.getRecords(), ValidateRecordSecurityLockHelper.Action.UPDATE, updateInput.getTransaction());
for(QRecord record : page)
{
@ -399,7 +457,7 @@ public class UpdateAction
QFieldType fieldType = table.getField(lock.getFieldName()).getType();
Serializable lockValue = ValueUtils.getValueAsFieldType(fieldType, oldRecord.getValue(lock.getFieldName()));
List<QErrorMessage> errors = ValidateRecordSecurityLockHelper.validateRecordSecurityValue(table, lock, lockValue, fieldType, ValidateRecordSecurityLockHelper.Action.UPDATE, Collections.emptyMap());
List<QErrorMessage> errors = ValidateRecordSecurityLockHelper.validateRecordSecurityValue(table, lock, lockValue, fieldType, ValidateRecordSecurityLockHelper.Action.UPDATE, Collections.emptyMap(), QContext.getQSession());
if(CollectionUtils.nullSafeHasContents(errors))
{
errors.forEach(e -> record.addError(e));
@ -548,6 +606,7 @@ public class UpdateAction
{
LOG.debug("Deleting associatedRecords", logPair("associatedTable", associatedTable.getName()), logPair("noOfRecords", queryOutput.getRecords().size()));
DeleteInput deleteInput = new DeleteInput();
deleteInput.setFlags(updateInput.getFlags());
deleteInput.setTransaction(updateInput.getTransaction());
deleteInput.setTableName(association.getAssociatedTableName());
deleteInput.setPrimaryKeys(queryOutput.getRecords().stream().map(r -> r.getValue(associatedTable.getPrimaryKeyField())).collect(Collectors.toList()));
@ -560,6 +619,7 @@ public class UpdateAction
LOG.debug("Updating associatedRecords", logPair("associatedTable", associatedTable.getName()), logPair("noOfRecords", nextLevelUpdates.size()));
UpdateInput nextLevelUpdateInput = new UpdateInput();
nextLevelUpdateInput.setTransaction(updateInput.getTransaction());
nextLevelUpdateInput.setFlags(updateInput.getFlags());
nextLevelUpdateInput.setTableName(association.getAssociatedTableName());
nextLevelUpdateInput.setRecords(nextLevelUpdates);
UpdateOutput nextLevelUpdateOutput = new UpdateAction().execute(nextLevelUpdateInput);
@ -570,6 +630,7 @@ public class UpdateAction
LOG.debug("Inserting associatedRecords", logPair("associatedTable", associatedTable.getName()), logPair("noOfRecords", nextLevelUpdates.size()));
InsertInput nextLevelInsertInput = new InsertInput();
nextLevelInsertInput.setTransaction(updateInput.getTransaction());
nextLevelInsertInput.setFlags(updateInput.getFlags());
nextLevelInsertInput.setTableName(association.getAssociatedTableName());
nextLevelInsertInput.setRecords(nextLevelInserts);
InsertOutput nextLevelInsertOutput = new InsertAction().execute(nextLevelInsertInput);

View File

@ -41,6 +41,7 @@ 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.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.tables.QueryOrGetInputInterface;
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput;
@ -569,7 +570,7 @@ public class QueryActionCacheHelper
QQueryFilter filter = new QQueryFilter().withBooleanOperator(QQueryFilter.BooleanOperator.OR);
sourceQueryInput.setFilter(filter);
sourceQueryInput.setCommonParamsFrom(cacheQueryInput);
((QueryOrGetInputInterface) sourceQueryInput).setCommonParamsFrom(cacheQueryInput);
for(List<Serializable> uniqueKeyValue : uniqueKeyValues)
{

View File

@ -31,16 +31,12 @@ import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.instances.QMetaDataVariableInterpreter;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
@ -54,12 +50,10 @@ import com.kingsrook.qqq.backend.core.model.querystats.QueryStatCriteriaField;
import com.kingsrook.qqq.backend.core.model.querystats.QueryStatJoinTable;
import com.kingsrook.qqq.backend.core.model.querystats.QueryStatOrderByField;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.model.tables.QQQTable;
import com.kingsrook.qqq.backend.core.model.tables.QQQTablesMetaDataProvider;
import com.kingsrook.qqq.backend.core.model.tables.QQQTableTableManager;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.PrefixedDefaultThreadFactory;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
@ -371,7 +365,7 @@ public class QueryStatManager
//////////////////////
// set the table id //
//////////////////////
Integer qqqTableId = getQQQTableId(queryStat.getTableName());
Integer qqqTableId = QQQTableTableManager.getQQQTableId(getInstance().qInstance, queryStat.getTableName());
queryStat.setQqqTableId(qqqTableId);
//////////////////////////////
@ -382,7 +376,7 @@ public class QueryStatManager
List<QueryStatJoinTable> queryStatJoinTableList = new ArrayList<>();
for(String joinTableName : queryStat.getJoinTableNames())
{
queryStatJoinTableList.add(new QueryStatJoinTable().withQqqTableId(getQQQTableId(joinTableName)));
queryStatJoinTableList.add(new QueryStatJoinTable().withQqqTableId(QQQTableTableManager.getQQQTableId(getInstance().qInstance, joinTableName)));
}
queryStat.setQueryStatJoinTableList(queryStatJoinTableList);
}
@ -460,7 +454,7 @@ public class QueryStatManager
String[] parts = fieldName.split("\\.");
if(parts.length > 1)
{
queryStatCriteriaField.setQqqTableId(getQQQTableId(parts[0]));
queryStatCriteriaField.setQqqTableId(QQQTableTableManager.getQQQTableId(getInstance().qInstance, parts[0]));
queryStatCriteriaField.setName(parts[1]);
}
}
@ -498,7 +492,7 @@ public class QueryStatManager
String[] parts = fieldName.split("\\.");
if(parts.length > 1)
{
queryStatOrderByField.setQqqTableId(getQQQTableId(parts[0]));
queryStatOrderByField.setQqqTableId(QQQTableTableManager.getQQQTableId(getInstance().qInstance, parts[0]));
queryStatOrderByField.setName(parts[1]);
}
}
@ -512,44 +506,6 @@ public class QueryStatManager
}
}
}
/*******************************************************************************
**
*******************************************************************************/
private static Integer getQQQTableId(String tableName) throws QException
{
/////////////////////////////
// look in the cache table //
/////////////////////////////
GetInput getInput = new GetInput();
getInput.setTableName(QQQTablesMetaDataProvider.QQQ_TABLE_CACHE_TABLE_NAME);
getInput.setUniqueKey(MapBuilder.of("name", tableName));
GetOutput getOutput = new GetAction().execute(getInput);
////////////////////////
// upon cache miss... //
////////////////////////
if(getOutput.getRecord() == null)
{
///////////////////////////////////////////////////////
// insert the record (into the table, not the cache) //
///////////////////////////////////////////////////////
QTableMetaData tableMetaData = getInstance().qInstance.getTable(tableName);
InsertInput insertInput = new InsertInput();
insertInput.setTableName(QQQTable.TABLE_NAME);
insertInput.setRecords(List.of(new QRecord().withValue("name", tableName).withValue("label", tableMetaData.getLabel())));
InsertOutput insertOutput = new InsertAction().execute(insertInput);
///////////////////////////////////
// repeat the get from the cache //
///////////////////////////////////
getOutput = new GetAction().execute(getInput);
}
return getOutput.getRecord().getValueInteger("id");
}
}

View File

@ -50,6 +50,7 @@ import com.kingsrook.qqq.backend.core.utils.ValueUtils;
*******************************************************************************/
public class UniqueKeyHelper
{
private static Integer pageSize = 1000;
/*******************************************************************************
**
@ -60,62 +61,71 @@ public class UniqueKeyHelper
Map<List<Serializable>, Serializable> existingRecords = new HashMap<>();
if(ukFieldNames != null)
{
QueryInput queryInput = new QueryInput();
queryInput.setTableName(table.getName());
queryInput.setTransaction(transaction);
for(List<QRecord> page : CollectionUtils.getPages(recordList, pageSize))
{
QueryInput queryInput = new QueryInput();
queryInput.setTableName(table.getName());
queryInput.setTransaction(transaction);
QQueryFilter filter = new QQueryFilter();
if(ukFieldNames.size() == 1)
{
List<Serializable> values = recordList.stream()
.filter(r -> CollectionUtils.nullSafeIsEmpty(r.getErrors()))
.map(r -> r.getValue(ukFieldNames.get(0)))
.collect(Collectors.toList());
filter.addCriteria(new QFilterCriteria(ukFieldNames.get(0), QCriteriaOperator.IN, values));
}
else
{
filter.setBooleanOperator(QQueryFilter.BooleanOperator.OR);
for(QRecord record : recordList)
QQueryFilter filter = new QQueryFilter();
if(ukFieldNames.size() == 1)
{
if(CollectionUtils.nullSafeHasContents(record.getErrors()))
List<Serializable> values = page.stream()
.filter(r -> CollectionUtils.nullSafeIsEmpty(r.getErrors()))
.map(r -> r.getValue(ukFieldNames.get(0)))
.collect(Collectors.toList());
if(values.isEmpty())
{
continue;
}
QQueryFilter subFilter = new QQueryFilter();
filter.addSubFilter(subFilter);
for(String fieldName : ukFieldNames)
filter.addCriteria(new QFilterCriteria(ukFieldNames.get(0), QCriteriaOperator.IN, values));
}
else
{
filter.setBooleanOperator(QQueryFilter.BooleanOperator.OR);
for(QRecord record : page)
{
Serializable value = record.getValue(fieldName);
if(value == null)
if(CollectionUtils.nullSafeHasContents(record.getErrors()))
{
subFilter.addCriteria(new QFilterCriteria(fieldName, QCriteriaOperator.IS_BLANK));
continue;
}
else
QQueryFilter subFilter = new QQueryFilter();
filter.addSubFilter(subFilter);
for(String fieldName : ukFieldNames)
{
subFilter.addCriteria(new QFilterCriteria(fieldName, QCriteriaOperator.EQUALS, value));
Serializable value = record.getValue(fieldName);
if(value == null)
{
subFilter.addCriteria(new QFilterCriteria(fieldName, QCriteriaOperator.IS_BLANK));
}
else
{
subFilter.addCriteria(new QFilterCriteria(fieldName, QCriteriaOperator.EQUALS, value));
}
}
}
if(CollectionUtils.nullSafeIsEmpty(filter.getSubFilters()))
{
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if we didn't build any sub-filters (because all records have errors in them), don't run a query w/ no clauses - continue to next page //
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
continue;
}
}
if(CollectionUtils.nullSafeIsEmpty(filter.getSubFilters()))
queryInput.setFilter(filter);
QueryOutput queryOutput = new QueryAction().execute(queryInput);
for(QRecord record : queryOutput.getRecords())
{
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if we didn't build any sub-filters (because all records have errors in them), don't run a query w/ no clauses - rather - return early. //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
return (existingRecords);
}
}
queryInput.setFilter(filter);
QueryOutput queryOutput = new QueryAction().execute(queryInput);
for(QRecord record : queryOutput.getRecords())
{
Optional<List<Serializable>> keyValues = getKeyValues(table, uniqueKey, record, allowNullKeyValuesToEqual);
if(keyValues.isPresent())
{
existingRecords.put(keyValues.get(), record.getValue(table.getPrimaryKeyField()));
Optional<List<Serializable>> keyValues = getKeyValues(table, uniqueKey, record, allowNullKeyValuesToEqual);
if(keyValues.isPresent())
{
existingRecords.put(keyValues.get(), record.getValue(table.getPrimaryKeyField()));
}
}
}
}
@ -200,4 +210,26 @@ public class UniqueKeyHelper
}
}
/*******************************************************************************
** Getter for pageSize
**
*******************************************************************************/
public static Integer getPageSize()
{
return pageSize;
}
/*******************************************************************************
** Setter for pageSize
**
*******************************************************************************/
public static void setPageSize(Integer pageSize)
{
UniqueKeyHelper.pageSize = pageSize;
}
}

View File

@ -28,6 +28,7 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
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;
@ -49,6 +50,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.security.QSecurityKeyType;
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLockFilters;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.model.statusmessages.PermissionDeniedMessage;
import com.kingsrook.qqq.backend.core.model.statusmessages.QErrorMessage;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
@ -83,7 +85,7 @@ public class ValidateRecordSecurityLockHelper
/*******************************************************************************
**
*******************************************************************************/
public static void validateSecurityFields(QTableMetaData table, List<QRecord> records, Action action) throws QException
public static void validateSecurityFields(QTableMetaData table, List<QRecord> records, Action action, QBackendTransaction transaction) throws QException
{
MultiRecordSecurityLock locksToCheck = getRecordSecurityLocks(table, action);
if(locksToCheck == null || CollectionUtils.nullSafeIsEmpty(locksToCheck.getLocks()))
@ -101,7 +103,7 @@ public class ValidateRecordSecurityLockHelper
// actually check lock values //
////////////////////////////////
Map<Serializable, RecordWithErrors> errorRecords = new HashMap<>();
evaluateRecordLocks(table, records, action, locksToCheck, errorRecords, new ArrayList<>(), madeUpPrimaryKeys);
evaluateRecordLocks(table, records, action, locksToCheck, errorRecords, new ArrayList<>(), madeUpPrimaryKeys, transaction, QContext.getQSession());
/////////////////////////////////
// propagate errors to records //
@ -123,6 +125,29 @@ public class ValidateRecordSecurityLockHelper
/***************************************************************************
** return boolean if given session can read given record
***************************************************************************/
public static boolean allowedToReadRecord(QTableMetaData table, QRecord record, QSession qSession, QBackendTransaction transaction) throws QException
{
MultiRecordSecurityLock locksToCheck = getRecordSecurityLocks(table, Action.SELECT);
if(locksToCheck == null || CollectionUtils.nullSafeIsEmpty(locksToCheck.getLocks()))
{
return (true);
}
Map<Serializable, RecordWithErrors> errorRecords = new HashMap<>();
evaluateRecordLocks(table, List.of(record), Action.SELECT, locksToCheck, errorRecords, new ArrayList<>(), Collections.emptyMap(), transaction, qSession);
if(errorRecords.containsKey(record.getValue(table.getPrimaryKeyField())))
{
return (false);
}
return (true);
}
/*******************************************************************************
** For a list of `records` from a `table`, and a given `action`, evaluate a
** `recordSecurityLock` (which may be a multi-lock) - populating the input map
@ -141,7 +166,7 @@ public class ValidateRecordSecurityLockHelper
** BUT - WRITE locks - in their case, we read the record no matter what, and in
** here we need to verify we have a key that allows us to WRITE the record.
*******************************************************************************/
private static void evaluateRecordLocks(QTableMetaData table, List<QRecord> records, Action action, RecordSecurityLock recordSecurityLock, Map<Serializable, RecordWithErrors> errorRecords, List<Integer> treePosition, Map<Serializable, QRecord> madeUpPrimaryKeys) throws QException
private static void evaluateRecordLocks(QTableMetaData table, List<QRecord> records, Action action, RecordSecurityLock recordSecurityLock, Map<Serializable, RecordWithErrors> errorRecords, List<Integer> treePosition, Map<Serializable, QRecord> madeUpPrimaryKeys, QBackendTransaction transaction, QSession qSession) throws QException
{
if(recordSecurityLock instanceof MultiRecordSecurityLock multiRecordSecurityLock)
{
@ -152,7 +177,7 @@ public class ValidateRecordSecurityLockHelper
for(RecordSecurityLock childLock : CollectionUtils.nonNullList(multiRecordSecurityLock.getLocks()))
{
treePosition.add(i);
evaluateRecordLocks(table, records, action, childLock, errorRecords, treePosition, madeUpPrimaryKeys);
evaluateRecordLocks(table, records, action, childLock, errorRecords, treePosition, madeUpPrimaryKeys, transaction, qSession);
treePosition.remove(treePosition.size() - 1);
i++;
}
@ -164,7 +189,7 @@ public class ValidateRecordSecurityLockHelper
// if this lock has an all-access key, and the user has that key, then there can't be any errors here, so return early //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
QSecurityKeyType securityKeyType = QContext.getQInstance().getSecurityKeyType(recordSecurityLock.getSecurityKeyType());
if(StringUtils.hasContent(securityKeyType.getAllAccessKeyName()) && QContext.getQSession().hasSecurityKeyValue(securityKeyType.getAllAccessKeyName(), true, QFieldType.BOOLEAN))
if(StringUtils.hasContent(securityKeyType.getAllAccessKeyName()) && qSession.hasSecurityKeyValue(securityKeyType.getAllAccessKeyName(), true, QFieldType.BOOLEAN))
{
return;
}
@ -192,7 +217,7 @@ public class ValidateRecordSecurityLockHelper
}
Serializable recordSecurityValue = record.getValue(field.getName());
List<QErrorMessage> recordErrors = validateRecordSecurityValue(table, recordSecurityLock, recordSecurityValue, field.getType(), action, madeUpPrimaryKeys);
List<QErrorMessage> recordErrors = validateRecordSecurityValue(table, recordSecurityLock, recordSecurityValue, field.getType(), action, madeUpPrimaryKeys, qSession);
if(CollectionUtils.nullSafeHasContents(recordErrors))
{
errorRecords.computeIfAbsent(record.getValue(primaryKeyField), (k) -> new RecordWithErrors(record)).addAll(recordErrors, treePosition);
@ -225,6 +250,7 @@ public class ValidateRecordSecurityLockHelper
// query will be like (fkey1=? and fkey2=?) OR (fkey1=? and fkey2=?) OR (fkey1=? and fkey2=?) //
////////////////////////////////////////////////////////////////////////////////////////////////
QueryInput queryInput = new QueryInput();
queryInput.setTransaction(transaction);
queryInput.setTableName(leftMostJoin.getLeftTable());
QQueryFilter filter = new QQueryFilter().withBooleanOperator(QQueryFilter.BooleanOperator.OR);
queryInput.setFilter(filter);
@ -337,7 +363,7 @@ public class ValidateRecordSecurityLockHelper
for(QRecord inputRecord : inputRecords)
{
List<QErrorMessage> recordErrors = validateRecordSecurityValue(table, recordSecurityLock, recordSecurityValue, field.getType(), action, madeUpPrimaryKeys);
List<QErrorMessage> recordErrors = validateRecordSecurityValue(table, recordSecurityLock, recordSecurityValue, field.getType(), action, madeUpPrimaryKeys, qSession);
if(CollectionUtils.nullSafeHasContents(recordErrors))
{
errorRecords.computeIfAbsent(inputRecord.getValue(primaryKeyField), (k) -> new RecordWithErrors(inputRecord)).addAll(recordErrors, treePosition);
@ -444,7 +470,7 @@ public class ValidateRecordSecurityLockHelper
/*******************************************************************************
**
*******************************************************************************/
public static List<QErrorMessage> validateRecordSecurityValue(QTableMetaData table, RecordSecurityLock recordSecurityLock, Serializable recordSecurityValue, QFieldType fieldType, Action action, Map<Serializable, QRecord> madeUpPrimaryKeys)
public static List<QErrorMessage> validateRecordSecurityValue(QTableMetaData table, RecordSecurityLock recordSecurityLock, Serializable recordSecurityValue, QFieldType fieldType, Action action, Map<Serializable, QRecord> madeUpPrimaryKeys, QSession qSession)
{
if(recordSecurityValue == null || (madeUpPrimaryKeys != null && madeUpPrimaryKeys.containsKey(recordSecurityValue)))
{
@ -459,7 +485,7 @@ public class ValidateRecordSecurityLockHelper
}
else
{
if(!QContext.getQSession().hasSecurityKeyValue(recordSecurityLock.getSecurityKeyType(), recordSecurityValue, fieldType))
if(!qSession.hasSecurityKeyValue(recordSecurityLock.getSecurityKeyType(), recordSecurityValue, fieldType))
{
if(CollectionUtils.nullSafeHasContents(recordSecurityLock.getJoinNameChain()))
{

View File

@ -26,22 +26,32 @@ import java.nio.file.Path;
import java.util.Map;
import com.kingsrook.qqq.backend.core.actions.AbstractQActionFunction;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.templates.ConvertHtmlToPdfInput;
import com.kingsrook.qqq.backend.core.model.actions.templates.ConvertHtmlToPdfOutput;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.openhtmltopdf.css.constants.IdentValue;
import com.openhtmltopdf.pdfboxout.PdfBoxFontResolver;
import com.openhtmltopdf.pdfboxout.PdfBoxRenderer;
import com.openhtmltopdf.pdfboxout.PdfRendererBuilder;
import org.jsoup.Jsoup;
import org.jsoup.helper.W3CDom;
import org.jsoup.nodes.Document;
import org.xhtmlrenderer.layout.SharedContext;
import org.xhtmlrenderer.pdf.ITextRenderer;
/*******************************************************************************
** Action to convert a string of HTML to a PDF!
**
** Much credit to https://www.baeldung.com/java-html-to-pdf
*******************************************************************************/
**
** Updated in March 2025 to go from flying-saucer-pdf-openpdf lib to openhtmltopdf,
** mostly to get support for max-height on images...
********************************************************************************/
public class ConvertHtmlToPdfAction extends AbstractQActionFunction<ConvertHtmlToPdfInput, ConvertHtmlToPdfOutput>
{
private static final QLogger LOG = QLogger.getLogger(ConvertHtmlToPdfAction.class);
/*******************************************************************************
**
@ -58,35 +68,37 @@ public class ConvertHtmlToPdfAction extends AbstractQActionFunction<ConvertHtmlT
//////////////////////////////////////////////////////////////////
Document document = Jsoup.parse(input.getHtml());
document.outputSettings().syntax(Document.OutputSettings.Syntax.xml);
org.w3c.dom.Document w3cDoc = new W3CDom().fromJsoup(document);
//////////////////////////////
// convert the XHTML to PDF //
//////////////////////////////
ITextRenderer renderer = new ITextRenderer();
SharedContext sharedContext = renderer.getSharedContext();
sharedContext.setPrint(true);
sharedContext.setInteractive(false);
PdfRendererBuilder builder = new PdfRendererBuilder();
builder.toStream(input.getOutputStream());
builder.useFastMode();
builder.withW3cDocument(w3cDoc, input.getBasePath() == null ? "./" : input.getBasePath().toUri().toString());
if(input.getBasePath() != null)
try(PdfBoxRenderer pdfBoxRenderer = builder.buildPdfRenderer())
{
String baseUrl = input.getBasePath().toUri().toURL().toString();
renderer.setDocumentFromString(document.html(), baseUrl);
}
else
{
renderer.setDocumentFromString(document.html());
}
pdfBoxRenderer.layout();
pdfBoxRenderer.getSharedContext().setPrint(true);
pdfBoxRenderer.getSharedContext().setInteractive(false);
//////////////////////////////////////////////////
// register any custom fonts the input supplied //
//////////////////////////////////////////////////
for(Map.Entry<String, Path> entry : CollectionUtils.nonNullMap(input.getCustomFonts()).entrySet())
{
renderer.getFontResolver().addFont(entry.getValue().toAbsolutePath().toString(), entry.getKey(), "UTF-8", true, null);
}
for(Map.Entry<String, Path> entry : CollectionUtils.nonNullMap(input.getCustomFonts()).entrySet())
{
LOG.warn("Note: Custom fonts appear to not be working in this class at this time...");
pdfBoxRenderer.getFontResolver().addFont(
entry.getValue().toAbsolutePath().toFile(), // Path to the TrueType font file
entry.getKey(), // Font family name to use in CSS
400, // Font weight (e.g., 400 for normal, 700 for bold)
IdentValue.NORMAL, // Font style (e.g., NORMAL, ITALIC)
true, // Whether to subset the font
PdfBoxFontResolver.FontGroup.MAIN // ??
);
}
renderer.layout();
renderer.createPDF(input.getOutputStream());
pdfBoxRenderer.createPDF();
}
return (output);
}

View File

@ -104,10 +104,21 @@ public class RenderTemplateAction extends AbstractQActionFunction<RenderTemplate
/*******************************************************************************
** Static wrapper to render a Velocity template.
*******************************************************************************/
@Deprecated(since = "Call the version that doesn't take an ActionInput")
public static String renderVelocity(AbstractActionInput parentActionInput, Map<String, Object> context, String code) throws QException
{
return (renderVelocity(context, code));
}
/*******************************************************************************
** Most convenient static wrapper to render a Velocity template.
*******************************************************************************/
public static String renderVelocity(AbstractActionInput parentActionInput, Map<String, Object> context, String code) throws QException
public static String renderVelocity(Map<String, Object> context, String code) throws QException
{
return (render(TemplateType.VELOCITY, context, code));
}

View File

@ -0,0 +1,91 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. 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.values;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.values.SearchPossibleValueSourceInput;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValue;
/*******************************************************************************
** Basic implementation of a possible value provider, for where there's a limited
** set of possible source objects - so you just have to define how to make one
** PV from a source object, how to list all of the source objects, and how to
** look up a PV from an id.
*******************************************************************************/
public abstract class BasicCustomPossibleValueProvider<S, ID extends Serializable> implements QCustomPossibleValueProvider<ID>
{
/***************************************************************************
**
***************************************************************************/
protected abstract QPossibleValue<ID> makePossibleValue(S sourceObject);
/***************************************************************************
**
***************************************************************************/
protected abstract S getSourceObject(Serializable id) throws QException;
/***************************************************************************
**
***************************************************************************/
protected abstract List<S> getAllSourceObjects() throws QException;
/***************************************************************************
**
***************************************************************************/
@Override
public QPossibleValue<ID> getPossibleValue(Serializable idValue) throws QException
{
S sourceObject = getSourceObject(idValue);
if(sourceObject == null)
{
return (null);
}
return makePossibleValue(sourceObject);
}
/***************************************************************************
**
***************************************************************************/
@Override
public List<QPossibleValue<ID>> search(SearchPossibleValueSourceInput input) throws QException
{
List<QPossibleValue<ID>> allPossibleValues = new ArrayList<>();
List<S> allSourceObjects = getAllSourceObjects();
for(S sourceObject : allSourceObjects)
{
allPossibleValues.add(makePossibleValue(sourceObject));
}
return completeCustomPVSSearch(input, allPossibleValues);
}
}

View File

@ -24,6 +24,7 @@ package com.kingsrook.qqq.backend.core.actions.values;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.values.SearchPossibleValueSourceInput;
@ -44,7 +45,7 @@ public interface QCustomPossibleValueProvider<T extends Serializable>
/*******************************************************************************
**
*******************************************************************************/
QPossibleValue<T> getPossibleValue(Serializable idValue);
QPossibleValue<T> getPossibleValue(Serializable idValue) throws QException;
/*******************************************************************************
**
@ -74,6 +75,31 @@ public interface QCustomPossibleValueProvider<T extends Serializable>
}
/***************************************************************************
** meant to be protected (but interface...) - for a custom PVS implementation
** to complete its search (e.g., after it generates the list of PVS objects,
** let this method do the filtering).
***************************************************************************/
default List<QPossibleValue<T>> completeCustomPVSSearch(SearchPossibleValueSourceInput input, List<QPossibleValue<T>> possibleValues)
{
SearchPossibleValueSourceAction.PreparedSearchPossibleValueSourceInput preparedInput = SearchPossibleValueSourceAction.prepareSearchPossibleValueSourceInput(input);
List<QPossibleValue<T>> rs = new ArrayList<>();
for(QPossibleValue<T> possibleValue : possibleValues)
{
if(possibleValue != null && SearchPossibleValueSourceAction.doesPossibleValueMatchSearchInput(possibleValue, preparedInput))
{
rs.add(possibleValue);
}
}
rs.sort(Comparator.nullsLast(Comparator.comparing((QPossibleValue<T> pv) -> pv.getLabel())));
return (rs);
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -341,7 +341,7 @@ public class QPossibleValueTranslator
try
{
QCustomPossibleValueProvider customPossibleValueProvider = QCodeLoader.getCustomPossibleValueProvider(possibleValueSource);
QCustomPossibleValueProvider<?> customPossibleValueProvider = QCodeLoader.getAdHoc(QCustomPossibleValueProvider.class, possibleValueSource.getCustomCodeReference());
return (formatPossibleValue(possibleValueSource, customPossibleValueProvider.getPossibleValue(value)));
}
catch(Exception e)

View File

@ -28,6 +28,7 @@ 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;
@ -44,6 +45,7 @@ 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.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import org.apache.commons.lang3.BooleanUtils;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
@ -308,6 +310,19 @@ public class QValueFormatter
/*******************************************************************************
** For a list of records, set their recordLabels and display values - including
** record label (e.g., from the table meta data).
*******************************************************************************/
public static void setDisplayValuesInRecordsIncludingPossibleValueTranslations(QTableMetaData table, List<QRecord> records)
{
QPossibleValueTranslator possibleValueTranslator = new QPossibleValueTranslator(QContext.getQInstance(), QContext.getQSession());
possibleValueTranslator.translatePossibleValuesInRecords(table, records);
setDisplayValuesInRecords(table, records);
}
/*******************************************************************************
** For a list of records, set their recordLabels and display values - including
** record label (e.g., from the table meta data).
@ -484,6 +499,8 @@ public class QValueFormatter
String fileNameFormat = ValueUtils.getValueAsString(adornmentValues.get(AdornmentType.FileDownloadValues.FILE_NAME_FORMAT));
String defaultExtension = ValueUtils.getValueAsString(adornmentValues.get(AdornmentType.FileDownloadValues.DEFAULT_EXTENSION));
Boolean downloadUrlDynamic = ValueUtils.getValueAsBoolean(adornmentValues.get(AdornmentType.FileDownloadValues.DOWNLOAD_URL_DYNAMIC));
for(QRecord record : records)
{
if(!doesFieldHaveValue(field, record))
@ -491,6 +508,11 @@ public class QValueFormatter
continue;
}
if(BooleanUtils.isTrue(downloadUrlDynamic))
{
continue;
}
Serializable primaryKey = record.getValue(table.getPrimaryKeyField());
String fileName = null;
@ -508,7 +530,7 @@ public class QValueFormatter
{
@SuppressWarnings("unchecked") // instance validation should make this safe!
List<String> fileNameFormatFields = (List<String>) adornmentValues.get(AdornmentType.FileDownloadValues.FILE_NAME_FORMAT_FIELDS);
List<String> values = fileNameFormatFields.stream().map(f -> ValueUtils.getValueAsString(record.getValue(f))).toList();
List<String> values = CollectionUtils.nullSafeHasContents(fileNameFormatFields) ? fileNameFormatFields.stream().map(f -> ValueUtils.getValueAsString(record.getValue(f))).toList() : Collections.emptyList();
fileName = QValueFormatter.formatStringWithValues(fileNameFormat, values);
}
}
@ -529,12 +551,18 @@ public class QValueFormatter
}
}
/////////////////////////////////////////////
// if field type is blob, update its value //
/////////////////////////////////////////////
if(QFieldType.BLOB.equals(field.getType()))
////////////////////////////////////////////////////////////////////////////////////////////////
// if field type is blob OR if there's a supplemental process or code-ref that needs to run - //
// then update its value to be a callback-url that'll give access to the bytes to download. //
// implied here is that a String value (w/o supplemental code/proc) has its value stay as a //
// URL, which is where the file is directly downloaded from. And in the case of a String //
// with code-to-run, then the code should run, followed by a redirect to the value URL. //
////////////////////////////////////////////////////////////////////////////////////////////////
if(QFieldType.BLOB.equals(field.getType())
|| adornmentValues.containsKey(AdornmentType.FileDownloadValues.SUPPLEMENTAL_CODE_REFERENCE)
|| adornmentValues.containsKey(AdornmentType.FileDownloadValues.SUPPLEMENTAL_PROCESS_NAME))
{
record.setValue(field.getName(), "/data/" + table.getName() + "/" + primaryKey + "/" + field.getName() + "/" + fileName);
record.setValue(field.getName(), AdornmentType.FileDownloadValues.makeFieldDownloadUrl(table.getName(), primaryKey, field.getName(), fileName));
}
record.setDisplayValue(field.getName(), fileName);
}

View File

@ -26,8 +26,13 @@ import java.io.Serializable;
import java.time.Instant;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
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;
@ -47,10 +52,9 @@ import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleVal
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.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import org.apache.commons.lang.NotImplementedException;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
@ -61,6 +65,9 @@ public class SearchPossibleValueSourceAction
{
private static final QLogger LOG = QLogger.getLogger(SearchPossibleValueSourceAction.class);
private static final Set<String> warnedAboutUnexpectedValueField = Collections.synchronizedSet(new HashSet<>());
private static final Set<String> warnedAboutUnexpectedNoOfFieldsToSearchByLabel = Collections.synchronizedSet(new HashSet<>());
private QPossibleValueTranslator possibleValueTranslator;
@ -101,47 +108,54 @@ public class SearchPossibleValueSourceAction
/***************************************************************************
** record to store "computed" values as part of a possible-value search -
** e.g., ids type-convered, and lower-cased labels.
***************************************************************************/
public record PreparedSearchPossibleValueSourceInput(Collection<?> inputIdsAsCorrectType, Collection<String> lowerCaseLabels, String searchTerm) {}
/***************************************************************************
**
***************************************************************************/
public static PreparedSearchPossibleValueSourceInput prepareSearchPossibleValueSourceInput(SearchPossibleValueSourceInput input)
{
QPossibleValueSource possibleValueSource = QContext.getQInstance().getPossibleValueSource(input.getPossibleValueSourceName());
List<?> inputIdsAsCorrectType = convertInputIdsToPossibleValueSourceIdType(possibleValueSource, input.getIdList());
Set<String> lowerCaseLabels = null;
if(input.getLabelList() != null)
{
lowerCaseLabels = input.getLabelList().stream()
.filter(Objects::nonNull)
.map(l -> l.toLowerCase())
.collect(Collectors.toSet());
}
return (new PreparedSearchPossibleValueSourceInput(inputIdsAsCorrectType, lowerCaseLabels, input.getSearchTerm()));
}
/*******************************************************************************
**
*******************************************************************************/
private SearchPossibleValueSourceOutput searchPossibleValueEnum(SearchPossibleValueSourceInput input, QPossibleValueSource possibleValueSource)
{
PreparedSearchPossibleValueSourceInput preparedSearchPossibleValueSourceInput = prepareSearchPossibleValueSourceInput(input);
SearchPossibleValueSourceOutput output = new SearchPossibleValueSourceOutput();
List<Serializable> matchingIds = new ArrayList<>();
List<?> inputIdsAsCorrectType = convertInputIdsToEnumIdType(possibleValueSource, input.getIdList());
for(QPossibleValue<?> possibleValue : possibleValueSource.getEnumValues())
{
boolean match = false;
if(input.getIdList() != null)
{
if(inputIdsAsCorrectType.contains(possibleValue.getId()))
{
match = true;
}
}
else
{
if(StringUtils.hasContent(input.getSearchTerm()))
{
match = (Objects.equals(ValueUtils.getValueAsString(possibleValue.getId()).toLowerCase(), input.getSearchTerm().toLowerCase())
|| possibleValue.getLabel().toLowerCase().startsWith(input.getSearchTerm().toLowerCase()));
}
else
{
match = true;
}
}
boolean match = doesPossibleValueMatchSearchInput(possibleValue, preparedSearchPossibleValueSourceInput);
if(match)
{
matchingIds.add((Serializable) possibleValue.getId());
matchingIds.add(possibleValue.getId());
}
// todo - skip & limit?
// todo - default filter
}
List<QPossibleValue<?>> qPossibleValues = possibleValueTranslator.buildTranslatedPossibleValueList(possibleValueSource, matchingIds);
@ -152,37 +166,95 @@ public class SearchPossibleValueSourceAction
/***************************************************************************
**
***************************************************************************/
public static boolean doesPossibleValueMatchSearchInput(QPossibleValue<?> possibleValue, PreparedSearchPossibleValueSourceInput input)
{
boolean match = false;
if(input.inputIdsAsCorrectType() != null)
{
if(input.inputIdsAsCorrectType().contains(possibleValue.getId()))
{
match = true;
}
}
else if(input.lowerCaseLabels() != null)
{
if(input.lowerCaseLabels().contains(possibleValue.getLabel().toLowerCase()))
{
match = true;
}
}
else
{
if(StringUtils.hasContent(input.searchTerm()))
{
match = (Objects.equals(ValueUtils.getValueAsString(possibleValue.getId()).toLowerCase(), input.searchTerm().toLowerCase())
|| possibleValue.getLabel().toLowerCase().startsWith(input.searchTerm().toLowerCase()));
}
else
{
match = true;
}
}
return match;
}
/*******************************************************************************
** The input list of ids might come through as a type that isn't the same as
** the type of the ids in the enum (e.g., strings from a frontend, integers
** in an enum). So, this method looks at the first id in the enum, and then
** maps all the inputIds to be of the same type.
** in an enum). So, this method type-converts them.
*******************************************************************************/
private List<Object> convertInputIdsToEnumIdType(QPossibleValueSource possibleValueSource, List<Serializable> inputIdList)
private static List<Object> convertInputIdsToPossibleValueSourceIdType(QPossibleValueSource possibleValueSource, List<Serializable> inputIdList)
{
List<Object> rs = new ArrayList<>();
if(CollectionUtils.nullSafeIsEmpty(inputIdList))
if(inputIdList == null)
{
return (null);
}
else if(inputIdList.isEmpty())
{
return (rs);
}
Object anIdFromTheEnum = possibleValueSource.getEnumValues().get(0).getId();
QFieldType type = possibleValueSource.getIdType();
if(anIdFromTheEnum instanceof Integer)
for(Serializable inputId : inputIdList)
{
inputIdList.forEach(id -> rs.add(ValueUtils.getValueAsInteger(id)));
}
else if(anIdFromTheEnum instanceof String)
{
inputIdList.forEach(id -> rs.add(ValueUtils.getValueAsString(id)));
}
else if(anIdFromTheEnum instanceof Boolean)
{
inputIdList.forEach(id -> rs.add(ValueUtils.getValueAsBoolean(id)));
}
else
{
LOG.warn("Unexpected type [" + anIdFromTheEnum.getClass().getSimpleName() + "] for ids in enum: " + possibleValueSource.getName());
Object properlyTypedId = null;
try
{
if(type.equals(QFieldType.INTEGER))
{
properlyTypedId = ValueUtils.getValueAsInteger(inputId);
}
else if(type.isStringLike())
{
properlyTypedId = ValueUtils.getValueAsString(inputId);
}
else if(type.equals(QFieldType.BOOLEAN))
{
properlyTypedId = ValueUtils.getValueAsBoolean(inputId);
}
else
{
LOG.warn("Unexpected type [" + type + "] for ids in enum: " + possibleValueSource.getName());
}
}
catch(Exception e)
{
LOG.debug("Error converting possible value id to expected id type", e, logPair("value", inputId));
}
if(properlyTypedId != null)
{
rs.add(properlyTypedId);
}
}
return (rs);
@ -209,6 +281,53 @@ public class SearchPossibleValueSourceAction
{
queryFilter.addCriteria(new QFilterCriteria(table.getPrimaryKeyField(), QCriteriaOperator.IN, input.getIdList()));
}
else if(input.getLabelList() != null)
{
List<String> fieldNames = new ArrayList<>();
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// the 'value fields' will either be 'id' or 'label' (which means, use the fields from the tableMetaData's label fields) //
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
for(String valueField : possibleValueSource.getValueFields())
{
if("id".equals(valueField))
{
fieldNames.add(table.getPrimaryKeyField());
}
else if("label".equals(valueField))
{
if(table.getRecordLabelFields() != null)
{
fieldNames.addAll(table.getRecordLabelFields());
}
}
else
{
String message = "Unexpected valueField defined in possibleValueSource when searching possibleValueSource by label (required: 'id' or 'label')";
if(!warnedAboutUnexpectedValueField.contains(possibleValueSource.getName()))
{
LOG.warn(message, logPair("valueField", valueField), logPair("possibleValueSource", possibleValueSource.getName()));
warnedAboutUnexpectedValueField.add(possibleValueSource.getName());
}
output.setWarning(message);
}
}
if(fieldNames.size() == 1)
{
queryFilter.addCriteria(new QFilterCriteria(fieldNames.get(0), QCriteriaOperator.IN, input.getLabelList()));
}
else
{
String message = "Unexpected number of fields found for searching possibleValueSource by label (required: 1, found: " + fieldNames.size() + ")";
if(!warnedAboutUnexpectedNoOfFieldsToSearchByLabel.contains(possibleValueSource.getName()))
{
LOG.warn(message);
warnedAboutUnexpectedNoOfFieldsToSearchByLabel.add(possibleValueSource.getName());
}
output.setWarning(message);
}
}
else
{
String searchTerm = input.getSearchTerm();
@ -269,8 +388,8 @@ public class SearchPossibleValueSourceAction
queryFilter = input.getDefaultQueryFilter();
}
// todo - skip & limit as params
queryFilter.setLimit(250);
queryFilter.setLimit(input.getLimit());
queryFilter.setSkip(input.getSkip());
queryFilter.setOrderBys(possibleValueSource.getOrderByFields());
@ -288,7 +407,7 @@ public class SearchPossibleValueSourceAction
fieldName = table.getPrimaryKeyField();
}
List<Serializable> ids = queryOutput.getRecords().stream().map(r -> r.getValue(fieldName)).toList();
List<Serializable> ids = queryOutput.getRecords().stream().map(r -> r.getValue(fieldName)).toList();
List<QPossibleValue<?>> qPossibleValues = possibleValueTranslator.buildTranslatedPossibleValueList(possibleValueSource, ids);
output.setResults(qPossibleValues);
@ -301,11 +420,11 @@ public class SearchPossibleValueSourceAction
**
*******************************************************************************/
@SuppressWarnings({ "rawtypes", "unchecked" })
private SearchPossibleValueSourceOutput searchPossibleValueCustom(SearchPossibleValueSourceInput input, QPossibleValueSource possibleValueSource)
private SearchPossibleValueSourceOutput searchPossibleValueCustom(SearchPossibleValueSourceInput input, QPossibleValueSource possibleValueSource) throws QException
{
try
{
QCustomPossibleValueProvider customPossibleValueProvider = QCodeLoader.getCustomPossibleValueProvider(possibleValueSource);
QCustomPossibleValueProvider customPossibleValueProvider = QCodeLoader.getAdHoc(QCustomPossibleValueProvider.class, possibleValueSource.getCustomCodeReference());
List<QPossibleValue<?>> possibleValues = customPossibleValueProvider.search(input);
SearchPossibleValueSourceOutput output = new SearchPossibleValueSourceOutput();
@ -314,11 +433,10 @@ public class SearchPossibleValueSourceAction
}
catch(Exception e)
{
// LOG.warn("Error sending [" + value + "] for field [" + field + "] through custom code for PVS [" + field.getPossibleValueSourceName() + "]", e);
String message = "Error searching custom possible value source [" + input.getPossibleValueSourceName() + "]";
LOG.warn(message, e);
throw (new QException(message, e));
}
throw new NotImplementedException("Not impleemnted");
// return (null);
}
}

View File

@ -29,7 +29,7 @@ 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.
** by MetaDataProducers in (or under) a single package.
*******************************************************************************/
public abstract class AbstractMetaDataProducerBasedQQQApplication extends AbstractQQQApplication
{

View File

@ -0,0 +1,61 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. 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.instances.loaders.MetaDataLoaderHelper;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
/*******************************************************************************
** Version of AbstractQQQApplication that assumes all meta-data is defined in
** config files (yaml, json, etc) under a given directory path.
*******************************************************************************/
public class ConfigFilesBasedQQQApplication extends AbstractQQQApplication
{
private final String path;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public ConfigFilesBasedQQQApplication(String path)
{
this.path = path;
}
/***************************************************************************
**
***************************************************************************/
@Override
public QInstance defineQInstance() throws QException
{
QInstance qInstance = new QInstance();
MetaDataLoaderHelper.processAllMetaDataFilesInDirectory(qInstance, path);
return (qInstance);
}
}

View File

@ -0,0 +1,67 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. 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;
/*******************************************************************************
** Version of AbstractQQQApplication that assumes all meta-data is produced
** by MetaDataProducers in (or under) a single package (where you can pass that
** package into the constructor, vs. the abstract base class, where you extend
** it and override the getMetaDataPackageName method.
*******************************************************************************/
public class MetaDataProducerBasedQQQApplication extends AbstractMetaDataProducerBasedQQQApplication
{
private final String metaDataPackageName;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public MetaDataProducerBasedQQQApplication(String metaDataPackageName)
{
this.metaDataPackageName = metaDataPackageName;
}
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public MetaDataProducerBasedQQQApplication(Class<?> aClassInMetaDataPackage)
{
this(aClassInMetaDataPackage.getPackageName());
}
/***************************************************************************
**
***************************************************************************/
@Override
public String getMetaDataPackageName()
{
return (this.metaDataPackageName);
}
}

View File

@ -0,0 +1,46 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. 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.Map;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.help.QHelpContent;
/*******************************************************************************
* interface that can be added to a QSupplementalInstanceMetaData, to receive
* QHelpContent records during instance boot or upon updates in the help content
* table.
*******************************************************************************/
public interface QHelpContentPlugin
{
/***************************************************************************
* accept a single helpContent record, and apply its data to some data in the
* qInstance
*
* @param qInstance the active qInstance, that the content should be applied to
* @param helpContent entity with values from HelpContent table
* @param nameValuePairs parsed string -> string map from the help content key.
***************************************************************************/
void acceptHelpContent(QInstance qInstance, QHelpContent helpContent, Map<String, String> nameValuePairs);
}

View File

@ -23,29 +23,44 @@ package com.kingsrook.qqq.backend.core.instances;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
import com.kingsrook.qqq.backend.core.actions.metadata.JoinGraph;
import com.kingsrook.qqq.backend.core.actions.permissions.BulkTableActionProcessPermissionChecker;
import com.kingsrook.qqq.backend.core.actions.values.QCustomPossibleValueProvider;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QRuntimeException;
import com.kingsrook.qqq.backend.core.instances.enrichment.plugins.QInstanceEnricherPluginInterface;
import com.kingsrook.qqq.backend.core.logging.QLogger;
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.QSupplementalInstanceMetaData;
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.fields.AdornmentType;
import com.kingsrook.qqq.backend.core.model.metadata.fields.AdornmentType.FileUploadAdornment;
import com.kingsrook.qqq.backend.core.model.metadata.fields.DynamicDefaultValueBehavior;
import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldAdornment;
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.fields.QSupplementalFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
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;
@ -54,6 +69,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.permissions.MetaDataWithPer
import com.kingsrook.qqq.backend.core.model.metadata.permissions.QPermissionRules;
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.QComponentType;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendComponentMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData;
@ -75,13 +91,20 @@ import com.kingsrook.qqq.backend.core.processes.implementations.bulk.edit.BulkEd
import com.kingsrook.qqq.backend.core.processes.implementations.bulk.edit.BulkEditTransformStep;
import com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert.BulkInsertExtractStep;
import com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert.BulkInsertLoadStep;
import com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert.BulkInsertPrepareFileMappingStep;
import com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert.BulkInsertPrepareFileUploadStep;
import com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert.BulkInsertPrepareValueMappingStep;
import com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert.BulkInsertReceiveFileMappingStep;
import com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert.BulkInsertReceiveValueMappingStep;
import com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert.BulkInsertTransformStep;
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.ExtractViaQueryStep;
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.StreamedETLWithFrontendProcess;
import com.kingsrook.qqq.backend.core.scheduler.QScheduleManager;
import com.kingsrook.qqq.backend.core.utils.ClassPathUtils;
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 static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
@ -107,6 +130,8 @@ public class QInstanceEnricher
//////////////////////////////////////////////////////////////////////////////////////////////////
private static final Map<String, String> labelMappings = new LinkedHashMap<>();
private static ListingHash<Class<?>, QInstanceEnricherPluginInterface<?>> enricherPlugins = new ListingHash<>();
/*******************************************************************************
@ -168,6 +193,7 @@ public class QInstanceEnricher
}
enrichJoins();
enrichInstance();
//////////////////////////////////////////////////////////////////////////////
// if the instance DOES have 1 or more scheduler, but no schedulable types, //
@ -184,6 +210,47 @@ public class QInstanceEnricher
/***************************************************************************
**
***************************************************************************/
private void enrichInstance()
{
////////////////////////////////////////////////////////////////////////////////////
// enriching some objects may cause additional ones to be added to the qInstance! //
// this caused concurrent modification exceptions, when we just iterated. //
// we could make a copy of the map and just process that, but then we wouldn't //
// enrich any new objects that do get added, so, use this technique instead. //
////////////////////////////////////////////////////////////////////////////////////
Set<QSupplementalInstanceMetaData> toEnrich = new LinkedHashSet<>(qInstance.getSupplementalMetaData().values());
Set<QSupplementalInstanceMetaData> enriched = new HashSet<>();
int count = 0;
while(!toEnrich.isEmpty())
{
Iterator<QSupplementalInstanceMetaData> iterator = toEnrich.iterator();
QSupplementalInstanceMetaData supplementalInstanceMetaData = iterator.next();
iterator.remove();
supplementalInstanceMetaData.enrich(qInstance);
enriched.add(supplementalInstanceMetaData);
for(QSupplementalInstanceMetaData possiblyNew : qInstance.getSupplementalMetaData().values())
{
if(!toEnrich.contains(possiblyNew) && !enriched.contains(possiblyNew))
{
if(count++ > 100)
{
throw (new QRuntimeException("Too many new QSupplementalInstanceMetaData objects were added while enriching others. This probably indicates a bug in enrichment code. Throwing to prevent infinite loop."));
}
toEnrich.add(possiblyNew);
}
}
}
runPlugins(QInstance.class, qInstance, qInstance);
}
/*******************************************************************************
**
*******************************************************************************/
@ -248,6 +315,14 @@ public class QInstanceEnricher
}
}
}
///////////////////////////////////////////
// run plugins on joins if there are any //
///////////////////////////////////////////
for(QJoinMetaData join : qInstance.getJoins().values())
{
runPlugins(QJoinMetaData.class, join, qInstance);
}
}
catch(Exception e)
{
@ -263,6 +338,7 @@ public class QInstanceEnricher
private void enrichWidget(QWidgetMetaDataInterface widgetMetaData)
{
enrichPermissionRules(widgetMetaData);
runPlugins(QWidgetMetaDataInterface.class, widgetMetaData, qInstance);
}
@ -273,6 +349,7 @@ public class QInstanceEnricher
private void enrichBackend(QBackendMetaData qBackendMetaData)
{
qBackendMetaData.enrich();
runPlugins(QBackendMetaData.class, qBackendMetaData, qInstance);
}
@ -289,7 +366,21 @@ public class QInstanceEnricher
if(table.getFields() != null)
{
table.getFields().values().forEach(this::enrichField);
for(Map.Entry<String, QFieldMetaData> entry : table.getFields().entrySet())
{
String name = entry.getKey();
QFieldMetaData field = entry.getValue();
////////////////////////////////////////////////////////////////////////////
// in case the field wasn't given a name, use its key from the fields map //
////////////////////////////////////////////////////////////////////////////
if(!StringUtils.hasContent(field.getName()))
{
field.setName(name);
}
enrichField(field);
}
for(QSupplementalTableMetaData supplementalTableMetaData : CollectionUtils.nonNullMap(table.getSupplementalMetaData()).values())
{
@ -313,6 +404,7 @@ public class QInstanceEnricher
enrichPermissionRules(table);
enrichAuditRules(table);
runPlugins(QTableMetaData.class, table, qInstance);
}
@ -403,6 +495,7 @@ public class QInstanceEnricher
}
enrichPermissionRules(process);
runPlugins(QProcessMetaData.class, process, qInstance);
}
@ -524,6 +617,16 @@ public class QInstanceEnricher
field.withBehavior(DynamicDefaultValueBehavior.MODIFY_DATE);
}
}
////////////////////////////////////////////////////
// enrich any supplemental meta data on the field //
////////////////////////////////////////////////////
for(QSupplementalFieldMetaData supplementalFieldMetaData : CollectionUtils.nonNullMap(field.getSupplementalMetaData()).values())
{
supplementalFieldMetaData.enrich(qInstance, field);
}
runPlugins(QFieldMetaData.class, field, qInstance);
}
@ -595,6 +698,7 @@ public class QInstanceEnricher
ensureAppSectionMembersAreAppChildren(app);
enrichPermissionRules(app);
runPlugins(QAppMetaData.class, app, qInstance);
}
@ -742,6 +846,7 @@ public class QInstanceEnricher
}
enrichPermissionRules(report);
runPlugins(QReportMetaData.class, report, qInstance);
}
@ -833,7 +938,7 @@ public class QInstanceEnricher
/*******************************************************************************
**
*******************************************************************************/
private void defineTableBulkInsert(QInstance qInstance, QTableMetaData table, String processName)
public void defineTableBulkInsert(QInstance qInstance, QTableMetaData table, String processName)
{
Map<String, Serializable> values = new HashMap<>();
values.put(StreamedETLWithFrontendProcess.FIELD_DESTINATION_TABLE, table.getName());
@ -845,6 +950,7 @@ public class QInstanceEnricher
values
)
.withName(processName)
.withIcon(new QIcon().withName("library_add"))
.withLabel(table.getLabel() + " Bulk Insert")
.withTableName(table.getName())
.withIsHidden(true)
@ -875,18 +981,76 @@ public class QInstanceEnricher
.map(QFieldMetaData::getLabel)
.collect(Collectors.joining(", "));
QBackendStepMetaData prepareFileUploadStep = new QBackendStepMetaData()
.withName("prepareFileUpload")
.withCode(new QCodeReference(BulkInsertPrepareFileUploadStep.class));
QFrontendStepMetaData uploadScreen = new QFrontendStepMetaData()
.withName("upload")
.withLabel("Upload File")
.withFormField(new QFieldMetaData("theFile", QFieldType.BLOB).withLabel(table.getLabel() + " File").withIsRequired(true))
.withComponent(new QFrontendComponentMetaData()
.withType(QComponentType.HELP_TEXT)
.withValue("previewText", "file upload instructions")
.withValue("text", "Upload a CSV file with the following columns:\n" + fieldsForHelpText))
.withFormField(new QFieldMetaData("theFile", QFieldType.BLOB)
.withFieldAdornment(FileUploadAdornment.newFieldAdornment()
.withValue(FileUploadAdornment.formatDragAndDrop())
.withValue(FileUploadAdornment.widthFull()))
.withLabel(table.getLabel() + " File")
.withIsRequired(true))
.withComponent(new QFrontendComponentMetaData().withType(QComponentType.HTML))
.withComponent(new QFrontendComponentMetaData().withType(QComponentType.EDIT_FORM));
process.addStep(0, uploadScreen);
process.getFrontendStep("review").setRecordListFields(editableFields);
QBackendStepMetaData prepareFileMappingStep = new QBackendStepMetaData()
.withName("prepareFileMapping")
.withCode(new QCodeReference(BulkInsertPrepareFileMappingStep.class));
QFrontendStepMetaData fileMappingScreen = new QFrontendStepMetaData()
.withName("fileMapping")
.withLabel("File Mapping")
.withBackStepName("prepareFileUpload")
.withComponent(new QFrontendComponentMetaData().withType(QComponentType.BULK_LOAD_FILE_MAPPING_FORM))
.withFormField(new QFieldMetaData("hasHeaderRow", QFieldType.BOOLEAN))
.withFormField(new QFieldMetaData("layout", QFieldType.STRING)); // is actually PVS, but, this field is only added to help support helpContent, so :shrug:
QBackendStepMetaData receiveFileMappingStep = new QBackendStepMetaData()
.withName("receiveFileMapping")
.withCode(new QCodeReference(BulkInsertReceiveFileMappingStep.class));
QBackendStepMetaData prepareValueMappingStep = new QBackendStepMetaData()
.withName("prepareValueMapping")
.withCode(new QCodeReference(BulkInsertPrepareValueMappingStep.class));
QFrontendStepMetaData valueMappingScreen = new QFrontendStepMetaData()
.withName("valueMapping")
.withLabel("Value Mapping")
.withBackStepName("prepareFileMapping")
.withComponent(new QFrontendComponentMetaData().withType(QComponentType.BULK_LOAD_VALUE_MAPPING_FORM));
QBackendStepMetaData receiveValueMappingStep = new QBackendStepMetaData()
.withName("receiveValueMapping")
.withCode(new QCodeReference(BulkInsertReceiveValueMappingStep.class));
int i = 0;
process.withStep(i++, prepareFileUploadStep);
process.withStep(i++, uploadScreen);
process.withStep(i++, prepareFileMappingStep);
process.withStep(i++, fileMappingScreen);
process.withStep(i++, receiveFileMappingStep);
process.withStep(i++, prepareValueMappingStep);
process.withStep(i++, valueMappingScreen);
process.withStep(i++, receiveValueMappingStep);
process.getFrontendStep(StreamedETLWithFrontendProcess.STEP_NAME_REVIEW).setRecordListFields(editableFields);
//////////////////////////////////////////////////////////////////////////////////////////
// put the bulk-load profile form (e.g., for saving it) on the review & result screens) //
//////////////////////////////////////////////////////////////////////////////////////////
process.getFrontendStep(StreamedETLWithFrontendProcess.STEP_NAME_REVIEW)
.withBackStepName("prepareFileMapping")
.getComponents().add(0, new QFrontendComponentMetaData().withType(QComponentType.BULK_LOAD_PROFILE_FORM));
process.getFrontendStep(StreamedETLWithFrontendProcess.STEP_NAME_RESULT)
.getComponents().add(0, new QFrontendComponentMetaData().withType(QComponentType.BULK_LOAD_PROFILE_FORM));
qInstance.addProcess(process);
}
@ -933,7 +1097,7 @@ public class QInstanceEnricher
Fields whose switches are off will not be updated."""))
.withComponent(new QFrontendComponentMetaData().withType(QComponentType.BULK_EDIT_FORM));
process.addStep(0, editScreen);
process.withStep(0, editScreen);
process.getFrontendStep("review").setRecordListFields(editableFields);
qInstance.addProcess(process);
}
@ -1281,6 +1445,159 @@ public class QInstanceEnricher
}
}
if(possibleValueSource.getIdType() == null)
{
QTableMetaData table = qInstance.getTable(possibleValueSource.getTableName());
if(table != null && table.getFields() != null)
{
String primaryKeyField = table.getPrimaryKeyField();
QFieldMetaData primaryKeyFieldMetaData = CollectionUtils.nonNullMap(table.getFields()).get(primaryKeyField);
if(primaryKeyFieldMetaData != null)
{
possibleValueSource.setIdType(primaryKeyFieldMetaData.getType());
}
}
}
}
else if(QPossibleValueSourceType.ENUM.equals(possibleValueSource.getType()))
{
if(possibleValueSource.getIdType() == null)
{
if(CollectionUtils.nullSafeHasContents(possibleValueSource.getEnumValues()))
{
Object id = possibleValueSource.getEnumValues().get(0).getId();
try
{
possibleValueSource.setIdType(QFieldType.fromClass(id.getClass()));
}
catch(Exception e)
{
LOG.warn("Error enriching possible value source with idType based on first enum value", e, logPair("possibleValueSource", possibleValueSource.getName()), logPair("id", id));
}
}
}
}
else if(QPossibleValueSourceType.CUSTOM.equals(possibleValueSource.getType()))
{
if(possibleValueSource.getIdType() == null)
{
try
{
QCustomPossibleValueProvider<?> customPossibleValueProvider = QCodeLoader.getAdHoc(QCustomPossibleValueProvider.class, possibleValueSource.getCustomCodeReference());
Method getPossibleValueMethod = customPossibleValueProvider.getClass().getMethod("getPossibleValue", Serializable.class);
Type returnType = getPossibleValueMethod.getGenericReturnType();
Type idType = ((ParameterizedType) returnType).getActualTypeArguments()[0];
if(idType instanceof Class<?> c)
{
possibleValueSource.setIdType(QFieldType.fromClass(c));
}
}
catch(Exception e)
{
LOG.warn("Error enriching possible value source with idType based on first custom value", e, logPair("possibleValueSource", possibleValueSource.getName()));
}
}
}
runPlugins(QPossibleValueSource.class, possibleValueSource, qInstance);
}
/*******************************************************************************
**
*******************************************************************************/
public static void addEnricherPlugin(QInstanceEnricherPluginInterface<?> plugin)
{
Optional<Method> enrichMethod = Arrays.stream(plugin.getClass().getDeclaredMethods())
.filter(m -> m.getName().equals("enrich")
&& m.getParameterCount() == 2
&& !m.getParameterTypes()[0].equals(Object.class)
&& m.getParameterTypes()[1].equals(QInstance.class)
).findFirst();
if(enrichMethod.isPresent())
{
Class<?> parameterType = enrichMethod.get().getParameterTypes()[0];
Set<String> existingPluginIdentifiers = enricherPlugins.getOrDefault(parameterType, Collections.emptyList())
.stream().map(p -> p.getPluginIdentifier())
.collect(Collectors.toSet());
if(existingPluginIdentifiers.contains(plugin.getPluginIdentifier()))
{
LOG.debug("Enricher plugin is already registered - not re-adding it", logPair("pluginIdentifer", plugin.getPluginIdentifier()));
}
else
{
enricherPlugins.add(parameterType, plugin);
}
}
else
{
LOG.warn("Could not find enrich method on enricher plugin [" + plugin.getClass().getName() + "] (to infer type being enriched) - this plugin will not be used.");
}
}
/*******************************************************************************
**
*******************************************************************************/
public static void removeAllEnricherPlugins()
{
enricherPlugins.clear();
}
/*******************************************************************************
** Getter for enricherPlugins
**
*******************************************************************************/
public static ListingHash<Class<?>, QInstanceEnricherPluginInterface<?>> getEnricherPlugins()
{
return enricherPlugins;
}
/***************************************************************************
** scan the classpath for classes in the specified package name which
** implement the QInstanceEnricherPluginInterface - any found get added
***************************************************************************/
public static void discoverAndAddPluginsInPackage(String packageName) throws QException
{
try
{
for(Class<?> aClass : ClassPathUtils.getClassesInPackage(packageName))
{
if(QInstanceEnricherPluginInterface.class.isAssignableFrom(aClass))
{
QInstanceEnricherPluginInterface<?> plugin = (QInstanceEnricherPluginInterface<?>) aClass.getConstructor().newInstance();
addEnricherPlugin(plugin);
}
}
}
catch(Exception e)
{
throw (new QException("Error discovering and adding enricher plugins in package [" + packageName + "]", e));
}
}
/*******************************************************************************
**
*******************************************************************************/
private <T> void runPlugins(Class<T> c, T t, QInstance qInstance)
{
for(QInstanceEnricherPluginInterface<?> plugin : CollectionUtils.nonNullList(enricherPlugins.get(c)))
{
@SuppressWarnings("unchecked")
QInstanceEnricherPluginInterface<T> castedPlugin = (QInstanceEnricherPluginInterface<T>) plugin;
castedPlugin.enrich(t, qInstance);
}
}

View File

@ -30,13 +30,13 @@ import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
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.query.QueryInput;
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.helpcontent.HelpContent;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.QSupplementalInstanceMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaDataInterface;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.help.HelpFormat;
@ -111,7 +111,7 @@ public class QInstanceHelpContentManager
}
else
{
LOG.info("Discarding help content with key that does not contain name:value format", logPair("key", key), logPair("id", record.getValue("id")));
LOG.info("Discarding help content with key-part that does not contain name:value format", logPair("key", key), logPair("part", part), logPair("id", record.getValue("id")));
}
}
@ -150,19 +150,36 @@ public class QInstanceHelpContentManager
///////////////////////////////////////////////////////////////////////////////////
if(StringUtils.hasContent(tableName))
{
processHelpContentForTable(key, tableName, sectionName, fieldName, slotName, roles, helpContent);
processHelpContentForTable(qInstance, key, tableName, sectionName, fieldName, slotName, roles, helpContent);
}
else if(StringUtils.hasContent(processName))
{
processHelpContentForProcess(key, processName, fieldName, stepName, roles, helpContent);
processHelpContentForProcess(qInstance, key, processName, fieldName, stepName, roles, helpContent);
}
else if(StringUtils.hasContent(widgetName))
{
processHelpContentForWidget(key, widgetName, slotName, roles, helpContent);
processHelpContentForWidget(qInstance, key, widgetName, slotName, roles, helpContent);
}
else if(nameValuePairs.containsKey("instanceLevel"))
{
processHelpContentForInstance(key, slotName, roles, helpContent);
processHelpContentForInstance(qInstance, key, slotName, roles, helpContent);
}
else
{
for(QSupplementalInstanceMetaData supplementalInstanceMetaData : qInstance.getSupplementalMetaData().values())
{
if(supplementalInstanceMetaData instanceof QHelpContentPlugin helpContentPlugin)
{
try
{
helpContentPlugin.acceptHelpContent(qInstance, helpContent, nameValuePairs);
}
catch(Exception e)
{
LOG.warn("Error processing a helpContent record in a helpContentPlugin", e, logPair("pluginName", supplementalInstanceMetaData.getName()), logPair("id", record.getValue("id")));
}
}
}
}
}
catch(Exception e)
@ -176,9 +193,9 @@ public class QInstanceHelpContentManager
/*******************************************************************************
**
*******************************************************************************/
private static void processHelpContentForTable(String key, String tableName, String sectionName, String fieldName, String slotName, Set<HelpRole> roles, QHelpContent helpContent)
private static void processHelpContentForTable(QInstance qInstance, String key, String tableName, String sectionName, String fieldName, String slotName, Set<HelpRole> roles, QHelpContent helpContent)
{
QTableMetaData table = QContext.getQInstance().getTable(tableName);
QTableMetaData table = qInstance.getTable(tableName);
if(table == null)
{
LOG.info("Unrecognized table in help content", logPair("key", key));
@ -246,9 +263,30 @@ public class QInstanceHelpContentManager
/*******************************************************************************
**
*******************************************************************************/
private static void processHelpContentForProcess(String key, String processName, String fieldName, String stepName, Set<HelpRole> roles, QHelpContent helpContent)
private static void processHelpContentForProcess(QInstance qInstance, String key, String processName, String fieldName, String stepName, Set<HelpRole> roles, QHelpContent helpContent)
{
QProcessMetaData process = QContext.getQInstance().getProcess(processName);
if(processName.startsWith("*") && processName.length() > 1)
{
boolean anyMatched = false;
String subName = processName.substring(1);
for(QProcessMetaData process : qInstance.getProcesses().values())
{
if(process.getName().endsWith(subName))
{
anyMatched = true;
processHelpContentForProcess(qInstance, key, process.getName(), fieldName, stepName, roles, helpContent);
}
}
if(!anyMatched)
{
LOG.info("Wildcard process name did not match any processes in help content", logPair("key", key));
}
return;
}
QProcessMetaData process = qInstance.getProcess(processName);
if(process == null)
{
LOG.info("Unrecognized process in help content", logPair("key", key));
@ -306,9 +344,9 @@ public class QInstanceHelpContentManager
/*******************************************************************************
**
*******************************************************************************/
private static void processHelpContentForWidget(String key, String widgetName, String slotName, Set<HelpRole> roles, QHelpContent helpContent)
private static void processHelpContentForWidget(QInstance qInstance, String key, String widgetName, String slotName, Set<HelpRole> roles, QHelpContent helpContent)
{
QWidgetMetaDataInterface widget = QContext.getQInstance().getWidget(widgetName);
QWidgetMetaDataInterface widget = qInstance.getWidget(widgetName);
if(!StringUtils.hasContent(slotName))
{
LOG.info("Missing slot name in help content", logPair("key", key));
@ -335,7 +373,7 @@ public class QInstanceHelpContentManager
/*******************************************************************************
**
*******************************************************************************/
private static void processHelpContentForInstance(String key, String slotName, Set<HelpRole> roles, QHelpContent helpContent)
private static void processHelpContentForInstance(QInstance qInstance, String key, String slotName, Set<HelpRole> roles, QHelpContent helpContent)
{
if(!StringUtils.hasContent(slotName))
{
@ -345,11 +383,11 @@ public class QInstanceHelpContentManager
{
if(helpContent != null)
{
QContext.getQInstance().withHelpContent(slotName, helpContent);
qInstance.withHelpContent(slotName, helpContent);
}
else
{
QContext.getQInstance().removeHelpContent(slotName, roles);
qInstance.removeHelpContent(slotName, roles);
}
}
}

View File

@ -37,12 +37,16 @@ import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.TimeZone;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationHandler;
import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationHandlerInterface;
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizerInterface;
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.MetaDataActionCustomizerInterface;
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;
@ -61,6 +65,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.QSupplementalInstanceMetaDa
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.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReferenceLambda;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType;
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.ParentWidgetMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaDataInterface;
@ -68,8 +73,10 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.AdornmentType;
import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldAdornment;
import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldBehavior;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QSupplementalFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior;
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn;
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinType;
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppChildMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData;
@ -107,12 +114,16 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.Automatio
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.QTableAutomationDetails;
import com.kingsrook.qqq.backend.core.model.metadata.tables.cache.CacheOf;
import com.kingsrook.qqq.backend.core.model.metadata.tables.cache.CacheUseCase;
import com.kingsrook.qqq.backend.core.model.metadata.variants.BackendVariantSetting;
import com.kingsrook.qqq.backend.core.model.metadata.variants.BackendVariantsConfig;
import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleCustomizerInterface;
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;
import com.kingsrook.qqq.backend.core.utils.lambdas.UnsafeFunction;
import com.kingsrook.qqq.backend.core.utils.lambdas.UnsafeLambda;
import org.apache.commons.lang.BooleanUtils;
import org.quartz.CronExpression;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
@ -135,6 +146,8 @@ public class QInstanceValidator
private static ListingHash<Class<?>, QInstanceValidatorPluginInterface<?>> validatorPlugins = new ListingHash<>();
private JoinGraph joinGraph = null;
private List<String> errors = new ArrayList<>();
@ -162,8 +175,7 @@ public class QInstanceValidator
// the enricher will build a join graph (if there are any joins). we'd like to only do that //
// once, during the enrichment/validation work, so, capture it, and store it back in the instance. //
/////////////////////////////////////////////////////////////////////////////////////////////////////
JoinGraph joinGraph = null;
long start = System.currentTimeMillis();
long start = System.currentTimeMillis();
try
{
/////////////////////////////////////////////////////////////////////////////////////////////////
@ -172,7 +184,7 @@ public class QInstanceValidator
// TODO - possible point of customization (use a different enricher, or none, or pass it options).
QInstanceEnricher qInstanceEnricher = new QInstanceEnricher(qInstance);
qInstanceEnricher.enrich();
joinGraph = qInstanceEnricher.getJoinGraph();
this.joinGraph = qInstanceEnricher.getJoinGraph();
}
catch(Exception e)
{
@ -229,14 +241,32 @@ public class QInstanceValidator
/***************************************************************************
**
* this method still supports the deprecated MetaDataFilter (plus its
* replacement, MetaDataActionCustomizer
***************************************************************************/
@SuppressWarnings("deprecation")
private void validateInstanceAttributes(QInstance qInstance)
{
if(qInstance.getMetaDataFilter() != null)
{
validateSimpleCodeReference("Instance metaDataFilter ", qInstance.getMetaDataFilter(), MetaDataFilterInterface.class);
}
if(qInstance.getMetaDataActionCustomizer() != null)
{
validateSimpleCodeReference("Instance metaDataActionCustomizer ", qInstance.getMetaDataActionCustomizer(), MetaDataActionCustomizerInterface.class);
}
if(qInstance.getTableCustomizers() != null)
{
for(Map.Entry<String, List<QCodeReference>> entry : qInstance.getTableCustomizers().entrySet())
{
for(QCodeReference codeReference : CollectionUtils.nonNullList(entry.getValue()))
{
validateSimpleCodeReference("Instance tableCustomizer of type " + entry.getKey() + ": ", codeReference, TableCustomizerInterface.class);
}
}
}
}
@ -268,7 +298,18 @@ public class QInstanceValidator
if(validateMethod.isPresent())
{
Class<?> parameterType = validateMethod.get().getParameterTypes()[0];
validatorPlugins.add(parameterType, plugin);
Set<String> existingPluginIdentifiers = validatorPlugins.getOrDefault(parameterType, Collections.emptyList())
.stream().map(p -> p.getPluginIdentifier())
.collect(Collectors.toSet());
if(existingPluginIdentifiers.contains(plugin.getPluginIdentifier()))
{
LOG.debug("Validator plugin is already registered - not re-adding it", logPair("pluginIdentifer", plugin.getPluginIdentifier()));
}
else
{
validatorPlugins.add(parameterType, plugin);
}
}
else
{
@ -288,6 +329,17 @@ public class QInstanceValidator
/*******************************************************************************
** Getter for validatorPlugins
**
*******************************************************************************/
public static ListingHash<Class<?>, QInstanceValidatorPluginInterface<?>> getValidatorPlugins()
{
return validatorPlugins;
}
/*******************************************************************************
**
*******************************************************************************/
@ -372,8 +424,8 @@ public class QInstanceValidator
assertCondition(join.getType() != null, "Missing type for join: " + joinName);
assertCondition(CollectionUtils.nullSafeHasContents(join.getJoinOns()), "Missing joinOns for join: " + joinName);
boolean leftTableExists = assertCondition(qInstance.getTable(join.getLeftTable()) != null, "Left-table name " + join.getLeftTable() + " join " + joinName + " is not a defined table in this instance.");
boolean rightTableExists = assertCondition(qInstance.getTable(join.getRightTable()) != null, "Right-table name " + join.getRightTable() + " join " + joinName + " is not a defined table in this instance.");
boolean leftTableExists = assertCondition(qInstance.getTable(join.getLeftTable()) != null, "Left-table name " + join.getLeftTable() + " in join " + joinName + " is not a defined table in this instance.");
boolean rightTableExists = assertCondition(qInstance.getTable(join.getRightTable()) != null, "Right-table name " + join.getRightTable() + " in join " + joinName + " is not a defined table in this instance.");
for(JoinOn joinOn : CollectionUtils.nonNullList(join.getJoinOns()))
{
@ -542,6 +594,60 @@ public class QInstanceValidator
{
assertCondition(Objects.equals(backendName, backend.getName()), "Inconsistent naming for backend: " + backendName + "/" + backend.getName() + ".");
///////////////////////
// validate variants //
///////////////////////
BackendVariantsConfig backendVariantsConfig = backend.getBackendVariantsConfig();
if(BooleanUtils.isTrue(backend.getUsesVariants()))
{
if(assertCondition(backendVariantsConfig != null, "Missing backendVariantsConfig in backend [" + backendName + "] which is marked as usesVariants"))
{
assertCondition(StringUtils.hasContent(backendVariantsConfig.getVariantTypeKey()), "Missing variantTypeKey in backendVariantsConfig in [" + backendName + "]");
String optionsTableName = backendVariantsConfig.getOptionsTableName();
QTableMetaData optionsTable = qInstance.getTable(optionsTableName);
if(assertCondition(StringUtils.hasContent(optionsTableName), "Missing optionsTableName in backendVariantsConfig in [" + backendName + "]"))
{
if(assertCondition(optionsTable != null, "Unrecognized optionsTableName [" + optionsTableName + "] in backendVariantsConfig in [" + backendName + "]"))
{
QQueryFilter optionsFilter = backendVariantsConfig.getOptionsFilter();
if(optionsFilter != null)
{
validateQueryFilter(qInstance, "optionsFilter in backendVariantsConfig in backend [" + backendName + "]: ", optionsTable, optionsFilter, null);
}
}
}
Map<BackendVariantSetting, String> backendSettingSourceFieldNameMap = backendVariantsConfig.getBackendSettingSourceFieldNameMap();
if(assertCondition(CollectionUtils.nullSafeHasContents(backendSettingSourceFieldNameMap), "Missing or empty backendSettingSourceFieldNameMap in backendVariantsConfig in [" + backendName + "]"))
{
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
// only validate field names in the backendSettingSourceFieldNameMap if there is NOT a variantRecordSupplier //
// (the idea being, that the supplier might be building a record with fieldNames that aren't in the table... //
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
if(optionsTable != null && backendVariantsConfig.getVariantRecordLookupFunction() == null)
{
for(Map.Entry<BackendVariantSetting, String> entry : backendSettingSourceFieldNameMap.entrySet())
{
assertCondition(optionsTable.getFields().containsKey(entry.getValue()), "Unrecognized fieldName [" + entry.getValue() + "] in backendSettingSourceFieldNameMap in backendVariantsConfig in [" + backendName + "]");
}
}
}
if(backendVariantsConfig.getVariantRecordLookupFunction() != null)
{
validateSimpleCodeReference("VariantRecordSupplier in backendVariantsConfig in backend [" + backendName + "]: ", backendVariantsConfig.getVariantRecordLookupFunction(), UnsafeFunction.class, Function.class);
}
}
}
else
{
assertCondition(backendVariantsConfig == null, "Should not have a backendVariantsConfig in backend [" + backendName + "] which is not marked as usesVariants");
}
///////////////////////////////////////////
// let the backend do its own validation //
///////////////////////////////////////////
backend.performValidation(this);
runPlugins(QBackendMetaData.class, backend, qInstance);
@ -576,13 +682,15 @@ public class QInstanceValidator
private void validateAuthentication(QInstance qInstance)
{
QAuthenticationMetaData authentication = qInstance.getAuthentication();
if(authentication != null)
if(assertCondition(authentication != null, "Authentication MetaData must be defined."))
{
if(authentication.getCustomizer() != null)
{
validateSimpleCodeReference("Instance Authentication meta data customizer ", authentication.getCustomizer(), QAuthenticationModuleCustomizerInterface.class);
}
authentication.validate(qInstance, this);
runPlugins(QAuthenticationMetaData.class, authentication, qInstance);
}
}
@ -779,15 +887,38 @@ public class QInstanceValidator
{
if(assertCondition(StringUtils.hasContent(association.getName()), "missing a name for an Association on table " + table.getName()))
{
String messageSuffix = " for Association " + association.getName() + " on table " + table.getName();
String messageSuffix = " for Association " + association.getName() + " on table " + table.getName();
boolean recognizedTable = false;
if(assertCondition(StringUtils.hasContent(association.getAssociatedTableName()), "missing associatedTableName" + messageSuffix))
{
assertCondition(qInstance.getTable(association.getAssociatedTableName()) != null, "unrecognized associatedTableName " + association.getAssociatedTableName() + messageSuffix);
if(assertCondition(qInstance.getTable(association.getAssociatedTableName()) != null, "unrecognized associatedTableName " + association.getAssociatedTableName() + messageSuffix))
{
recognizedTable = true;
}
}
if(assertCondition(StringUtils.hasContent(association.getJoinName()), "missing joinName" + messageSuffix))
{
assertCondition(qInstance.getJoin(association.getJoinName()) != null, "unrecognized joinName " + association.getJoinName() + messageSuffix);
QJoinMetaData join = qInstance.getJoin(association.getJoinName());
if(assertCondition(join != null, "unrecognized joinName " + association.getJoinName() + messageSuffix))
{
assert join != null; // covered by the assertCondition
if(recognizedTable)
{
boolean isLeftToRight = join.getLeftTable().equals(table.getName()) && join.getRightTable().equals(association.getAssociatedTableName());
boolean isRightToLeft = join.getRightTable().equals(table.getName()) && join.getLeftTable().equals(association.getAssociatedTableName());
assertCondition(isLeftToRight || isRightToLeft, "join [" + association.getJoinName() + "] does not connect tables [" + table.getName() + "] and [" + association.getAssociatedTableName() + "]" + messageSuffix);
if(isLeftToRight)
{
assertCondition(join.getType().equals(JoinType.ONE_TO_MANY) || join.getType().equals(JoinType.ONE_TO_ONE), "Join type does not have 'one' on this table's side side (left)" + messageSuffix);
}
else if(isRightToLeft)
{
assertCondition(join.getType().equals(JoinType.MANY_TO_ONE) || join.getType().equals(JoinType.ONE_TO_ONE), "Join type does not have 'one' on this table's side (right)" + messageSuffix);
}
}
}
}
}
}
@ -964,7 +1095,15 @@ public class QInstanceValidator
@SuppressWarnings("unchecked")
Class<FieldBehavior<?>> behaviorClass = (Class<FieldBehavior<?>>) fieldBehavior.getClass();
errors.addAll(fieldBehavior.validateBehaviorConfiguration(table, field));
List<String> behaviorErrors = fieldBehavior.validateBehaviorConfiguration(table, field);
if(behaviorErrors != null)
{
String prefixMinusTrailingSpace = prefix.replaceFirst(" *$", "");
for(String behaviorError : behaviorErrors)
{
errors.add(prefixMinusTrailingSpace + ": " + behaviorClass.getSimpleName() + ": " + behaviorError);
}
}
if(!fieldBehavior.allowMultipleBehaviorsOfThisType())
{
@ -1046,6 +1185,21 @@ public class QInstanceValidator
}
}
}
validateFieldSupplementalMetaData(field, qInstance);
}
/***************************************************************************
**
***************************************************************************/
public void validateFieldSupplementalMetaData(QFieldMetaData field, QInstance qInstance)
{
for(QSupplementalFieldMetaData supplementalFieldMetaData : CollectionUtils.nonNullMap(field.getSupplementalMetaData()).values())
{
supplementalFieldMetaData.validate(qInstance, field, this);
}
}
@ -1238,7 +1392,7 @@ public class QInstanceValidator
numberSet++;
if(preAssertionsForCodeReference(action.getCodeReference(), actionPrefix))
{
validateSimpleCodeReference(actionPrefix + "code reference: ", action.getCodeReference(), RecordAutomationHandler.class);
validateSimpleCodeReference(actionPrefix + "code reference: ", action.getCodeReference(), RecordAutomationHandlerInterface.class);
}
}
@ -1307,7 +1461,7 @@ public class QInstanceValidator
//////////////////////////////////////////////////
// make sure the customizer can be instantiated //
//////////////////////////////////////////////////
Object customizerInstance = getInstanceOfCodeReference(prefix, customizerClass);
Object customizerInstance = getInstanceOfCodeReference(prefix, customizerClass, codeReference);
TableCustomizers tableCustomizer = TableCustomizers.forRole(roleName);
if(tableCustomizer == null)
@ -1324,7 +1478,7 @@ public class QInstanceValidator
////////////////////////////////////////////////////////////////////////
if(customizerInstance != null && tableCustomizer.getExpectedType() != null)
{
assertObjectCanBeCasted(prefix, tableCustomizer.getExpectedType(), customizerInstance);
assertObjectCanBeCasted(prefix, customizerInstance, tableCustomizer.getExpectedType());
}
}
}
@ -1336,18 +1490,31 @@ public class QInstanceValidator
/*******************************************************************************
** Make sure that a given object can be casted to an expected type.
*******************************************************************************/
private <T> T assertObjectCanBeCasted(String errorPrefix, Class<T> expectedType, Object object)
private void assertObjectCanBeCasted(String errorPrefix, Object object, Class<?>... anyOfExpectedClasses)
{
T castedObject = null;
try
for(Class<?> expectedClass : anyOfExpectedClasses)
{
castedObject = expectedType.cast(object);
try
{
expectedClass.cast(object);
return;
}
catch(ClassCastException e)
{
/////////////////////////////////////
// try next type (if there is one) //
/////////////////////////////////////
}
}
catch(ClassCastException e)
if(anyOfExpectedClasses.length == 1)
{
errors.add(errorPrefix + "CodeReference is not of the expected type: " + expectedType);
errors.add(errorPrefix + "CodeReference is not of the expected type: " + anyOfExpectedClasses[0]);
}
else
{
errors.add(errorPrefix + "CodeReference is not any of the expected types: " + Arrays.stream(anyOfExpectedClasses).map(c -> c.getName()).collect(Collectors.joining(", ")));
}
return castedObject;
}
@ -1355,8 +1522,13 @@ public class QInstanceValidator
/*******************************************************************************
**
*******************************************************************************/
private Object getInstanceOfCodeReference(String prefix, Class<?> clazz)
private Object getInstanceOfCodeReference(String prefix, Class<?> clazz, QCodeReference codeReference)
{
if(codeReference instanceof QCodeReferenceLambda<?> lambdaCodeReference)
{
return (lambdaCodeReference.getLambda());
}
Object instance = null;
try
{
@ -1535,21 +1707,26 @@ public class QInstanceValidator
Set<String> usedStepNames = new HashSet<>();
if(assertCondition(CollectionUtils.nullSafeHasContents(process.getStepList()), "At least 1 step must be defined in process " + processName + "."))
{
int index = 0;
int index = -1;
for(QStepMetaData step : process.getStepList())
{
index++;
if(assertCondition(StringUtils.hasContent(step.getName()), "Missing name for a step at index " + index + " in process " + processName))
{
assertCondition(!usedStepNames.contains(step.getName()), "Duplicate step name [" + step.getName() + "] in process " + processName);
usedStepNames.add(step.getName());
}
index++;
////////////////////////////////////////////
// validate instantiation of step classes //
////////////////////////////////////////////
if(step instanceof QBackendStepMetaData backendStepMetaData)
{
if(assertCondition(backendStepMetaData.getCode() != null, "Missing code for a backend step at index " + index + " in process " + processName))
{
validateSimpleCodeReference("Process " + processName + ", backend step at index " + index + ", code reference: ", backendStepMetaData.getCode(), BackendStep.class);
}
if(backendStepMetaData.getInputMetaData() != null && CollectionUtils.nullSafeHasContents(backendStepMetaData.getInputMetaData().getFieldList()))
{
for(QFieldMetaData fieldMetaData : backendStepMetaData.getInputMetaData().getFieldList())
@ -1576,6 +1753,8 @@ public class QInstanceValidator
validateSimpleCodeReference("Process " + processName + " code reference:", codeReference, expectedClass);
}
validateFieldSupplementalMetaData(fieldMetaData, qInstance);
}
}
}
@ -1584,12 +1763,12 @@ public class QInstanceValidator
for(QFieldMetaData field : process.getInputFields())
{
validateFieldPossibleValueSourceAttributes(qInstance, field, "Process " + processName + ", input field " + field.getName());
validateFieldPossibleValueSourceAttributes(qInstance, field, "Process " + processName + ", input field " + field.getName() + " ");
}
for(QFieldMetaData field : process.getOutputFields())
{
validateFieldPossibleValueSourceAttributes(qInstance, field, "Process " + processName + ", output field " + field.getName());
validateFieldPossibleValueSourceAttributes(qInstance, field, "Process " + processName + ", output field " + field.getName() + " ");
}
if(process.getCancelStep() != null)
@ -1800,7 +1979,7 @@ public class QInstanceValidator
/*******************************************************************************
**
*******************************************************************************/
private void validateQueryFilter(QInstance qInstance, String context, QTableMetaData table, QQueryFilter queryFilter, List<QueryJoin> queryJoins)
public void validateQueryFilter(QInstance qInstance, String context, QTableMetaData table, QQueryFilter queryFilter, List<QueryJoin> queryJoins)
{
for(QFilterCriteria criterion : CollectionUtils.nonNullList(queryFilter.getCriteria()))
{
@ -1844,7 +2023,8 @@ public class QInstanceValidator
{
if(fieldName.contains("."))
{
String fieldNameAfterDot = fieldName.substring(fieldName.lastIndexOf(".") + 1);
String fieldNameAfterDot = fieldName.substring(fieldName.lastIndexOf(".") + 1);
String tableNameBeforeDot = fieldName.substring(0, fieldName.lastIndexOf("."));
if(CollectionUtils.nullSafeHasContents(queryJoins))
{
@ -1868,11 +2048,32 @@ public class QInstanceValidator
}
else
{
errors.add("QInstanceValidator does not yet support finding a field that looks like a join field, but isn't associated with a query.");
return (true);
// todo! for(QJoinMetaData join : CollectionUtils.nonNullMap(qInstance.getJoins()).values())
// {
// }
if(this.joinGraph != null)
{
Set<JoinGraph.JoinConnectionList> joinConnections = joinGraph.getJoinConnections(table.getName());
for(JoinGraph.JoinConnectionList joinConnectionList : joinConnections)
{
JoinGraph.JoinConnection joinConnection = joinConnectionList.list().get(joinConnectionList.list().size() - 1);
if(tableNameBeforeDot.equals(joinConnection.joinTable()))
{
QTableMetaData joinTable = qInstance.getTable(tableNameBeforeDot);
if(joinTable.getFields().containsKey(fieldNameAfterDot))
{
/////////////////////////
// mmm, looks valid... //
/////////////////////////
return (true);
}
}
}
}
//////////////////////////////////////////////////////////////////////////////////////
// todo - not sure how vulnerable we are to ongoing issues here... //
// idea: let a filter (or any object?) be opted out of validation, some version of //
// a static map of objects we can check at the top of various validate methods... //
//////////////////////////////////////////////////////////////////////////////////////
errors.add("Failed to find field named: " + fieldName);
}
}
}
@ -1976,6 +2177,11 @@ public class QInstanceValidator
}
}
if(widget.getValidatorPlugin() != null)
{
widget.getValidatorPlugin().validate(widget, qInstance, this);
}
runPlugins(QWidgetMetaDataInterface.class, widget, qInstance);
}
);
@ -2075,6 +2281,8 @@ public class QInstanceValidator
default -> errors.add("Unexpected possibleValueSource type: " + possibleValueSource.getType());
}
assertCondition(possibleValueSource.getIdType() != null, "possibleValueSource " + name + " is missing its idType.");
runPlugins(QPossibleValueSource.class, possibleValueSource, qInstance);
}
}
@ -2084,7 +2292,7 @@ public class QInstanceValidator
/*******************************************************************************
**
*******************************************************************************/
private void validateSimpleCodeReference(String prefix, QCodeReference codeReference, Class<?> expectedClass)
public void validateSimpleCodeReference(String prefix, QCodeReference codeReference, Class<?>... anyOfExpectedClasses)
{
if(!preAssertionsForCodeReference(codeReference, prefix))
{
@ -2105,14 +2313,14 @@ public class QInstanceValidator
//////////////////////////////////////////////////
// make sure the customizer can be instantiated //
//////////////////////////////////////////////////
Object classInstance = getInstanceOfCodeReference(prefix, clazz);
Object classInstance = getInstanceOfCodeReference(prefix, clazz, codeReference);
////////////////////////////////////////////////////////////////////////
// make sure the customizer instance can be cast to the expected type //
////////////////////////////////////////////////////////////////////////
if(classInstance != null)
{
assertObjectCanBeCasted(prefix, expectedClass, classInstance);
assertObjectCanBeCasted(prefix, classInstance, anyOfExpectedClasses);
}
}
}
@ -2128,6 +2336,11 @@ public class QInstanceValidator
Class<?> clazz = null;
try
{
if(codeReference instanceof QCodeReferenceLambda<?> lambdaCodeReference)
{
return (lambdaCodeReference.getLambda().getClass());
}
clazz = Class.forName(codeReference.getName());
}
catch(ClassNotFoundException e)

View File

@ -72,6 +72,18 @@ public class SecretsManagerUtils
** and write them to a .env file (backing up any pre-existing .env files first).
*******************************************************************************/
public static void writeEnvFromSecretsWithNamePrefix(String prefix) throws IOException
{
writeEnvFromSecretsWithNamePrefix(prefix, true);
}
/*******************************************************************************
** IF secret manager ENV vars are set,
** THEN lookup all secrets starting with the given prefix,
** and write them to a .env file (backing up any pre-existing .env files first).
*******************************************************************************/
public static void writeEnvFromSecretsWithNamePrefix(String prefix, boolean quoteValues) throws IOException
{
Optional<AWSSecretsManager> optionalSecretsManagerClient = getSecretsManagerClient();
if(optionalSecretsManagerClient.isPresent())
@ -91,7 +103,9 @@ public class SecretsManagerUtils
Optional<String> secretValue = getSecret(prefix, nameWithoutPrefix);
if(secretValue.isPresent())
{
String envLine = nameWithoutPrefix + "=" + secretValue.get();
String envLine = quoteValues
? nameWithoutPrefix + "=\"" + secretValue.get() + "\""
: nameWithoutPrefix + "=" + secretValue.get();
fullEnv.append(envLine).append('\n');
}
}

View File

@ -0,0 +1,37 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. 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.assessment;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
/*******************************************************************************
** marker for an object which can be processed by the QInstanceAssessor.
*******************************************************************************/
public interface Assessable
{
/***************************************************************************
**
***************************************************************************/
void assess(QInstanceAssessor qInstanceAssessor, QInstance qInstance);
}

View File

@ -0,0 +1,219 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. 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.assessment;
import java.util.ArrayList;
import java.util.List;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
/*******************************************************************************
** POC of a class that is meant to review meta-data for accuracy vs. real backends.
*******************************************************************************/
public class QInstanceAssessor
{
private static final QLogger LOG = QLogger.getLogger(QInstanceAssessor.class);
private final QInstance qInstance;
private List<String> errors = new ArrayList<>();
private List<String> warnings = new ArrayList<>();
private List<String> suggestions = new ArrayList<>();
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public QInstanceAssessor(QInstance qInstance)
{
this.qInstance = qInstance;
}
/*******************************************************************************
**
*******************************************************************************/
public void assess()
{
for(QBackendMetaData backend : qInstance.getBackends().values())
{
if(backend instanceof Assessable assessable)
{
assessable.assess(this, qInstance);
}
}
}
/*******************************************************************************
**
*******************************************************************************/
@SuppressWarnings("checkstyle:AvoidEscapedUnicodeCharacters")
public String getSummary()
{
StringBuilder rs = new StringBuilder();
///////////////////////////
// print header & errors //
///////////////////////////
if(CollectionUtils.nullSafeIsEmpty(errors))
{
rs.append("Assessment passed with no errors! \uD83D\uDE0E\n");
}
else
{
rs.append("Assessment found the following ").append(StringUtils.plural(errors, "error", "errors")).append(": \uD83D\uDE32\n");
for(String error : errors)
{
rs.append(" - ").append(error).append("\n");
}
}
/////////////////////////////////////
// print warnings if there are any //
/////////////////////////////////////
if(CollectionUtils.nullSafeHasContents(warnings))
{
rs.append("\nAssessment found the following ").append(StringUtils.plural(warnings, "warning", "warnings")).append(": \uD83E\uDD28\n");
for(String warning : warnings)
{
rs.append(" - ").append(warning).append("\n");
}
}
//////////////////////////////////////////
// print suggestions, if there were any //
//////////////////////////////////////////
if(CollectionUtils.nullSafeHasContents(suggestions))
{
rs.append("\nThe following ").append(StringUtils.plural(suggestions, "fix is", "fixes are")).append(" suggested: \uD83E\uDD13\n");
for(String suggestion : suggestions)
{
rs.append("\n").append(suggestion).append("\n\n");
}
}
return (rs.toString());
}
/*******************************************************************************
** Getter for qInstance
**
*******************************************************************************/
public QInstance getInstance()
{
return qInstance;
}
/*******************************************************************************
** Getter for errors
**
*******************************************************************************/
public List<String> getErrors()
{
return errors;
}
/*******************************************************************************
** Getter for warnings
**
*******************************************************************************/
public List<String> getWarnings()
{
return warnings;
}
/*******************************************************************************
**
*******************************************************************************/
public void addError(String errorMessage)
{
errors.add(errorMessage);
}
/*******************************************************************************
**
*******************************************************************************/
public void addWarning(String warningMessage)
{
warnings.add(warningMessage);
}
/*******************************************************************************
**
*******************************************************************************/
public void addError(String errorMessage, Exception e)
{
addError(errorMessage + " : " + e.getMessage());
}
/*******************************************************************************
**
*******************************************************************************/
public void addSuggestion(String message)
{
suggestions.add(message);
}
/*******************************************************************************
**
*******************************************************************************/
public int getExitCode()
{
if(CollectionUtils.nullSafeHasContents(errors))
{
return (1);
}
else
{
return (0);
}
}
}

View File

@ -0,0 +1,49 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. 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.enrichment.plugins;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
/*******************************************************************************
** Interface for additional / optional enrichment to be done on q instance members.
** Some may be provided by QQQ - others can be defined by applications.
*******************************************************************************/
public interface QInstanceEnricherPluginInterface<T>
{
/*******************************************************************************
**
*******************************************************************************/
void enrich(T object, QInstance qInstance);
/***************************************************************************
**
***************************************************************************/
default String getPluginIdentifier()
{
return getClass().getName();
}
}

View File

@ -0,0 +1,510 @@
/*
* 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.loaders;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import com.fasterxml.jackson.core.type.TypeReference;
import com.kingsrook.qqq.backend.core.instances.QMetaDataVariableInterpreter;
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.QMetaDataObject;
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import com.kingsrook.qqq.backend.core.utils.YamlUtils;
import org.apache.commons.io.IOUtils;
import static com.kingsrook.qqq.backend.core.utils.ValueUtils.getValueAsInteger;
import static com.kingsrook.qqq.backend.core.utils.ValueUtils.getValueAsString;
/*******************************************************************************
** Abstract base class in hierarchy of classes that know how to construct &
** populate QMetaDataObject instances, based on input streams (e.g., from files).
*******************************************************************************/
public abstract class AbstractMetaDataLoader<T extends QMetaDataObject>
{
private static final QLogger LOG = QLogger.getLogger(AbstractMetaDataLoader.class);
private String fileName;
private List<LoadingProblem> problems = new ArrayList<>();
/***************************************************************************
**
***************************************************************************/
public T fileToMetaDataObject(QInstance qInstance, InputStream inputStream, String fileName) throws QMetaDataLoaderException
{
this.fileName = fileName;
Map<String, Object> map = fileToMap(inputStream, fileName);
LoadingContext loadingContext = new LoadingContext(fileName, "/");
return (mapToMetaDataObject(qInstance, map, loadingContext));
}
/***************************************************************************
**
***************************************************************************/
public abstract T mapToMetaDataObject(QInstance qInstance, Map<String, Object> map, LoadingContext context) throws QMetaDataLoaderException;
/***************************************************************************
**
***************************************************************************/
protected Map<String, Object> fileToMap(InputStream inputStream, String fileName) throws QMetaDataLoaderException
{
try
{
String string = IOUtils.toString(inputStream, StandardCharsets.UTF_8);
string = StringUtils.ltrim(string);
if(fileName.toLowerCase().endsWith(".json"))
{
return JsonUtils.toObject(string, new TypeReference<>() {});
}
else if(fileName.toLowerCase().endsWith(".yaml") || fileName.toLowerCase().endsWith(".yml"))
{
return YamlUtils.toMap(string);
}
throw (new QMetaDataLoaderException("Unsupported file format (based on file name: " + fileName + ")"));
}
catch(IOException e)
{
throw new QMetaDataLoaderException("Error building map from file: " + fileName, e);
}
}
/***************************************************************************
*
***************************************************************************/
protected void reflectivelyMap(QInstance qInstance, QMetaDataObject targetObject, Map<String, Object> map, LoadingContext context)
{
Class<? extends QMetaDataObject> targetClass = targetObject.getClass();
Set<String> usedFieldNames = new HashSet<>();
for(Method method : targetClass.getMethods())
{
try
{
if(method.getName().startsWith("set") && method.getParameterTypes().length == 1)
{
String propertyName = StringUtils.lcFirst(method.getName().substring(3));
if(map.containsKey(propertyName))
{
usedFieldNames.add(propertyName);
Class<?> parameterType = method.getParameterTypes()[0];
Object rawValue = map.get(propertyName);
try
{
Object mappedValue = reflectivelyMapValue(qInstance, method, parameterType, rawValue, context.descendToProperty(propertyName));
method.invoke(targetObject, mappedValue);
}
catch(NoValueException nve)
{
///////////////////////
// don't call setter //
///////////////////////
LOG.debug("at " + context + ": No value was mapped for property [" + propertyName + "] on " + targetClass.getSimpleName() + "." + method.getName() + ", raw value: [" + rawValue + "]");
}
}
}
}
catch(Exception e)
{
addProblem(new LoadingProblem(context, "Error reflectively mapping on " + targetClass.getName() + "." + method.getName(), e));
}
}
//////////////////////////
// mmm, slightly sus... //
//////////////////////////
map.remove("class");
map.remove("version");
Set<String> unrecognizedKeys = new HashSet<>(map.keySet());
unrecognizedKeys.removeAll(usedFieldNames);
if(!unrecognizedKeys.isEmpty())
{
addProblem(new LoadingProblem(context, unrecognizedKeys.size() + " Unrecognized " + StringUtils.plural(unrecognizedKeys, "property", "properties") + ": " + unrecognizedKeys));
}
}
/***************************************************************************
*
***************************************************************************/
public Object reflectivelyMapValue(QInstance qInstance, Method method, Class<?> parameterType, Object rawValue, LoadingContext context) throws Exception
{
if(rawValue instanceof String s && s.matches("^\\$\\{.+\\..+}"))
{
rawValue = new QMetaDataVariableInterpreter().interpret(s);
LOG.debug("Interpreted raw value [" + s + "] as [" + StringUtils.maskAndTruncate(ValueUtils.getValueAsString(rawValue) + "]"));
}
if(parameterType.equals(String.class))
{
return (getValueAsString(rawValue));
}
else if(parameterType.equals(Integer.class))
{
try
{
return (getValueAsInteger(rawValue));
}
catch(Exception e)
{
addProblem(new LoadingProblem(context, "[" + rawValue + "] is not an Integer value."));
}
}
else if(parameterType.equals(Boolean.class))
{
if("true".equals(rawValue) || Boolean.TRUE.equals(rawValue))
{
return (true);
}
else if("false".equals(rawValue) || Boolean.FALSE.equals(rawValue))
{
return (false);
}
else if(rawValue == null)
{
return (null);
}
else
{
addProblem(new LoadingProblem(context, "[" + rawValue + "] is not a boolean value (must be 'true' or 'false')."));
return (null);
}
}
else if(parameterType.equals(boolean.class))
{
if("true".equals(rawValue) || Boolean.TRUE.equals(rawValue))
{
return (true);
}
else if("false".equals(rawValue) || Boolean.FALSE.equals(rawValue))
{
return (false);
}
else
{
addProblem(new LoadingProblem(context, rawValue + " is not a boolean value (must be 'true' or 'false')."));
throw (new NoValueException());
}
}
else if(parameterType.equals(List.class))
{
Type actualTypeArgument = ((ParameterizedType) method.getGenericParameterTypes()[0]).getActualTypeArguments()[0];
Class<?> actualTypeClass = Class.forName(actualTypeArgument.getTypeName());
if(rawValue instanceof @SuppressWarnings("rawtypes")List valueList)
{
List<Object> mappedValueList = new ArrayList<>();
for(Object o : valueList)
{
try
{
Object mappedValue = reflectivelyMapValue(qInstance, null, actualTypeClass, o, context);
mappedValueList.add(mappedValue);
}
catch(NoValueException nve)
{
// leave off list
}
}
return (mappedValueList);
}
}
else if(parameterType.equals(Set.class))
{
Type actualTypeArgument = ((ParameterizedType) method.getGenericParameterTypes()[0]).getActualTypeArguments()[0];
Class<?> actualTypeClass = Class.forName(actualTypeArgument.getTypeName());
if(rawValue instanceof @SuppressWarnings("rawtypes")List valueList)
{
Set<Object> mappedValueSet = new LinkedHashSet<>();
for(Object o : valueList)
{
try
{
Object mappedValue = reflectivelyMapValue(qInstance, null, actualTypeClass, o, context);
mappedValueSet.add(mappedValue);
}
catch(NoValueException nve)
{
// leave off list
}
}
return (mappedValueSet);
}
}
else if(parameterType.equals(Map.class))
{
Type keyType = ((ParameterizedType) method.getGenericParameterTypes()[0]).getActualTypeArguments()[0];
if(!keyType.equals(String.class))
{
addProblem(new LoadingProblem(context, "Unsupported key type for " + method + " got [" + keyType + "], expected [String]"));
throw new NoValueException();
}
// todo make sure string
Type actualTypeArgument = ((ParameterizedType) method.getGenericParameterTypes()[0]).getActualTypeArguments()[1];
Class<?> actualTypeClass = Class.forName(actualTypeArgument.getTypeName());
if(rawValue instanceof @SuppressWarnings("rawtypes")Map valueMap)
{
Map<String, Object> mappedValueMap = new LinkedHashMap<>();
for(Object o : valueMap.entrySet())
{
try
{
@SuppressWarnings("unchecked")
Map.Entry<String, Object> entry = (Map.Entry<String, Object>) o;
Object mappedValue = reflectivelyMapValue(qInstance, null, actualTypeClass, entry.getValue(), context);
mappedValueMap.put(entry.getKey(), mappedValue);
}
catch(NoValueException nve)
{
// leave out of map
}
}
return (mappedValueMap);
}
}
else if(parameterType.isEnum())
{
String value = getValueAsString(rawValue);
for(Object enumConstant : parameterType.getEnumConstants())
{
if(((Enum<?>) enumConstant).name().equals(value))
{
return (enumConstant);
}
}
addProblem(new LoadingProblem(context, "Unrecognized value [" + rawValue + "]. Expected one of: " + Arrays.toString(parameterType.getEnumConstants())));
}
else if(MetaDataLoaderRegistry.hasLoaderForClass(parameterType))
{
if(rawValue instanceof @SuppressWarnings("rawtypes")Map valueMap)
{
Class<? extends AbstractMetaDataLoader<?>> loaderClass = MetaDataLoaderRegistry.getLoaderForClass(parameterType);
AbstractMetaDataLoader<?> loader = loaderClass.getConstructor().newInstance();
//noinspection unchecked
return (loader.mapToMetaDataObject(qInstance, valueMap, context));
}
}
else if(QMetaDataObject.class.isAssignableFrom(parameterType))
{
if(rawValue instanceof @SuppressWarnings("rawtypes")Map valueMap)
{
QMetaDataObject childObject = (QMetaDataObject) parameterType.getConstructor().newInstance();
//noinspection unchecked
reflectivelyMap(qInstance, childObject, valueMap, context);
return (childObject);
}
}
else if(parameterType.equals(Serializable.class))
{
if(rawValue instanceof String
|| rawValue instanceof Integer
|| rawValue instanceof BigDecimal
|| rawValue instanceof Boolean
)
{
return rawValue;
}
}
else
{
// todo clean up this message/level
addProblem(new LoadingProblem(context, "No case for " + parameterType + " (arg to: " + method + ")"));
}
throw new NoValueException();
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// unclear if the below is needed. if so, useful to not re-write, but is hurting test coverage, so zombie until used //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
///***************************************************************************
// *
// ***************************************************************************/
//protected ListOfMapOrMapOfMap getListOfMapOrMapOfMap(Map<String, Object> map, String key)
//{
// if(map.containsKey(key))
// {
// if(map.get(key) instanceof List)
// {
// return (new ListOfMapOrMapOfMap((List<Map<String, Object>>) map.get(key)));
// }
// else if(map.get(key) instanceof Map)
// {
// return (new ListOfMapOrMapOfMap((Map<String, Map<String, Object>>) map.get(key)));
// }
// else
// {
// LOG.warn("Expected list or map under key [" + key + "] while processing [" + getClass().getSimpleName() + "] from [" + fileName + "], but found: " + (map.get(key) == null ? "null" : map.get(key).getClass().getSimpleName()));
// }
// }
// return (null);
//}
///***************************************************************************
// *
// ***************************************************************************/
//protected List<Map<String, Object>> getListOfMap(Map<String, Object> map, String key)
//{
// if(map.containsKey(key))
// {
// if(map.get(key) instanceof List)
// {
// return (List<Map<String, Object>>) map.get(key);
// }
// else
// {
// LOG.warn("Expected list under key [" + key + "] while processing [" + getClass().getSimpleName() + "] from [" + fileName + "], but found: " + (map.get(key) == null ? "null" : map.get(key).getClass().getSimpleName()));
// }
// }
// return (null);
//}
///***************************************************************************
// *
// ***************************************************************************/
//protected Map<String, Map<String, Object>> getMapOfMap(Map<String, Object> map, String key)
//{
// if(map.containsKey(key))
// {
// if(map.get(key) instanceof Map)
// {
// return (Map<String, Map<String, Object>>) map.get(key);
// }
// else
// {
// LOG.warn("Expected map under key [" + key + "] while processing [" + getClass().getSimpleName() + "] from [" + fileName + "], but found: " + (map.get(key) == null ? "null" : map.get(key).getClass().getSimpleName()));
// }
// }
// return (null);
//}
///***************************************************************************
// **
// ***************************************************************************/
//protected record ListOfMapOrMapOfMap(List<Map<String, Object>> listOf, Map<String, Map<String, Object>> mapOf)
//{
// /*******************************************************************************
// ** Constructor
// **
// *******************************************************************************/
// public ListOfMapOrMapOfMap(List<Map<String, Object>> listOf)
// {
// this(listOf, null);
// }
// /*******************************************************************************
// ** Constructor
// **
// *******************************************************************************/
// public ListOfMapOrMapOfMap(Map<String, Map<String, Object>> mapOf)
// {
// this(null, mapOf);
// }
//}
/*******************************************************************************
** Getter for fileName
**
*******************************************************************************/
public String getFileName()
{
return fileName;
}
/***************************************************************************
**
***************************************************************************/
private static class NoValueException extends Exception
{
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public NoValueException()
{
super("No value");
}
}
/***************************************************************************
**
***************************************************************************/
public void addProblem(LoadingProblem problem)
{
problems.add(problem);
}
/*******************************************************************************
** Getter for problems
**
*******************************************************************************/
public List<LoadingProblem> getProblems()
{
return (problems);
}
}

View File

@ -0,0 +1,120 @@
/*
* 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.loaders;
import java.io.InputStream;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import com.kingsrook.qqq.backend.core.instances.loaders.implementations.GenericMetaDataLoader;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.QMetaDataObject;
import com.kingsrook.qqq.backend.core.utils.ClassPathUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import com.kingsrook.qqq.backend.core.utils.memoization.AnyKey;
import com.kingsrook.qqq.backend.core.utils.memoization.Memoization;
/*******************************************************************************
** Generic implementation of AbstractMetaDataLoader, who "detects" the class
** of meta data object to be created, then defers to an appropriate subclass
** to do the work.
*******************************************************************************/
public class ClassDetectingMetaDataLoader extends AbstractMetaDataLoader<QMetaDataObject>
{
private static final Memoization<AnyKey, List<Class<?>>> memoizedMetaDataObjectClasses = new Memoization<>();
/***************************************************************************
*
***************************************************************************/
public AbstractMetaDataLoader<?> getLoaderForFile(InputStream inputStream, String fileName) throws QMetaDataLoaderException
{
Map<String, Object> map = fileToMap(inputStream, fileName);
return (getLoaderForMap(map));
}
/***************************************************************************
*
***************************************************************************/
public AbstractMetaDataLoader<?> getLoaderForMap(Map<String, Object> map) throws QMetaDataLoaderException
{
if(map.containsKey("class"))
{
String classProperty = ValueUtils.getValueAsString(map.get("class"));
try
{
if(MetaDataLoaderRegistry.hasLoaderForSimpleName(classProperty))
{
Class<? extends AbstractMetaDataLoader<?>> loaderClass = MetaDataLoaderRegistry.getLoaderForSimpleName(classProperty);
return (loaderClass.getConstructor().newInstance());
}
else
{
Optional<List<Class<?>>> metaDataClasses = memoizedMetaDataObjectClasses.getResult(AnyKey.getInstance(), k -> ClassPathUtils.getClassesContainingNameAndOfType("MetaData", QMetaDataObject.class));
if(metaDataClasses.isEmpty())
{
throw (new QMetaDataLoaderException("Could not get list of metaDataObjects from class loader"));
}
for(Class<?> c : metaDataClasses.get())
{
if(c.getSimpleName().equals(classProperty) && QMetaDataObject.class.isAssignableFrom(c))
{
@SuppressWarnings("unchecked")
Class<? extends QMetaDataObject> metaDataClass = (Class<? extends QMetaDataObject>) c;
return new GenericMetaDataLoader<>(metaDataClass);
}
}
}
throw new QMetaDataLoaderException("Unexpected class [" + classProperty + "] (not a QMetaDataObject; doesn't have a registered MetaDataLoader) specified in " + getFileName());
}
catch(QMetaDataLoaderException qmdle)
{
throw (qmdle);
}
catch(Exception e)
{
throw new QMetaDataLoaderException("Error handling class [" + classProperty + "] specified in " + getFileName(), e);
}
}
else
{
throw new QMetaDataLoaderException("Cannot detect meta-data type, because [class] attribute was not specified in file: " + getFileName());
}
}
/***************************************************************************
**
***************************************************************************/
@Override
public QMetaDataObject mapToMetaDataObject(QInstance qInstance, Map<String, Object> map, LoadingContext context) throws QMetaDataLoaderException
{
AbstractMetaDataLoader<?> loaderForMap = getLoaderForMap(map);
return loaderForMap.mapToMetaDataObject(qInstance, map, context);
}
}

View File

@ -0,0 +1,38 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. 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.loaders;
/*******************************************************************************
** Record to track where loader objects are - e.g., what file they're on,
** and at what property path within the file (e.g., helps report problems).
*******************************************************************************/
public record LoadingContext(String fileName, String propertyPath)
{
/***************************************************************************
**
***************************************************************************/
public LoadingContext descendToProperty(String propertyName)
{
return new LoadingContext(fileName, propertyPath + propertyName + "/");
}
}

View File

@ -0,0 +1,49 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. 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.loaders;
/*******************************************************************************
** record that tracks a problem that was encountered when loading files.
*******************************************************************************/
public record LoadingProblem(LoadingContext context, String message, Exception exception) // todo Level if useful
{
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public LoadingProblem(LoadingContext context, String message)
{
this(context, message, null);
}
/***************************************************************************
**
***************************************************************************/
@Override
public String toString()
{
return "at[" + context.fileName() + "][" + context.propertyPath() + "]: " + message;
}
}

View File

@ -0,0 +1,118 @@
/*
* 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.loaders;
import java.io.File;
import java.io.FileInputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import com.kingsrook.qqq.backend.core.exceptions.QException;
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.QMetaDataObject;
import com.kingsrook.qqq.backend.core.model.metadata.TopLevelMetaDataInterface;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.Pair;
/*******************************************************************************
** class that loads a directory full of meta data files into meta data objects,
** and then sets all of them in a QInstance.
*******************************************************************************/
public class MetaDataLoaderHelper
{
private static final QLogger LOG = QLogger.getLogger(MetaDataLoaderHelper.class);
/***************************************************************************
*
***************************************************************************/
public static void processAllMetaDataFilesInDirectory(QInstance qInstance, String path) throws QException
{
List<Pair<File, AbstractMetaDataLoader<?>>> loaders = new ArrayList<>();
File directory = new File(path);
processAllMetaDataFilesInDirectory(loaders, directory);
// todo - some version of sorting the loaders by type or possibly a sort field within the files (or file names)
for(Pair<File, AbstractMetaDataLoader<?>> pair : loaders)
{
File file = pair.getA();
AbstractMetaDataLoader<?> loader = pair.getB();
try(FileInputStream fileInputStream = new FileInputStream(file))
{
QMetaDataObject qMetaDataObject = loader.fileToMetaDataObject(qInstance, fileInputStream, file.getName());
if(CollectionUtils.nullSafeHasContents(loader.getProblems()))
{
loader.getProblems().forEach(System.out::println);
}
if(qMetaDataObject instanceof TopLevelMetaDataInterface topLevelMetaData)
{
topLevelMetaData.addSelfToInstance(qInstance);
}
else
{
LOG.warn("Received a non-topLevelMetaDataObject from file: " + file.getAbsolutePath());
}
}
catch(Exception e)
{
LOG.error("Error processing file: " + file.getAbsolutePath(), e);
}
}
}
/***************************************************************************
*
***************************************************************************/
private static void processAllMetaDataFilesInDirectory(List<Pair<File, AbstractMetaDataLoader<?>>> loaders, File directory) throws QException
{
for(File file : Objects.requireNonNullElse(directory.listFiles(), new File[0]))
{
if(file.isDirectory())
{
processAllMetaDataFilesInDirectory(loaders, file);
}
else
{
try(FileInputStream fileInputStream = new FileInputStream(file))
{
AbstractMetaDataLoader<?> loader = new ClassDetectingMetaDataLoader().getLoaderForFile(fileInputStream, file.getName());
loaders.add(Pair.of(file, loader));
}
catch(Exception e)
{
LOG.error("Error processing file: " + file.getAbsolutePath(), e);
}
}
}
}
}

View File

@ -0,0 +1,120 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. 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.loaders;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.instances.loaders.implementations.QTableMetaDataLoader;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.utils.ClassPathUtils;
/*******************************************************************************
**
*******************************************************************************/
public class MetaDataLoaderRegistry
{
private static final QLogger LOG = QLogger.getLogger(AbstractMetaDataLoader.class);
private static final Map<Class<?>, Class<? extends AbstractMetaDataLoader<?>>> registeredLoaders = new HashMap<>();
private static final Map<String, Class<? extends AbstractMetaDataLoader<?>>> registeredLoadersByTargetSimpleName = new HashMap<>();
static
{
try
{
List<Class<?>> classesInPackage = ClassPathUtils.getClassesInPackage(QTableMetaDataLoader.class.getPackageName());
for(Class<?> possibleLoaderClass : classesInPackage)
{
try
{
Type superClass = possibleLoaderClass.getGenericSuperclass();
if(superClass.getTypeName().startsWith(AbstractMetaDataLoader.class.getName() + "<"))
{
Type actualTypeArgument = ((ParameterizedType) superClass).getActualTypeArguments()[0];
if(actualTypeArgument instanceof Class)
{
//noinspection unchecked
Class<? extends AbstractMetaDataLoader<?>> loaderClass = (Class<? extends AbstractMetaDataLoader<?>>) possibleLoaderClass;
Class<?> metaDataObjectType = Class.forName(actualTypeArgument.getTypeName());
registeredLoaders.put(metaDataObjectType, loaderClass);
registeredLoadersByTargetSimpleName.put(metaDataObjectType.getSimpleName(), loaderClass);
}
}
}
catch(Exception e)
{
LOG.info("Error on class: " + possibleLoaderClass, e);
}
}
System.out.println("Registered loaders: " + registeredLoadersByTargetSimpleName);
}
catch(Exception e)
{
LOG.error("Error in static init block for MetaDataLoaderRegistry", e);
}
}
/***************************************************************************
**
***************************************************************************/
public static boolean hasLoaderForClass(Class<?> metaDataClass)
{
return registeredLoaders.containsKey(metaDataClass);
}
/***************************************************************************
**
***************************************************************************/
public static Class<? extends AbstractMetaDataLoader<?>> getLoaderForClass(Class<?> metaDataClass)
{
return registeredLoaders.get(metaDataClass);
}
/***************************************************************************
**
***************************************************************************/
public static boolean hasLoaderForSimpleName(String targetSimpleName)
{
return registeredLoadersByTargetSimpleName.containsKey(targetSimpleName);
}
/***************************************************************************
**
***************************************************************************/
public static Class<? extends AbstractMetaDataLoader<?>> getLoaderForSimpleName(String targetSimpleName)
{
return registeredLoadersByTargetSimpleName.get(targetSimpleName);
}
}

View File

@ -0,0 +1,50 @@
/*
* 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.loaders;
/*******************************************************************************
**
*******************************************************************************/
public class QMetaDataLoaderException extends Exception
{
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public QMetaDataLoaderException(String message)
{
super(message);
}
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public QMetaDataLoaderException(String message, Throwable cause)
{
super(message, cause);
}
}

View File

@ -0,0 +1,71 @@
/*
* 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.loaders.implementations;
import java.util.Map;
import com.kingsrook.qqq.backend.core.instances.loaders.AbstractMetaDataLoader;
import com.kingsrook.qqq.backend.core.instances.loaders.LoadingContext;
import com.kingsrook.qqq.backend.core.instances.loaders.QMetaDataLoaderException;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.QMetaDataObject;
/*******************************************************************************
**
*******************************************************************************/
public class GenericMetaDataLoader<T extends QMetaDataObject> extends AbstractMetaDataLoader<T>
{
private final Class<T> metaDataClass;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public GenericMetaDataLoader(Class<T> metaDataClass)
{
this.metaDataClass = metaDataClass;
}
/***************************************************************************
**
***************************************************************************/
@Override
public T mapToMetaDataObject(QInstance qInstance, Map<String, Object> map, LoadingContext context) throws QMetaDataLoaderException
{
try
{
T object = metaDataClass.getConstructor().newInstance();
reflectivelyMap(qInstance, object, map, context);
return (object);
}
catch(Exception e)
{
throw (new QMetaDataLoaderException("Error loading metaData object of type " + metaDataClass.getSimpleName(), e));
}
}
}

View File

@ -0,0 +1,85 @@
/*
* 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.loaders.implementations;
import java.util.Map;
import com.kingsrook.qqq.backend.core.instances.loaders.AbstractMetaDataLoader;
import com.kingsrook.qqq.backend.core.instances.loaders.LoadingContext;
import com.kingsrook.qqq.backend.core.instances.loaders.QMetaDataLoaderException;
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.processes.QBackendStepMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
/*******************************************************************************
**
*******************************************************************************/
public class QStepDataLoader extends AbstractMetaDataLoader<QStepMetaData>
{
private static final QLogger LOG = QLogger.getLogger(QStepDataLoader.class);
/***************************************************************************
**
***************************************************************************/
@Override
public QStepMetaData mapToMetaDataObject(QInstance qInstance, Map<String, Object> map, LoadingContext context) throws QMetaDataLoaderException
{
String stepType = ValueUtils.getValueAsString(map.get("stepType"));
if(!StringUtils.hasContent(stepType))
{
throw (new QMetaDataLoaderException("stepType was not specified for process step"));
}
QStepMetaData step;
if("backend".equalsIgnoreCase(stepType))
{
step = new QBackendStepMetaData();
reflectivelyMap(qInstance, step, map, context);
}
else if("frontend".equalsIgnoreCase(stepType))
{
step = new QFrontendStepMetaData();
reflectivelyMap(qInstance, step, map, context);
}
// todo - we have custom factory methods for this, so, maybe needs all custom loader?
// else if("stateMachine".equalsIgnoreCase(stepType))
// {
// step = new QStateMachineStep();
// reflectivelyMap(qInstance, step, map, context);
// }
else
{
throw (new QMetaDataLoaderException("Unsupported step stepType: " + stepType));
}
return (step);
}
}

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.instances.loaders.implementations;
import java.util.Map;
import com.kingsrook.qqq.backend.core.instances.loaders.AbstractMetaDataLoader;
import com.kingsrook.qqq.backend.core.instances.loaders.LoadingContext;
import com.kingsrook.qqq.backend.core.instances.loaders.QMetaDataLoaderException;
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.tables.QTableMetaData;
/*******************************************************************************
**
*******************************************************************************/
public class QTableMetaDataLoader extends AbstractMetaDataLoader<QTableMetaData>
{
private static final QLogger LOG = QLogger.getLogger(QTableMetaDataLoader.class);
/***************************************************************************
**
***************************************************************************/
@Override
public QTableMetaData mapToMetaDataObject(QInstance qInstance, Map<String, Object> map, LoadingContext context) throws QMetaDataLoaderException
{
QTableMetaData table = new QTableMetaData();
reflectivelyMap(qInstance, table, map, context);
// todo - handle QTableBackendDetails, based on backend's type
return (table);
}
}

View File

@ -38,4 +38,13 @@ public interface QInstanceValidatorPluginInterface<T>
*******************************************************************************/
void validate(T object, QInstance qInstance, QInstanceValidator qInstanceValidator);
/***************************************************************************
**
***************************************************************************/
default String getPluginIdentifier()
{
return getClass().getName();
}
}

View File

@ -25,6 +25,7 @@ package com.kingsrook.qqq.backend.core.model.actions.audits;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
@ -36,6 +37,8 @@ public class AuditInput extends AbstractActionInput implements Serializable
{
private List<AuditSingleInput> auditSingleInputList = new ArrayList<>();
private QBackendTransaction transaction;
/*******************************************************************************
@ -92,4 +95,42 @@ public class AuditInput extends AbstractActionInput implements Serializable
return (this);
}
/*******************************************************************************
* Getter for transaction
* @see #withTransaction(QBackendTransaction)
*******************************************************************************/
public QBackendTransaction getTransaction()
{
return (this.transaction);
}
/*******************************************************************************
* Setter for transaction
* @see #withTransaction(QBackendTransaction)
*******************************************************************************/
public void setTransaction(QBackendTransaction transaction)
{
this.transaction = transaction;
}
/*******************************************************************************
* Fluent setter for transaction
*
* @param transaction
* transaction upon which the audits will be inserted.
*
* @return this
*******************************************************************************/
public AuditInput withTransaction(QBackendTransaction transaction)
{
this.transaction = transaction;
return (this);
}
}

View File

@ -326,6 +326,20 @@ public class AuditSingleInput implements Serializable
/*******************************************************************************
** Fluent setter for details
*******************************************************************************/
public AuditSingleInput withDetailMessages(List<String> details)
{
for(String detail : details)
{
addDetail(message);
}
return (this);
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -24,6 +24,7 @@ package com.kingsrook.qqq.backend.core.model.actions.audits;
import java.io.Serializable;
import java.util.List;
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
@ -38,6 +39,8 @@ public class DMLAuditInput extends AbstractActionInput implements Serializable
private List<QRecord> oldRecordList;
private AbstractTableActionInput tableActionInput;
private QBackendTransaction transaction;
private String auditContext = null;
@ -164,4 +167,43 @@ public class DMLAuditInput extends AbstractActionInput implements Serializable
return (this);
}
/*******************************************************************************
* Getter for transaction
* @see #withTransaction(QBackendTransaction)
*******************************************************************************/
public QBackendTransaction getTransaction()
{
return (this.transaction);
}
/*******************************************************************************
* Setter for transaction
* @see #withTransaction(QBackendTransaction)
*******************************************************************************/
public void setTransaction(QBackendTransaction transaction)
{
this.transaction = transaction;
}
/*******************************************************************************
* Fluent setter for transaction
*
* @param transaction
* transaction that will be used for inserting the audits, where (presumably)
* the DML against the record occurred as well
*
* @return this
*******************************************************************************/
public DMLAuditInput withTransaction(QBackendTransaction transaction)
{
this.transaction = transaction;
return (this);
}
}

View File

@ -25,6 +25,7 @@ package com.kingsrook.qqq.backend.core.model.actions.metadata;
import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput;
import com.kingsrook.qqq.backend.core.model.metadata.QSupplementalInstanceMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.branding.QBrandingMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.frontend.AppTreeNode;
import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendAppMetaData;
@ -41,12 +42,13 @@ import com.kingsrook.qqq.backend.core.model.metadata.help.QHelpContent;
*******************************************************************************/
public class MetaDataOutput extends AbstractActionOutput
{
private Map<String, QFrontendTableMetaData> tables;
private Map<String, QFrontendProcessMetaData> processes;
private Map<String, QFrontendReportMetaData> reports;
private Map<String, QFrontendAppMetaData> apps;
private Map<String, QFrontendWidgetMetaData> widgets;
private Map<String, String> environmentValues;
private Map<String, QFrontendTableMetaData> tables;
private Map<String, QFrontendProcessMetaData> processes;
private Map<String, QFrontendReportMetaData> reports;
private Map<String, QFrontendAppMetaData> apps;
private Map<String, QFrontendWidgetMetaData> widgets;
private Map<String, String> environmentValues;
private Map<String, QSupplementalInstanceMetaData> supplementalInstanceMetaData;
private List<AppTreeNode> appTree;
private QBrandingMetaData branding;
@ -230,6 +232,28 @@ public class MetaDataOutput extends AbstractActionOutput
/*******************************************************************************
** Getter for supplementalInstanceMetaData
**
*******************************************************************************/
public Map<String, QSupplementalInstanceMetaData> getSupplementalInstanceMetaData()
{
return supplementalInstanceMetaData;
}
/*******************************************************************************
** Setter for supplementalInstanceMetaData
**
*******************************************************************************/
public void setSupplementalInstanceMetaData(Map<String, QSupplementalInstanceMetaData> supplementalInstanceMetaData)
{
this.supplementalInstanceMetaData = supplementalInstanceMetaData;
}
/*******************************************************************************
** Setter for helpContents
**

View File

@ -40,6 +40,8 @@ public class ProcessState implements Serializable
private Map<String, Serializable> values = new HashMap<>();
private List<String> stepList = new ArrayList<>();
private Optional<String> nextStepName = Optional.empty();
private Optional<String> backStepName = Optional.empty();
private boolean isStepBack = false;
private ProcessMetaDataAdjustment processMetaDataAdjustment = null;
@ -122,6 +124,39 @@ public class ProcessState implements Serializable
/*******************************************************************************
** Getter for backStepName
**
*******************************************************************************/
public Optional<String> getBackStepName()
{
return backStepName;
}
/*******************************************************************************
** Setter for backStepName
**
*******************************************************************************/
public void setBackStepName(String backStepName)
{
this.backStepName = Optional.of(backStepName);
}
/*******************************************************************************
** clear out the value of backStepName (set the Optional to empty)
**
*******************************************************************************/
public void clearBackStepName()
{
this.backStepName = Optional.empty();
}
/*******************************************************************************
** Getter for stepList
**
@ -176,4 +211,35 @@ public class ProcessState implements Serializable
}
/*******************************************************************************
** Getter for isStepBack
*******************************************************************************/
public boolean getIsStepBack()
{
return (this.isStepBack);
}
/*******************************************************************************
** Setter for isStepBack
*******************************************************************************/
public void setIsStepBack(boolean isStepBack)
{
this.isStepBack = isStepBack;
}
/*******************************************************************************
** Fluent setter for isStepBack
*******************************************************************************/
public ProcessState withIsStepBack(boolean isStepBack)
{
this.isStepBack = isStepBack;
return (this);
}
}

View File

@ -53,6 +53,7 @@ public class ProcessSummaryLine implements ProcessSummaryLineInterface
//////////////////////////////////////////////////////////////////////////
private ArrayList<Serializable> primaryKeys;
private ArrayList<String> bulletsOfText;
/*******************************************************************************
@ -497,4 +498,35 @@ public class ProcessSummaryLine implements ProcessSummaryLineInterface
return (this);
}
/*******************************************************************************
** Getter for bulletsOfText
*******************************************************************************/
public ArrayList<String> getBulletsOfText()
{
return (this.bulletsOfText);
}
/*******************************************************************************
** Setter for bulletsOfText
*******************************************************************************/
public void setBulletsOfText(ArrayList<String> bulletsOfText)
{
this.bulletsOfText = bulletsOfText;
}
/*******************************************************************************
** Fluent setter for bulletsOfText
*******************************************************************************/
public ProcessSummaryLine withBulletsOfText(ArrayList<String> bulletsOfText)
{
this.bulletsOfText = bulletsOfText;
return (this);
}
}

View File

@ -23,7 +23,11 @@ package com.kingsrook.qqq.backend.core.model.actions.processes;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import com.kingsrook.qqq.backend.core.logging.LogPair;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
@ -31,6 +35,45 @@ import com.kingsrook.qqq.backend.core.logging.LogPair;
*******************************************************************************/
public interface ProcessSummaryLineInterface extends Serializable
{
QLogger LOG = QLogger.getLogger(ProcessSummaryLineInterface.class);
/***************************************************************************
**
***************************************************************************/
static void log(String message, Serializable summaryLines, List<LogPair> additionalLogPairs)
{
try
{
if(summaryLines instanceof List)
{
List<ProcessSummaryLineInterface> list = (List<ProcessSummaryLineInterface>) summaryLines;
List<LogPair> logPairs = new ArrayList<>();
for(ProcessSummaryLineInterface processSummaryLineInterface : list)
{
LogPair logPair = processSummaryLineInterface.toLogPair();
logPair.setKey(logPair.getKey() + logPairs.size());
logPairs.add(logPair);
}
if(additionalLogPairs != null)
{
logPairs.addAll(0, additionalLogPairs);
}
logPairs.add(0, logPair("message", message));
LOG.info(logPairs);
}
else
{
LOG.info("Unrecognized type for summaryLines (expected List)", logPair("processSummary", summaryLines));
}
}
catch(Exception e)
{
LOG.info("Error logging a process summary", e, logPair("processSummary", summaryLines));
}
}
/*******************************************************************************
** Getter for status

View File

@ -0,0 +1,162 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. 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.io.Serializable;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
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.data.QRecordEntityField;
import com.kingsrook.qqq.backend.core.utils.ListingHash;
import com.kingsrook.qqq.backend.core.utils.ReflectiveBeanLikeClassUtils;
/*******************************************************************************
** base-class for bean-like classes to represent the fields of a process.
** similar in spirit to QRecordEntity, but for processes.
*******************************************************************************/
public class QProcessPayload
{
private static final QLogger LOG = QLogger.getLogger(QProcessPayload.class);
private static final ListingHash<Class<? extends QProcessPayload>, QRecordEntityField> fieldMapping = new ListingHash<>();
/*******************************************************************************
** Build an entity of this QRecord type from a QRecord
**
*******************************************************************************/
public static <T extends QProcessPayload> T fromProcessState(Class<T> c, ProcessState processState) throws QException
{
try
{
T entity = c.getConstructor().newInstance();
entity.populateFromProcessState(processState);
return (entity);
}
catch(Exception e)
{
throw (new QException("Error building process payload from state.", e));
}
}
/***************************************************************************
**
***************************************************************************/
protected void populateFromProcessState(ProcessState processState)
{
try
{
List<QRecordEntityField> fieldList = getFieldList(this.getClass());
for(QRecordEntityField qRecordEntityField : fieldList)
{
Serializable value = processState.getValues().get(qRecordEntityField.getFieldName());
Object typedValue = qRecordEntityField.convertValueType(value);
qRecordEntityField.getSetter().invoke(this, typedValue);
}
}
catch(Exception e)
{
throw (new QRuntimeException("Error building process payload from process state.", e));
}
}
/*******************************************************************************
** Copy the values from this payload into the given process state.
** ALL fields in the entity will be set in the process state.
**
*******************************************************************************/
public void toProcessState(ProcessState processState) throws QRuntimeException
{
try
{
for(QRecordEntityField qRecordEntityField : getFieldList(this.getClass()))
{
processState.getValues().put(qRecordEntityField.getFieldName(), (Serializable) qRecordEntityField.getGetter().invoke(this));
}
}
catch(Exception e)
{
throw (new QRuntimeException("Error populating process state from process payload.", e));
}
}
/***************************************************************************
*
***************************************************************************/
public static Set<Class<?>> allowedFieldTypes()
{
HashSet<Class<?>> classes = new HashSet<>(ReflectiveBeanLikeClassUtils.defaultAllowedTypes());
classes.add(Map.class);
classes.add(List.class);
return (classes);
}
/*******************************************************************************
**
*******************************************************************************/
public static List<QRecordEntityField> getFieldList(Class<? extends QProcessPayload> c)
{
if(!fieldMapping.containsKey(c))
{
List<QRecordEntityField> fieldList = new ArrayList<>();
for(Method possibleGetter : c.getMethods())
{
if(ReflectiveBeanLikeClassUtils.isGetter(possibleGetter, false, allowedFieldTypes()))
{
Optional<Method> setter = ReflectiveBeanLikeClassUtils.getSetterForGetter(c, possibleGetter);
if(setter.isPresent())
{
String fieldName = ReflectiveBeanLikeClassUtils.getFieldNameFromGetter(possibleGetter);
fieldList.add(new QRecordEntityField(fieldName, possibleGetter, setter.get(), possibleGetter.getReturnType(), null));
}
else
{
LOG.debug("Getter method [" + possibleGetter.getName() + "] does not have a corresponding setter.");
}
}
}
fieldMapping.put(c, fieldList);
}
return (fieldMapping.get(c));
}
}

View File

@ -25,19 +25,28 @@ package com.kingsrook.qqq.backend.core.model.actions.processes;
import java.io.Serializable;
import java.time.Instant;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import com.kingsrook.qqq.backend.core.actions.async.AsyncJobCallback;
import com.kingsrook.qqq.backend.core.actions.async.AsyncJobStatus;
import com.kingsrook.qqq.backend.core.actions.async.NonPersistedAsyncJobCallback;
import com.kingsrook.qqq.backend.core.actions.processes.QProcessCallback;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
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.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.processes.tracing.ProcessTracerInterface;
import com.kingsrook.qqq.backend.core.processes.tracing.ProcessTracerMessage;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
@ -46,6 +55,8 @@ import com.kingsrook.qqq.backend.core.utils.ValueUtils;
*******************************************************************************/
public class RunBackendStepInput extends AbstractActionInput
{
private static final QLogger LOG = QLogger.getLogger(RunBackendStepInput.class);
private ProcessState processState;
private String processName;
private String tableName;
@ -55,12 +66,13 @@ public class RunBackendStepInput extends AbstractActionInput
private RunProcessInput.FrontendStepBehavior frontendStepBehavior;
private Instant basepullLastRunTime;
private ProcessTracerInterface processTracer;
////////////////////////////////////////////////////////////////////////////
// note - new fields should generally be added in method: cloneFieldsInto //
////////////////////////////////////////////////////////////////////////////
/*******************************************************************************
**
*******************************************************************************/
@ -96,6 +108,7 @@ public class RunBackendStepInput extends AbstractActionInput
target.setAsyncJobCallback(getAsyncJobCallback());
target.setFrontendStepBehavior(getFrontendStepBehavior());
target.setValues(getValues());
target.setProcessTracer(getProcessTracer().orElse(null));
}
@ -238,6 +251,26 @@ public class RunBackendStepInput extends AbstractActionInput
/*******************************************************************************
** Getter for records converted to entities of a given type.
**
*******************************************************************************/
public <E extends QRecordEntity> List<E> getRecordsAsEntities(Class<E> entityClass) throws QException
{
List<E> rs = new ArrayList<>();
///////////////////////////////////////////////////////////////////////////////////
// note - important to call getRecords here, which is overwritten in subclasses! //
///////////////////////////////////////////////////////////////////////////////////
for(QRecord record : getRecords())
{
rs.add(QRecordEntity.fromQRecord(entityClass, record));
}
return (rs);
}
/*******************************************************************************
** Setter for records
**
@ -419,6 +452,17 @@ public class RunBackendStepInput extends AbstractActionInput
/*******************************************************************************
** Accessor for processState's isStepBack attribute
**
*******************************************************************************/
public boolean getIsStepBack()
{
return processState.getIsStepBack();
}
/*******************************************************************************
** Accessor for processState - protected, because we generally want to access
** its members through wrapper methods, we think
@ -524,4 +568,75 @@ public class RunBackendStepInput extends AbstractActionInput
return (this);
}
/*******************************************************************************
** Setter for processTracer
*******************************************************************************/
public void setProcessTracer(ProcessTracerInterface processTracer)
{
this.processTracer = processTracer;
}
/*******************************************************************************
** Fluent setter for processTracer
*******************************************************************************/
public RunBackendStepInput withProcessTracer(ProcessTracerInterface processTracer)
{
this.processTracer = processTracer;
return (this);
}
/***************************************************************************
**
***************************************************************************/
public Optional<ProcessTracerInterface> getProcessTracer()
{
return Optional.ofNullable(processTracer);
}
/***************************************************************************
**
***************************************************************************/
public void traceMessage(ProcessTracerMessage message)
{
if(processTracer != null && message != null)
{
try
{
processTracer.handleMessage(this, message);
}
catch(Exception e)
{
LOG.warn("Error tracing message", e, logPair("message", message));
}
}
}
/***************************************************************************
**
***************************************************************************/
public QProcessMetaData getProcess()
{
return (QContext.getQInstance().getProcess(getProcessName()));
}
/***************************************************************************
** return a QProcessPayload subclass instance, with values populated from
** the current process state.
***************************************************************************/
public <T extends QProcessPayload> T getProcessPayload(Class<T> payloadClass) throws QException
{
return QProcessPayload.fromProcessState(payloadClass, getProcessState());
}
}

View File

@ -33,6 +33,7 @@ import com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput;
import com.kingsrook.qqq.backend.core.model.actions.audits.AuditInput;
import com.kingsrook.qqq.backend.core.model.actions.audits.AuditSingleInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
@ -258,7 +259,7 @@ public class RunBackendStepOutput extends AbstractActionOutput implements Serial
/*******************************************************************************
**
** add a record to the step output, e.g., for going through to the next step.
*******************************************************************************/
public void addRecord(QRecord record)
{
@ -271,6 +272,16 @@ public class RunBackendStepOutput extends AbstractActionOutput implements Serial
/***************************************************************************
** add a RecordEntity to the step output, e.g., for going through to the next step.
***************************************************************************/
public void addRecordEntity(QRecordEntity recordEntity)
{
addRecord(recordEntity.toQRecord());
}
/*******************************************************************************
** Getter for auditInputList
*******************************************************************************/
@ -434,4 +445,14 @@ public class RunBackendStepOutput extends AbstractActionOutput implements Serial
this.processState.setProcessMetaDataAdjustment(processMetaDataAdjustment);
}
/***************************************************************************
** Update the process state with values from the input processPayload
** subclass instance.
***************************************************************************/
public void setProcessPayload(QProcessPayload processPayload)
{
processPayload.toProcessState(getProcessState());
}
}

View File

@ -49,6 +49,7 @@ public class RunProcessInput extends AbstractActionInput
private ProcessState processState;
private FrontendStepBehavior frontendStepBehavior = FrontendStepBehavior.BREAK;
private String startAfterStep;
private String startAtStep;
private String processUUID;
private AsyncJobCallback asyncJobCallback;
@ -451,4 +452,35 @@ public class RunProcessInput extends AbstractActionInput
{
return asyncJobCallback;
}
/*******************************************************************************
** Getter for startAtStep
*******************************************************************************/
public String getStartAtStep()
{
return (this.startAtStep);
}
/*******************************************************************************
** Setter for startAtStep
*******************************************************************************/
public void setStartAtStep(String startAtStep)
{
this.startAtStep = startAtStep;
}
/*******************************************************************************
** Fluent setter for startAtStep
*******************************************************************************/
public RunProcessInput withStartAtStep(String startAtStep)
{
this.startAtStep = startAtStep;
return (this);
}
}

View File

@ -28,6 +28,7 @@ import java.util.Map;
import java.util.function.Supplier;
import com.kingsrook.qqq.backend.core.actions.reporting.ExportStreamerInterface;
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData;
@ -44,6 +45,7 @@ public class ReportInput extends AbstractTableActionInput
private ReportDestination reportDestination;
private Supplier<? extends ExportStreamerInterface> overrideExportStreamerSupplier;
private QCodeReference exportStyleCustomizer;
@ -208,4 +210,35 @@ public class ReportInput extends AbstractTableActionInput
return (this);
}
/*******************************************************************************
** Getter for exportStyleCustomizer
*******************************************************************************/
public QCodeReference getExportStyleCustomizer()
{
return (this.exportStyleCustomizer);
}
/*******************************************************************************
** Setter for exportStyleCustomizer
*******************************************************************************/
public void setExportStyleCustomizer(QCodeReference exportStyleCustomizer)
{
this.exportStyleCustomizer = exportStyleCustomizer;
}
/*******************************************************************************
** Fluent setter for exportStyleCustomizer
*******************************************************************************/
public ReportInput withExportStyleCustomizer(QCodeReference exportStyleCustomizer)
{
this.exportStyleCustomizer = exportStyleCustomizer;
return (this);
}
}

View File

@ -0,0 +1,35 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. 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;
import java.io.Serializable;
/*******************************************************************************
** interface to mark enums (presumably classes too, but the original intent is
** enums) that can be added to insert/update/delete action inputs to flag behaviors
*******************************************************************************/
public interface ActionFlag extends Serializable
{
}

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