Compare commits

..

107 Commits

Author SHA1 Message Date
04103281af Fix iteration over form params (changed w/ addition of associations) - to handle empty values list 2024-03-18 15:11:46 -05:00
2088c5dab3 Fixing tests 2024-03-18 15:03:42 -05:00
59a4ad7de6 Checkstyle 2024-03-18 12:49:26 -05:00
7b457b4936 Initial checkin 2024-03-18 12:29:17 -05:00
d1e4091eb4 CE-936 - Add support for managing associations on insert/edit screens, via childRecordList widget 2024-03-18 12:28:23 -05:00
753c224196 CE-936 - Refactored, for more generic handling of job types
- Introduced SchedulableType, SchedulableRunner, SchedulableIdentity classes
- removed job-type specific methods from QScheduleManager and QSchedulerInterface
- Add scheduler-level management processes
- Change quartz to not change schedules during service startup
- re-added repeatSeconds to ScheduledJob
2024-03-18 12:27:22 -05:00
0130e34112 Change withTemporaryContext to take UnsafeVoidVoidMethod instead of VoidVoidMethod 2024-03-18 10:57:58 -05:00
5ed21d1fed Add memoization of constructors in getAdHoc (micro optimization) 2024-03-18 10:57:58 -05:00
c5e381abdb Add toString 2024-03-18 10:57:58 -05:00
1bffd4d46e CE-936 - Move process variant info from schedule to process meta data 2024-03-18 10:57:58 -05:00
c093c680c0 Work-around default delay when scheduling, for test 2024-03-14 11:33:28 -05:00
aa69f0e7d7 Checkstyle 2024-03-14 11:27:33 -05:00
2fec4891d3 CE-936 - dynamic scheduling of records from ScheduledJob table; quartz re-pause if paused before rescheduling 2024-03-14 11:24:21 -05:00
60ffac4646 CE-936 - add reviewStepRecordFields to these processes 2024-03-14 11:24:21 -05:00
2a97598309 Put icons on these apps 2024-03-14 11:24:21 -05:00
1a1ebcbe02 Add constructor that takes field 2024-03-14 11:24:21 -05:00
5f72978528 Refactor table customizers to follow a common interface (point being, so you can have 1 class instead of many for a table's closely-related actions) 2024-03-14 11:24:21 -05:00
3265d6d842 CE-936 - scheduling updates:
- move queues & automations to be scheduled (only) at the lower-level (per-queue, per-table) - not at the higher "provider" levels.
- update quartz to delete jobs which are no-longer active, at end of QScheduleManager's setup
-
2024-03-13 12:08:13 -05:00
246984892a Don't include not-protected widgets in available-permissions list 2024-03-13 12:05:13 -05:00
03b93658d5 CE-936 - Add test on RDBMSTableMetaDataBuilder (and support in it for h2, and bug-fix to use fieldName, not columnName, for primaryKey) 2024-03-12 18:26:47 -05:00
b093ff5ece CE-936 - Test coverage on quartz code 2024-03-12 15:47:15 -05:00
7181643abb CE-936 - Move log4j-slf4j-impl from mongo to core 2024-03-12 15:46:15 -05:00
178b902665 Merged dev into feature/quartz-scheduler 2024-03-12 14:44:15 -05:00
d6d0e910ec Merge pull request #75 from Kingsrook/feature/collecting-logger
Add new "Collecting Logger", that can be activated to "collect" logs …
2024-03-12 14:42:59 -05:00
d09b12ca5b Add unInit, to fix leaked state between tests. 2024-03-12 14:05:03 -05:00
2c4fc6a0d4 CE-936 - Update start method to actually start schedulers; add stop & stopAsync 2024-03-12 13:49:10 -05:00
11d33c6d06 CE-936 - Make test pass (by starting simple scheduler via QScheduleManager) 2024-03-12 13:39:12 -05:00
3684101259 CE-936 - Fix anti-merge commit from previous 2024-03-12 12:09:24 -05:00
c054bf5ddd CE-936 - Fix missing javadoc 2024-03-12 12:08:41 -05:00
7155180a76 CE-936 - Checkpoint on Quartz scheduler implementation.
- Add QSchedulerMetaData as new type of top-level meta data
- Move existing ScheduleManager to be SimpleScheduler, an instance of new QSchedulerInterface
- Update QuartzScheduler to implement new QSchedulerInterface, plus:
-- support cron schedules
-- handle parallel variant jobs
-- handle automations & sqs pollers
2024-03-12 11:55:14 -05:00
dabaafa482 CE-936 - Update setBlobValuesToDownloadUrls to only do this if the file has a fileDownload adornment 2024-03-12 11:53:13 -05:00
a4cdbc429d CE-936 - Add method inferNameFromBackendName 2024-03-12 11:51:29 -05:00
03f1fc1436 CE-936 - Add method withTemporaryContext 2024-03-12 11:51:10 -05:00
d551ad71a6 CE-936 - Remove slf4j-api (originally added when quartz was added) 2024-03-12 11:50:43 -05:00
891c567a8d CE-936 - update getAllAvailablePermissions, for table permissions, to only include them based on table capabilities 2024-03-12 11:50:43 -05:00
3e604f4b6f CE-936 - Update for camelCase quartz table names 2024-03-12 11:50:43 -05:00
5564e94ad7 CE-936 - Set c3p0 (com.mchange) and quartz to INFO level 2024-03-12 11:50:43 -05:00
58c15e6eaa CE-936 - Update to not re-do post-actions if using the defaultGetInterface (which does a query) 2024-03-12 11:50:43 -05:00
f448cff5dd CE-936 - add setting to useCamelCaseNames 2024-03-12 11:50:43 -05:00
2e1bf399f9 CE-936 - Add methods getJdbcDriverClassName, getJdbcUrl 2024-03-12 11:50:43 -05:00
621997efd9 CE-936 - Add alternate constructor that takes just QInstance and QSession 2024-03-12 11:50:43 -05:00
87ef20aff4 CE-936 - Wrap sets w/ HashSets, for mutability. 2024-03-12 11:50:43 -05:00
12eb1804ad CE-936 - Add getName to TopLevelMetaDataInterface 2024-03-12 11:50:43 -05:00
51945aa844 Initial checkin 2024-03-08 16:49:00 -06:00
1ee4f67286 Initial quartz scheduler and processes 2024-03-08 16:49:00 -06:00
3c2a34291a Refactor, moving methods into SchedulerUtils, for use by other schedulers 2024-03-08 16:48:57 -06:00
949d9cd088 put everything back around mvn git-flow stuff 2024-03-08 13:01:59 -06:00
acc7abf5c4 Randomly trying to update git flow version 2024-03-08 12:30:09 -06:00
5d24780c47 updates to fix 'end-of-sprint-release.sh' 2024-03-08 12:19:07 -06:00
cf92e20315 updates to fix 'end-of-sprint-release.sh' 2024-03-08 12:05:25 -06:00
4f0cba88e6 updates to fix 'end-of-sprint-release.sh' 2024-03-08 12:00:42 -06:00
2b31c7957f Merge pull request #76 from Kingsrook/feature/q-instance-validator-plugins
Add plugin support in QInstanceValidator, with first implementation b…
2024-03-08 10:54:49 -06:00
35d6e23157 CE-847 - Fix to not duplicate output records (wasn't duplicating the work, just the result screen in UI), with test to assert it. 2024-03-08 09:56:25 -06:00
9de1f6924a Change log message to include countByStatus map 2024-03-07 19:41:56 -06:00
865c1cae20 Add plugin support in QInstanceValidator, with first implementation being BasepullExtractStepValidator 2024-03-07 19:40:58 -06:00
21b2e5ffc0 Add *.html to .gitignore here - to avoid committing rendered files 2024-03-07 11:25:39 -06:00
7285efe656 Add some asciidocs that haven't been previously committed 2024-03-07 11:22:40 -06:00
1a23f5f7e9 Add new "Collecting Logger", that can be activated to "collect" logs (e.g., in an internal list), for later access (e.g., by a test, to assert if things were logged). 2024-03-07 11:19:30 -06:00
5f0fe9ce27 Merged dev into feature/quartz-scheduler 2024-03-06 19:12:39 -06:00
7edcb61b4d added utility method for loading a table that takes in a filter 2024-03-06 13:01:22 -06:00
7b9f659afa Add blockId string member 2024-03-05 14:33:10 -06:00
44b75b6437 Merge pull request #74 from Kingsrook/feature/allow-omit-modify-date-updates
updates to allow update input to omit updating the modify date behavior
2024-03-05 14:29:08 -06:00
b8330bd2f0 changed audit failures from debugs to warnings 2024-03-05 14:24:19 -06:00
83ba490eb9 downgraded expected audit failures to debug 2024-03-05 13:59:50 -06:00
3d9a6fa249 updates to allow update input to omit updating the modify date behavior 2024-03-05 12:14:45 -06:00
e47b2c9497 Merge pull request #71 from Kingsrook/feature/CE-889-bug-orders-sent-to-wms-with
CE-889 - improvements for streaming etl pipes:
2024-03-04 11:55:39 -06:00
fdc948f96c Merge pull request #70 from Kingsrook/feature/CE-940-rollo-optimization-to-use-tnt
CE-940 Add AuditDetailAccumulator, and a means to share it (generical…
2024-03-04 11:46:50 -06:00
59ca1e3a1c Merge pull request #68 from Kingsrook/feature/heal-automations-updates
CE-847 - Add review screen to HealBadRecordAutomationStatusesProcess;…
2024-03-04 11:46:38 -06:00
52d7d0e8ae Merge pull request #73 from Kingsrook/hotfix/file-importer
Increasing logging for s3 importer archive errors
2024-03-04 11:00:11 -06:00
d1792dde11 Merge pull request #72 from Kingsrook/feature/CE-878-make-the-operations-dashboard
Feature/ce 878 make the operations dashboard
2024-03-04 10:58:36 -06:00
b0aaf61e99 CE-940 Add AuditDetailAccumulator, and a means to share it (generically) via QContext 2024-03-04 07:52:47 -06:00
66f9d1b500 CE-889 - improvements for streaming etl pipes:
allow StreamedETLWithFrontendProcess pipe capacity to come from field 'recordPipeCapacity';  also use either field-based on transform-step-based pipe capacity in Validate step as well as Execute step;

in AsyncRecordPipeLoop, if pipe capacity is less than minRecordsToConsume, then set minRecordsToConsume down to pipe capacity.

change AbstractLoadStep and AbstractTransformStep for StreamedETLWithFrontendProcesses to no implement BackendStep, and as such to (eventually) require a runOnePage method, rather than run (run marked as @Deprecated until apps can migrate);
2024-03-03 16:45:28 -06:00
dd103d323d CE-878: added flag to not do deletes upon replace action 2024-03-01 07:19:57 -06:00
238521aa57 CE-878: added sublabel to widget data 2024-02-29 15:33:26 -06:00
8056e49e24 Merged feature/CE-878-make-the-operations-dashboard into dev 2024-02-29 09:20:30 -06:00
2bdb5911e7 Merge pull request #69 from Kingsrook/feature/CE-937-clean-up-customers-and
Feature/ce 937 clean up customers and
2024-02-28 16:48:12 -06:00
9680812828 Merge branch 'dev' into feature/CE-878-make-the-operations-dashboard 2024-02-28 11:54:21 -06:00
049f5a6b42 CE-878: attempt to fix log4j dependencies 2024-02-28 10:55:17 -06:00
15c0d6a45a CE-937 Somehow better comments than "// hmm..." :) 2024-02-27 20:03:04 -06:00
6919d9d810 CE-878 add helpContent to widgetMetaData 2024-02-27 19:39:46 -06:00
cdf59e8f2b CE-847 - Add review screen to HealBadRecordAutomationStatusesProcess; update to query by createDate for pending-inserts 2024-02-27 10:09:00 -06:00
217acc954a Merge branch 'feature/CE-876-develop-missing-widget-types' into feature/CE-878-make-the-operations-dashboard 2024-02-21 11:29:00 -06:00
85ac2c10e7 CE-781 Fix s3 delete to handle slashes it doesn't want 2024-02-21 10:46:03 -06:00
4609ff8976 CE-875 - update qqq-fmd to build from this story; set some widgets to use MaterialDashboardIconRoleNames.TOP_RIGHT_INSIDE_CARD 2024-02-20 21:47:20 -06:00
bf11ea5185 Merged dev into feature/CE-876-develop-missing-widget-types 2024-02-20 20:12:55 -06:00
01d96cc8ae Increasing logging for s3 importer archive errors 2024-02-20 18:24:48 -06:00
a6d84c66b7 Skip quicksights; missing credentials 2024-02-20 18:19:38 -06:00
69015be2f6 Add test coverage for sample widgets 2024-02-20 18:12:11 -06:00
0f16446f69 Change to use TOP_RIGHT_INSIDE_CARD from QFMD, since LEFT doesn't exist (yet!) 2024-02-20 17:07:49 -06:00
9464a308f2 Fix test per new widget/instance validation 2024-02-20 17:02:11 -06:00
51f021193d Add dep on qqq-frontend-material-dashboard - pinned back to 0.20.0-20240219.210012-18 2024-02-20 16:57:36 -06:00
0090601166 Build out samples of widgets 2024-02-20 16:56:23 -06:00
cf78f1cb94 Wrap IOException in QException 2024-02-20 16:55:43 -06:00
0b1b570f28 CE-876 New dashboard widgets (more or less):
- New Composite & Block widget constructs.
- Option for a parent widget's label to be the app home page's label (supported by app-level supplemental meta data)
- Option for widgets to have CSV Data that can be exported differently from just the data "in" the widget.
-- This included changing the default value for showExportButton from true to false
2024-02-20 16:55:08 -06:00
73e54dd56f Avoid NPE on orderBys in toString 2024-02-20 16:53:43 -06:00
e936468f29 Add validation check for widget names used within apps 2024-02-20 16:53:24 -06:00
0dd97d9dc1 Add overload that lets caller customize the jackson object mapper 2023-12-22 19:12:37 -06:00
b1e68017cc Fix warn message to have correct name 2023-12-22 19:09:58 -06:00
a37a0b489d Add a nonNullList around orderBys in toString 2023-12-22 19:09:34 -06:00
940c5ca8de Overload withSectionOfChildren that takes Collection instead of varargs 2023-12-22 19:09:17 -06:00
84093dfde5 Fall back to field name if field label isn't set, when giving missing-required-field error 2023-12-22 19:08:43 -06:00
9c7d94f764 Little more user-facing error message 2023-12-22 19:07:37 -06:00
346443996b Adding QFieldType.LONG 2023-12-22 19:05:22 -06:00
2fc513891f Add methods allReadCapabilities and allWriteCapabilities (alright) 2023-12-22 19:05:22 -06:00
7426aa36a5 Thread name and log cleanups 2023-12-22 19:05:22 -06:00
9d8e8a74e2 Merged dev into feature/quartz-scheduler 2023-12-22 19:05:03 -06:00
455ab69104 Adding QFieldType.LONG 2023-12-22 18:59:08 -06:00
243 changed files with 16380 additions and 1135 deletions

1
docs/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
*.html

View File

@ -0,0 +1,64 @@
== GetAction
include::../variables.adoc[]
The `*GetAction*` is essentially a subset of the <<QueryAction>>, only specifically meant to get just a single record from a {link-table}.
In SQL/RDBMS terms, it is analogous to a `SELECT` statement, where a single record may be found - or - it may not be found.
For all tables, `GetAction` can do a lookup by primary key.
In addition, for tables that define a `UniqueKey`, name/value pairs (in the form of a `Map`) can be used as input for `GetAction`.
=== Examples
[source,java]
.Basic form - by primary key
----
GetInput input = new GetInput();
input.setTableName("orders");
input.setPrimaryKey(1);
GetOutput output = new GetAction.execute(input);
QRecord record = output.getRecord();
----
[source,java]
.Secondary form - by Unique Key
----
GetInput input = new GetInput();
input.setTableName("products");
input.setUniqueKey(Map.of("storeId", 1701, "sku", "ABCD"));
GetOutput output = new GetAction.execute(input);
QRecord record = output.getRecord();
----
=== GetInput
* `table` - *String, Required* - Name of the table being queried against.
* `primaryKey` - *Serializable, Conditional* - Value for the primary key field of the table being queried.
Type should match the table's primary key field's type.
If a `primaryKey` is not given, then a `uniqueKey` must be given.
* `uniqueKey` - *Map of String -> Serializable, Conditional* - Map of name-value pairs that define the record to be fetcheed.
Keys in the map must be field names from the table being queried.
Values in the map should should be of types that correspond to the fields.
If a `primaryKey` is not given, then a `uniqueKey` must be given.
If both `primaryKey` and `uniqueKey` are given, then `uniqueKey` is ignored.
* `transaction` - *QBackendTransaction object* - Optional transaction object.
** Behavior for this object is backend-dependant.
In an RDBMS backend, this object is generally needed if you want your query to see data that may have been modified within the same transaction.
* `shouldTranslatePossibleValues` - *boolean, default: false* - Controls whether any fields in the table with a *possibleValueSource* assigned to them should have those possible values looked up
(e.g., to provide text translations in the generated records' `displayValues` map).
** For example, if getting a record to present to a user, this would generally need to be *true*.
But if getting a record as part of a process, then this can generally be left as *false*.
* `shouldGenerateDisplayValues` - *boolean, default: false* - Controls whether field level *displayFormats* should be used to populate the generated records' `displayValues` map.
** For example, if getting a record to present to a user, this would generally need to be *true*.
But if getting a record as part of a process, then this can generally be left as *false*.
* `shouldFetchHeavyFields` - *boolean, default: true* - Controls whether or not fields marked as `isHeavy` should be fetched & returned or not.
* `shouldOmitHiddenFields` - *boolean, default: true* - Controls whether or not fields marked as `isHidden` should be included in the result or not.
* `shouldMaskPassword` - *boolean, default: true* - Controls whether or not fields with `type` = `PASSWORD` should be masked, or if their actual values should be returned.
* `queryJoins` - *List of <<QueryJoin>> objects* - Optional list of tables to be joined with the main table being queried.
See QueryJoin under <<QueryAction>> for further details.
* `includeAssociations` - *boolean, default: false* - Control whether or not records from tables defined as `associations` under the table being queried should be included in the result or not.
* `associationNamesToInclude` - *Collection of String* - If `includeAssociations` is true, then this field can be used to limit which associated tables are included.
If this field is null, then all associated tables are included.
Otherwise, a table is only included if its name is in this collection.
=== GetOutput
* `record` - *QRecord* - The record that was specified by the input `primaryKey` or `uniqueKey`.
Will be null if the record was not found.

View File

@ -0,0 +1,86 @@
== InsertAction
include::../variables.adoc[]
To insert (add, create) new records into any {link-table}, the `*InsertAction*` is used.
In SQL/RDBMS terms, it is analogous to a `INSERT` statement, where one or more records can be provided as input.
=== Examples
[source,java]
.Canonical InsertAction invocation
----
InsertInput insertInput = new InsertInput();
insertInput.setTableName("person");
insertInput.setRecords(personRecordList);
InsertOutput insertOutput = new InsertAction().execute(insertInput);
List<QRecord> insertedPersonRecords = insertOutput.getRecords();
----
=== Details
`InsertAction` does several things beyond just inserting records into the specified table.
A high-level flow of its internal logic is:
. For tables using an automation status field, set its value to `PENDING_INSERT_AUTOMATIONS` for all `records` that are going to be inserted.
. Perform the following validations, which include running the table's `PRE_INSERT_CUSTOMIZER`, if one is defined, at the time that is specified by the customizer's `WhenToRun` property (default is `AFTER_ALL_VALIDATIONS`):
.. Ensure that default values specified in the table's fields are present if needed.
.. Apply per-field behaviors, as defined in {link-field} meta-data, such as truncating strings that are longer than their specified max.
.. Check for unique key violations (done here instead of in the backend, to provide better error messaging, and to allow a subset of records to be stored while some fail).
_We might want to make an input control in the future to specify that either the full input set should succeed or fail..._
.. Validate that required fields (again, per {link-field} meta-data) are set, generating per-record errors if they are not.
.. Validate any security fields in the records - e.g., ensure that the user has permission to insert records with the values they are attempting to insert.
. Send the records to the table's backend module to actually insert them into the backend storage.
. If the table has any associations defined, and if associated records are present, then recursively run `InsertAction` on the associated records.
.. In particular, before these recursive `InsertAction` calls are made, values that were generated by the original insert may need to be propagated down into the associated records.
*** For example, if inserting `order` and `lineItem` records, where a {link-join} exists between the two tables on `order.id` and `lineItem.orderId`, and `order.id` values were generated in the first `InsertAction`, then those values are propagated down into the associated `lineItem.orderId` fields.
. If the {link-instance} has an `audit` table, then based on the {link-table}'s audit rules, audits about the inserted records are created.
. If the table has a `POST_INSERT_CUSTOMIZER`, it is executed.
=== Overloads
`InsertAction` can be called in a few alternate forms, mostly just for convenience:
[source,java]
.If inserting a single record, get that record back instead of the InsertOutput:
----
InsertInput insertInput = new InsertInput();
insertInput.setTableName("person");
insertInput.setRecords(List.of(personRecord));
QRecord insertedRecord = new InsertAction().executeForRecord(insertInput);
// or more compactly, using InsertInput.withRecord (instead of withRecords)
QRecord insertedRecord = new InsertAction()
.executeForRecord(new InsertInput("person").withRecord(personRecord));
----
[source,java]
.Taking QRecordEntity objects as inputs instead of QRecords:
----
// insert a list of person entities:
InsertInput insertInput = new InsertInput("person").withRecordEntities(personList);
InsertOutput insertOutput = new InsertAction().execute(insertInput);
// or for a single person entity (also mapping the output record back to an entity):
Person insertedPerson = new Person(new InsertAction()
.executeForRecord(new InsertInput("person").withRecordEntity(person)));
----
=== InsertInput
* `table` - *String, Required* - Name of the table that records are to be inserted into.
* `records` - *List of QRecord, Required* - List of records to be inserted into the table.
If the list is empty, the insert action does a `noop`.
.Less common options
* `inputSource` - *InputSource object, default: QInputSource.SYSTEM* - an indicator of what the source of the action is - generally, a `SYSTEM` triggered action, or a `USER` triggered action.
** `InsertAction` will call the `shouldValidateRequiredFields()` method on this object to determine if it should validate that required fields on the records have values.
Both `QInputSource.SYSTEM` and `QInputSource.USER` return `true` from this method, but application can define their own `InputSource` objects with different behavior.
** In addition, this field can be used in pre- and post-insert customizers to drive further custom logic.
* `skipUniqueKeyCheck` - *boolean, default: false* - control whether or not `InsertAction` should check for unique key violations before attempting to insert its records.
** In a context where one has previously done this validation, or is okay letting the backend provide such checks, they may wish to avoid re-doing this work, and thus may set this property to `true`.
* `omitDmlAudit` - *boolean, default: false* - control if the automatic DML audit that `InsertAction` generally performs should be omitted.
* `auditContext` - *String* - optional message which can be included in DML audit messages, to give users more context about why the insert occurred.
=== InsertOutput
* `records` - *List of QRecord* - Copy of the input list of records, with details added based on the results of the input action.
** If there were warnings or errors, the corresponding field (`warnings` or `errors`) will be set in the records.
** If the insert action generated any values (such as a serial id or a default value), those values will be in the record's `fields` map.

12
docs/justfile Normal file
View File

@ -0,0 +1,12 @@
## https://github.com/casey/just
default:
just --list
build-index-html:
asciidoctor -a docinfo=shared index.adoc
line=$(grep 'Last updated' index.html) && sed -i "s/id=\"content\">/&$line/" index.html
build-and-publish-index-html: build-index-html
scp index.html first-node:/mnt/first-volume/dkelkhoff/nginx/html/justinsgotskinnylegs.com/qqq-docs.html
@echo "Updated: https://justinsgotskinnylegs.com/qqq-docs.html"

20
docs/metaData/Icons.adoc Normal file
View File

@ -0,0 +1,20 @@
[#Icons]
== Icons
include::../variables.adoc[]
#TODO#
=== QIcon
Icons are defined in a QQQ Instance in a `*QIcon*` object.
#TODO#
*QIcon Properties:*
* `name` - *String* - Name of an icon from the https://mui.com/material-ui/material-icons/[Material UI Icon set]
** Note that icon names from the link above need to be converted from _CamelCase_ to _underscore_case_...
* `path` - *String* - Path to a file served under the application's web root, to be used as the icon.
_Either `name` or `path` must be specified. If both are given, then name is used._

View File

@ -0,0 +1,13 @@
[#PermissionRules]
== Permission Rules
include::../variables.adoc[]
#TODO#
=== PermissionRule
#TODO#
*PermissionRule Properties:*
#TODO#

View File

@ -0,0 +1,39 @@
[#QInstance]
== QInstance
include::../variables.adoc[]
An application in QQQ is defined as a set of Meta Data objects. These objects are all stored together in an object called a `*QInstance*`.
Currently, a `QInstance` must be programmatically constructed in java code - e.g., by constructing objects which get added to the QInstance, for example:
[source,java]
.Adding meta-data for two tables and one process to a QInstance
----
QInstance qInstance = new QInstance();
qInstance.addTable(definePersonTable());
qInstance.addTable(defineHomeTable());
qInstance.addProcess(defineSendPersonHomeProcess());
----
It is on the QQQ roadmap to allow meta-data to be defined in a non-programmatic way, for example, in YAML or JSON files, or even from a dynamic data source (e.g. a document or relational database).
The middleware and/or frontends being used in an application will drive how the `QInstance` is connected to the running server/process.
For example, using the `qqq-middleware-javalin` module, a the `QJavalinImplementation` class () has a constructor which takes a `QInstance` as an argument:
[source,java]
.Starting a QQQ Javalin middleware server - passing a QInstance as a parameter to the new QJavalinImplementation
----
QJavalinImplementation qJavalinImplementation = new QJavalinImplementation(qInstance);
Javalin service = Javalin.create();
service.routes(qJavalinImplementation.getRoutes());
service.start();
----
*QBackendMetaData Setup Methods:*
These are the methods that one is most likely to use when setting up (defining) a `QInstance` object:
* asdf
*QBackendMetaData Usage Methods:*

View File

@ -0,0 +1,155 @@
== Process Backend Steps
include::../variables.adoc[]
In many QQQ applications, much of the code that engineers write will take the form of Backend Steps for {link-processes}.
Such code is defined in classes which implement the interface `BackendStep`.
This interface defines only a single method:
[source,java]
.BackendStep.java
----
public interface BackendStep
{
/*******************************************************************************
** Execute the backend step - using the request as input, and the result as output.
**
*******************************************************************************/
void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException;
}
----
Process backend steps have access to state information - specifically, a list of records, and a map of name=value pairs - in the input & output objects.
This state data is persisted by QQQ between steps (e.g., if a frontend step is presented to a user between backend steps).
=== RunBackendStepInput
All input data to the step is available in the `RunBackendStepInput` object.
Key methods in this class are:
* `getRecords()` - Returns the List of <<QRecords>> that are currently being acted on in the process.
* `getValues()` - Returns a Map of String -> Serializable; name=value pairs that are the state-data of the process.
** Values can be added to this state from a process's meta-data, from a screen, or from another backend step.
* `getValue(String fieldName)` - Returns a specific value, by name, from the process's state.
** This method has several variations that return the value as a specific type, such as `getValueString`, `getValueInteger`, `getValueBoolean`...
* `getAsyncJobCallback()` - Accessor for an `AsyncJobCallback` object, which provides a way for the process backend step to communicate about its status or progress with a user running the process in a frontend.
Provides methods:
** `updateStatus(String message)` - Where general status messages can be given.
For example, `"Loading census data"`
** `updateStatus(int current, int total)` - For updating a progress meter.
e.g., "47 of 1701" would be display by calling `.updateStatus(47, 1701)`
* `getFrontendStepBehavior()` - Enum, indicating what should happen when a frontend step is encountered as the process's next step to run.
Possible values are:
** `BREAK` - Indicates that the process's execution should be suspended, so that the screen represented by the frontend step can be presented to a user.
This would be the expected behavior if a process is being run by a user from a UI.
** `SKIP` - Indicates that frontend steps should be skipped.
This would be the expected behavior if a process is running from a scheduled job (without a user present to drive it), for example.
** `FAIL` - Indicates that the process should end with an exception if a frontend step is encountered.
** A backend step may want to act differently based on its frontendStepBehavior.
For example, additional data may be looked up for displaying to a user if the behavior is `BREAK`.
* `getBasepullLastRunTime()` - For <<BasepullConfiguration,Basepull>> processes, this is the `Instant` stored in the basepull table as the process's last run time.
=== RunBackendStepOutput
All output from a process step should be placed in its `RunBackendStepOutput` object (and/or stored to a backend, as appropriate).
Key methods in this class are:
* `addValue(String fieldName, Serializable value)` - Adds a single named value to the process's state, overwriting it the value if it already exists.
* `addRecord(QRecord record)` - Add a `<<QRecord>>` to the process's output.
* `addAuditSingleInput(AuditSingleInput auditSingleInput)` - Add a new entry to the process's list of audit inputs, to be stored at the completion of the process.
** An `AuditSingleInput` object can most easily be built with the constructor: `AuditSingleInput(String tableName, QRecord record, String auditMessage)`.
** Additional audit details messages (sub-bullets that accompany the primary `auditMessage`) can be added to an `AuditSingleInput` via the `addDetail(String message)` method.
** _Note that at this time, the automatic storing of these audits is only provided by the execute step of a StreamedETLWithFrontendProcesses._
=== Example
[source,java]
.Example of a BackendStep
----
/*******************************************************************************
** For the "person" table's "Add Age" process -
** For each input person record, add the specified yearsToAdd to their age.
*******************************************************************************/
public class AddAge implements BackendStep
{
/*******************************************************************************
**
*******************************************************************************/
@Override
public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput)
{
/////////////////////////////////////////////////////////////////
// get the yearsToAdd input field value from the process input //
/////////////////////////////////////////////////////////////////
Integer yearsToAdd = runBackendStepInput.getValueInteger("yearsToAdd");
int totalYearsAdded = 0;
///////////////////////////////////////////////////
// loop over the records passed into the process //
///////////////////////////////////////////////////
for(QRecord record : runBackendStepInput.getRecords())
{
Integer age = record.getValueInteger("age");
age += yearsToAdd;
totalYearsAdded += yearsToAdd;
////////////////////////////////////////////////////////////////////////////////////////////
// update the record with the new "age" value. //
// note that this update record object will implicitly be available to the process's next //
// backend step, via the sharing of the processState object. //
////////////////////////////////////////////////////////////////////////////////////////////
record.setValue("age", age);
}
/////////////////////////////////////////
// set an output value for the process //
/////////////////////////////////////////
runBackendStepOutput.addValue("totalYearsAdded", totalYearsAdded);
}
}
----
=== Backend Steps for StreamedETLWithFrontendProcesses
For <<StreamedETLWithFrontendProcess>> type processes, backend steps are defined a little bit differently than they are for other process types.
In this type of process, the process meta-data defines 3 backend steps which are built-in to QQQ, and which do not have any custom application logic.
These steps are:
* `StreamedETLPreviewStep`
* `StreamedETLValidateStep`
* `StreamedETLExecuteStep`
For custom application logic to be implemented in a StreamedETLWithFrontendProcesses, an application engineer must define (up to) 3 backend step classes which are loaded by the steps listed above.
These application-defined steps must extend specific classes (which themselves implement the `BackendStep` interface), to provide the needed logic of this style of process.
These steps are:
* *Extract* - a subclass of `AbstractExtractStep` - is responsible for Extracting records from the source table.
** For this step, we can often use the QQQ-provided class `ExtractViaQueryStep`, or sometimes a subclass of it.
** The Extract step is called before the Preview, Validate, and Result screens, though for the Preview screen, it is set to only extract a small number of records (10).
* *Transform* - a subclass of `AbstractTransformStep` - is responsible for applying the majority of the business logic of the process.
In ETL terminology, this is the "Transform" action - which means applying some type of logical transformation an input record (found by the Extract step) to generate an output record (stored by the Load step).
** A Transform step's `run` method will be called, potentially, multiple times, each time with a page of records in the `runBackendStepInput` parameter.
** This method is responsible for adding records to the `runBackendStepOutput`, which will then be passed to the *Load* step.
** This class is also responsible for implementing the method `getProcessSummary`, which provides the data to the *Validate* screen.
** The run method will generally update ProcessSummaryLine objects to facilitate this functionality.
** The Transform step is called before the Preview, Validate, and Result screens, consuming all records selected by the Extract step.
* *Load* - a subclass of `AbstractLoadStep` - is responsible for the Load function of the ETL job.
_A quick word on terminology - this step is actually doing what we are more likely to think of as storing data - which feels like the opposite of “loading” - but we use the name Load to keep in line with the ETL naming convention…_
** The Load step is ONLY called before the Result screen is presented (possibly after Preview, if the user chose to skip validation, otherwise, after validation).
** Similar to the Transform step, the Load step's `run` method will be called potentially multiple times, with pages of records in its input.
** As such, the Load step is generally the only step where data writes should occur.
*** e.g., a Transform step should not do any writes, as it will be called when the user is going to the Preview & Validate screens - e.g., before the user confirmed that they want to execute the action!
** A common pattern is that the Load step just needs to insert or update the list of records output by the Transform step, in which case the QQQ-provided `LoadViaInsertStep` or `LoadViaUpdateStep` can be used, but custom use-cases can be built as well.
Another distinction between StreamedELTWithFrontendProcess steps and general QQQ process backend steps, is that the list of records in the input & output objects is NOT shared for StreamedELTWithFrontendProcess steps.
The direct implication of this is, that a Transform step MUST explicitly call `output.addRecord()` for any records that it wants to pass along to the Load step.
==== Example
[source,java]
.Examples of a Transform and Load step for a StreamedELTWithFrontendProcess
----
// todo!
----
#todo: more details on these 3 specialized types of process steps (e.g., method to overload, when stuff like pre-action is called; how summaries work).#

30
docs/misc/QContext.adoc Normal file
View File

@ -0,0 +1,30 @@
== QContext
include::../variables.adoc[]
The class `QContext` contains a collection of thread-local variables, to define the current context of the QQQ code that is currently running.
For example, what `QInstance` (meta-data container) is being used, what `QSession` (user attributes) is active, etc.
Most of the time, main-line application code does not need to worry about setting up the `QContext` - although unit-test code can is a common exception to that rule.
This is because all of QQQ's entry-points into execution (e.g., web context handlers, CLI startup methods, schedulers, and multi-threaded executors) take care of initializing the context.
It is more common though, for application code to need to get data from the context, such as the current session or any piece of meta-data from the QInstance.
The methods to access data from the `QContext` are fairly straightforward:
=== Examples
==== Get a QTableMetaData from the active QInstance
[source,java]
----
QTableMeataData myTable = QContext.getQInstance().getTable("myTable");
for(QFieldMeataData field : myTable.getFields().values())
{
// ...
}
----
==== Get a security key value for the current user session
[source,java]
----
QSession session = QContext.getQSession();
List<Serializable> clientIds = session.getSecurityKeyValues("clientId");
----

View File

@ -0,0 +1,116 @@
== QRecordEntities
include::../variables.adoc[]
While `<<QRecords>>` provide a flexible mechanism for passing around record data in a QQQ Application, they have one big disadvantage from the point-of-view of a Java Application:
They do not provide a mechanism to ensure compile-time checks of field names or field types.
As such, an alternative mechanism exists, which allows records in a QQQ application to be worked with following a more familiar Java Bean (Entity Bean) like pattern.
This mechanism is known as a `QRecordEntity`.
Specifically speaking, `QRecordEntity` is an abstract base class, whose purpose is to be the base class for entity-bean classes.
Using reflection, `QRecordEntity` is able to convert objects back and forth from `QRecord` to specific entity-bean subtypes.
For example, the method `QRecordEntity::toQRecord()` converts a record entity object into a `QRecord`.
Inversely, `QRecordEntity::populateFromQRecord(QRecord record)` sets fields in a record entity object, based on the values in the supplied `QRecord`.
It is conventional for a subclass of `QRecordEntity` to have both a no-arg (default) constructor, and a constructor that takes a `QRecord` as a parameter, and calls `populateFromQRecord`.
In addition to these constructors, a `QRecordEntity` subclass will generally contain:
* A `public static final String TABLE_NAME`, used throughout the application as a constant reference to the name for the {link-table}.
* A series of `private` fields, corresponding to the fields in the table that the entity represents.
** If these fields are annotated as `@QField()`, then the {link-table} meta-data for the table that the entity represents can in part be inferred by QQQ, by using the method `QTableMetaData::withFieldsFromEntity`.
* `getX()`, `setX()`, and `withX()` methods for all of the entity's fields.
=== Examples
[source,java]
.Example Definition of a QRecordEntity subclass: Person.java
----
/*******************************************************************************
** QRecordEntity for the person table.
*******************************************************************************/
public class Person extends QRecordEntity
{
public static final String TABLE_NAME = "person";
@QField(isEditable = false)
private Integer id;
@QField()
private String firstName;
@QField()
private String lastName;
@QField()
private Integer age;
// all other fields
/*******************************************************************************
** Default constructor
*******************************************************************************/
public Person()
{
}
/*******************************************************************************
** Constructor that takes a QRecord
*******************************************************************************/
public Person(QRecord record)
{
populateFromQRecord(record);
}
/*******************************************************************************
** Custom method to concatenate firstName and lastName
*******************************************************************************/
public String getFullName()
{
//////////////////////////////////////////////////////////////////////
// if there were more than an example, we could do some null-checks //
// here to avoid silly output like "Bobby null" :) //
//////////////////////////////////////////////////////////////////////
return (firstName + " " + lastName);
}
// all getter, setter, and fluent setter (wither) methods
}
----
The core ORM actions and process-execution actions of QQQ work with `QRecords`.
However, application engineers may want to apply patterns like the following example, to gain the compile-time safety of `QRecordEntities`:
[source,java]
.Example Usage of a QRecordEntity
----
//////////////////////////////////////////////////////////////////////
// assume we're working with the "person" table & entity from above //
//////////////////////////////////////////////////////////////////////
List<QRecord> recordsToUpdate = new ArrayList<>();
for(QRecord record : inputRecordList)
{
/////////////////////////////////////////////////////////////////////////////
// call the constructor that copies values from the record into the entity //
/////////////////////////////////////////////////////////////////////////////
Person person = new Person(record);
////////////////////////////////////////////////
// call a custom method defined in the entity //
////////////////////////////////////////////////
LOG.info("Processing: " + person.getFullName());
/////////////////////////////////////////////////////////////
// age is an Integer, so no type-conversion is needed here //
/////////////////////////////////////////////////////////////
person.setAge(person.getAge() + 1);
///////////////////////////////////////////////////////////////////////////
// to pass the updated records to UpdateAction, convert them to QRecords //
///////////////////////////////////////////////////////////////////////////
recordsToUpdate.add(person.toQRecord());
}
----

68
docs/misc/QRecords.adoc Normal file
View File

@ -0,0 +1,68 @@
== QRecords
include::../variables.adoc[]
Almost all code inside a QQQ application will be dealing with *Records* (aka Tuples or Rows).
That is: a collection of named values, representing a single Entity, Fact, or, Row from a {link-table}.
The class that QQQ uses to work with records is called: `QRecord`.
=== Values
At its core, a `QRecord` is a wrapper around a `Map<String, Serializable> values`.
These are the *actual* values for the fields in the table for the record.
That is, direct representations of the values as they are stored in the {link-backend}.
The keys in the `values` map are names from the {link-fields} in the {link-table}.
The values in `values` map are declared as `Serializable` (to help ensure the serializability of the `QRecord` as a whole).
In practice, their types will be based on the `QFieldType` of the {link-field} that they correspond to.
That will typically be one of: `String`, `Integer`, `Boolean`, `BigDecimal`, `Instant`, `LocalDate`, `LocalTime`, or `byte[]`.
Be aware that `null` values may be in the `values` map, especially/per if the backend/table support `null`.
To work with the `values` map, the following methods are provided:
* `setValue(String fieldName, Serializable value)` - Sets a value for the specified field in the record.
** Overloaded as `setValue(String fieldName, Object value)` - For some cases where the value may not be known to be `Serializable`.
In this overload, if the value is `null` or `Serializable`, the primary version of `setValue` is called.
Otherwise, the `value` is passed through `String::valueOf`, and the result is stored.
** Overloaded as `setValue(QFieldMetaData field, Serializable value)` - which simply defers to the primary version of `setValue`, passing `field.getName()` as the first parameter.
* `removeValue(String fieldName)` - Remove the given field from the `values` map.
** Note that in some situations this is an important distinction from having a `null` value in the map (See <<UpdateAction)>>).
* `setValues(Map<String, Serializable> values)` - Sets the full map of `values`.
* `getValues()` - Returns the full map of `values`.
* `getValue(String fieldName)` - Returns the value for the named field - possibly `null` - as a `Serializable`.
* Several type-specific variations of `getValueX(String fieldName)`, where internally, values will be not exactly type-cast, but effectively converted (if possible) to the requested type.
These conversions are done using the `ValueUtils.getValueAsX(Object)` methods.
These methods are generally the preferred/cleanest way to get record values in application code, when it is needed in a type-specific way .
** `getValueString(String fieldName)`
** `getValueInteger(String fieldName)`
** `getValueBoolean(String fieldName)`
** `getValueBigDecimal(String fieldName)`
** `getValueInstant(String fieldName)`
** `getValueLocalDate(String fieldName)`
** `getValueLocalTime(String fieldName)`
** `getValueByteArray(String fieldName)`
=== Display Values
In addition to the `values` map, a `QRecord` contains another map called `displayValues`, which only stores `String` values.
That is to say, values for other types are stringified, based on their {link-field}'s type and `displayFormat` property.
In addition, fields which have a `possibleValueSource` property will have their translated values set in the `displayValues` map.
By default, a `QRecord` will not have its `displayValues` populated.
To populate `displayValues`, the <<QueryAction>> and <<GetAction>> classes take a property in their inputs called `shouldGenerateDisplayValues`, which must be set to `true` to generate `displayValues`.
In addition, these two actions also have a property `shouldTranslatePossibleValues` in their inputs, which needs to be set to `true` if possible-value lookups are to be performed.
As an alternative to the `shouldGenerateDisplayValues` and `shouldTranslatePossibleValues` inputs to <<QueryAction>> and <<GetAction>>, one can directly call the `QValueFormatter.setDisplayValuesInRecords` and/or `qPossibleValueTranslator.translatePossibleValuesInRecords` methods.
Or, for special cases, `setDisplayValue(String fieldName, String displayValue)` or `setDisplayValues(Map<String, String> displayValues)` can be called directly.
=== Backend Details
Sometimes a backend may want to place additional data in a `QRecord` that doesn't exactly correspond to a field.
To do this, the `Map<String, Serializable> backendDetails` member is used.
For example, an API backend may store the full JSON `String` that came from the API as a backend detail in a `QRecord`.
Or fields that are marked as `isHeavy`, if the full (heavy) value of the field hasn't been fetched, then the lengths of any such heavy fields may be stored in `backendDetails`.
=== Errors and Warnings
#todo#
=== Associated Records
#todo#

44
pom.xml
View File

@ -330,28 +330,28 @@ fi
<!-- mvn javadoc:aggregate -->
<reporting>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>3.6.2</version>
<reportSets>
<reportSet>
<id>aggregate</id>
<inherited>false</inherited>
<reports>
<report>aggregate</report>
</reports>
</reportSet>
<reportSet>
<id>default</id>
<reports>
<report>javadoc</report>
</reports>
</reportSet>
</reportSets>
</plugin>
</plugins>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>3.6.2</version>
<reportSets>
<reportSet>
<id>aggregate</id>
<inherited>false</inherited>
<reports>
<report>aggregate</report>
</reports>
</reportSet>
<reportSet>
<id>default</id>
<reports>
<report>javadoc</report>
</reports>
</reportSet>
</reportSets>
</plugin>
</plugins>
</reporting>
<repositories>

View File

@ -163,6 +163,19 @@
<version>1.12.321</version>
</dependency>
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>2.3.2</version>
</dependency>
<!-- Many of the deps we bring in use slf4j. This dep maps slf4j to our logger, log4j -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
<version>2.23.0</version>
</dependency>
<!-- Common deps for all qqq modules -->
<dependency>
<groupId>org.apache.maven.plugins</groupId>

View File

@ -159,7 +159,8 @@ public class AsyncJobManager
private <T extends Serializable> T runAsyncJob(String jobName, AsyncJob<T> asyncJob, UUIDAndTypeStateKey uuidAndTypeStateKey, AsyncJobStatus asyncJobStatus)
{
String originalThreadName = Thread.currentThread().getName();
Thread.currentThread().setName("Job:" + jobName + ":" + uuidAndTypeStateKey.getUuid().toString().substring(0, 8));
// Thread.currentThread().setName("Job:" + jobName + ":" + uuidAndTypeStateKey.getUuid().toString().substring(0, 8));
Thread.currentThread().setName("Job:" + jobName);
try
{
LOG.debug("Starting job " + uuidAndTypeStateKey.getUuid());

View File

@ -83,6 +83,15 @@ public class AsyncRecordPipeLoop
long jobStartTime = System.currentTimeMillis();
boolean everCalledConsumer = false;
////////////////////////////////////////////////////////////////////////////
// in case the pipe capacity has been made very small (useful in tests!), //
// then make the minRecordsToConsume match it. //
////////////////////////////////////////////////////////////////////////////
if(recordPipe.getCapacity() < minRecordsToConsume)
{
minRecordsToConsume = recordPipe.getCapacity();
}
while(jobState.equals(AsyncJobState.RUNNING))
{
if(recordPipe.countAvailableRecords() < minRecordsToConsume)

View File

@ -295,7 +295,7 @@ public class AuditAction extends AbstractQActionFunction<AuditInput, AuditOutput
}
catch(Exception e)
{
LOG.error("Error performing an audit", e);
LOG.warn("Error performing an audit", e);
}
}

View File

@ -187,7 +187,7 @@ public class DMLAuditAction extends AbstractQActionFunction<DMLAuditInput, DMLAu
}
catch(Exception e)
{
LOG.error("Error performing DML audit", e, logPair("type", String.valueOf(dmlType)), logPair("table", table.getName()));
LOG.warn("Error performing DML audit", e, logPair("type", String.valueOf(dmlType)), logPair("table", table.getName()));
}
return (output);

View File

@ -132,6 +132,11 @@ public class PollingAutomationPerTableRunner implements Runnable
*******************************************************************************/
String tableName();
/*******************************************************************************
**
*******************************************************************************/
QTableAutomationDetails tableAutomationDetails();
/*******************************************************************************
**
*******************************************************************************/
@ -143,7 +148,7 @@ public class PollingAutomationPerTableRunner implements Runnable
/*******************************************************************************
** Wrapper for a pair of (tableName, automationStatus)
*******************************************************************************/
public record TableActions(String tableName, AutomationStatus status) implements TableActionsInterface
public record TableActions(String tableName, QTableAutomationDetails tableAutomationDetails, AutomationStatus status) implements TableActionsInterface
{
/*******************************************************************************
**
@ -159,7 +164,7 @@ public class PollingAutomationPerTableRunner implements Runnable
** extended version of TableAction, for sharding use-case - adds the shard
** details.
*******************************************************************************/
public record ShardedTableActions(String tableName, AutomationStatus status, String shardByFieldName, Serializable shardValue, String shardLabel) implements TableActionsInterface
public record ShardedTableActions(String tableName, QTableAutomationDetails tableAutomationDetails, AutomationStatus status, String shardByFieldName, Serializable shardValue, String shardLabel) implements TableActionsInterface
{
/*******************************************************************************
**
@ -198,8 +203,8 @@ public class PollingAutomationPerTableRunner implements Runnable
{
Serializable shardId = record.getValue(automationDetails.getShardIdFieldName());
String label = record.getValueString(automationDetails.getShardLabelFieldName());
tableActionList.add(new ShardedTableActions(table.getName(), AutomationStatus.PENDING_INSERT_AUTOMATIONS, automationDetails.getShardByFieldName(), shardId, label));
tableActionList.add(new ShardedTableActions(table.getName(), AutomationStatus.PENDING_UPDATE_AUTOMATIONS, automationDetails.getShardByFieldName(), shardId, label));
tableActionList.add(new ShardedTableActions(table.getName(), automationDetails, AutomationStatus.PENDING_INSERT_AUTOMATIONS, automationDetails.getShardByFieldName(), shardId, label));
tableActionList.add(new ShardedTableActions(table.getName(), automationDetails, AutomationStatus.PENDING_UPDATE_AUTOMATIONS, automationDetails.getShardByFieldName(), shardId, label));
}
}
catch(Exception e)
@ -209,11 +214,11 @@ public class PollingAutomationPerTableRunner implements Runnable
}
else
{
///////////////////////////////////////////////////////////////////
// for non-sharded, we just need tabler name & automation status //
///////////////////////////////////////////////////////////////////
tableActionList.add(new TableActions(table.getName(), AutomationStatus.PENDING_INSERT_AUTOMATIONS));
tableActionList.add(new TableActions(table.getName(), AutomationStatus.PENDING_UPDATE_AUTOMATIONS));
//////////////////////////////////////////////////////////////////
// for non-sharded, we just need table name & automation status //
//////////////////////////////////////////////////////////////////
tableActionList.add(new TableActions(table.getName(), automationDetails, AutomationStatus.PENDING_INSERT_AUTOMATIONS));
tableActionList.add(new TableActions(table.getName(), automationDetails, AutomationStatus.PENDING_UPDATE_AUTOMATIONS));
}
}
}

View File

@ -47,12 +47,24 @@ import com.kingsrook.qqq.backend.core.model.data.QRecord;
** records that the delete action marked in error - the user might want to do
** something special with them (idk, try some other way to delete them?)
*******************************************************************************/
public abstract class AbstractPostDeleteCustomizer
public abstract class AbstractPostDeleteCustomizer implements TableCustomizerInterface
{
protected DeleteInput deleteInput;
/*******************************************************************************
**
*******************************************************************************/
@Override
public List<QRecord> postDelete(DeleteInput deleteInput, List<QRecord> records) throws QException
{
this.deleteInput = deleteInput;
return apply(records);
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -42,12 +42,24 @@ import com.kingsrook.qqq.backend.core.model.data.QRecord;
**
** Note that the full insertInput is available as a field in this class.
*******************************************************************************/
public abstract class AbstractPostInsertCustomizer
public abstract class AbstractPostInsertCustomizer implements TableCustomizerInterface
{
protected InsertInput insertInput;
/*******************************************************************************
**
*******************************************************************************/
@Override
public List<QRecord> postInsert(InsertInput insertInput, List<QRecord> records) throws QException
{
this.insertInput = insertInput;
return (apply(records));
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -23,16 +23,29 @@ package com.kingsrook.qqq.backend.core.actions.customizers;
import java.util.List;
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
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.data.QRecord;
/*******************************************************************************
**
*******************************************************************************/
public abstract class AbstractPostQueryCustomizer
public abstract class AbstractPostQueryCustomizer implements TableCustomizerInterface
{
protected AbstractTableActionInput input;
protected QueryOrGetInputInterface input;
/*******************************************************************************
**
*******************************************************************************/
@Override
public List<QRecord> postQuery(QueryOrGetInputInterface queryInput, List<QRecord> records) throws QException
{
input = queryInput;
return apply(records);
}
@ -47,7 +60,7 @@ public abstract class AbstractPostQueryCustomizer
** Getter for input
**
*******************************************************************************/
public AbstractTableActionInput getInput()
public QueryOrGetInputInterface getInput()
{
return (input);
}
@ -58,7 +71,7 @@ public abstract class AbstractPostQueryCustomizer
** Setter for input
**
*******************************************************************************/
public void setInput(AbstractTableActionInput input)
public void setInput(QueryOrGetInputInterface input)
{
this.input = input;
}

View File

@ -26,6 +26,7 @@ import java.io.Serializable;
import java.util.HashMap;
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.model.actions.tables.update.UpdateInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
@ -48,7 +49,7 @@ import com.kingsrook.qqq.backend.core.model.data.QRecord;
** available (if the backend supports it) - both as a list (`getOldRecordList`)
** and as a memoized (by this class) map of primaryKey to record (`getOldRecordMap`).
*******************************************************************************/
public abstract class AbstractPostUpdateCustomizer
public abstract class AbstractPostUpdateCustomizer implements TableCustomizerInterface
{
protected UpdateInput updateInput;
protected List<QRecord> oldRecordList;
@ -57,6 +58,19 @@ public abstract class AbstractPostUpdateCustomizer
/*******************************************************************************
**
*******************************************************************************/
@Override
public List<QRecord> postUpdate(UpdateInput updateInput, List<QRecord> records, Optional<List<QRecord>> oldRecordList) throws QException
{
this.updateInput = updateInput;
this.oldRecordList = oldRecordList.orElse(null);
return apply(records);
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -50,7 +50,7 @@ import com.kingsrook.qqq.backend.core.model.data.QRecord;
** Note that the full deleteInput is available as a field in this class.
**
*******************************************************************************/
public abstract class AbstractPreDeleteCustomizer
public abstract class AbstractPreDeleteCustomizer implements TableCustomizerInterface
{
protected DeleteInput deleteInput;
@ -58,6 +58,19 @@ public abstract class AbstractPreDeleteCustomizer
/*******************************************************************************
**
*******************************************************************************/
@Override
public List<QRecord> preDelete(DeleteInput deleteInput, List<QRecord> records, boolean isPreview) throws QException
{
this.deleteInput = deleteInput;
this.isPreview = isPreview;
return apply(records);
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -47,7 +47,7 @@ import com.kingsrook.qqq.backend.core.model.data.QRecord;
**
** Note that the full insertInput is available as a field in this class.
*******************************************************************************/
public abstract class AbstractPreInsertCustomizer
public abstract class AbstractPreInsertCustomizer implements TableCustomizerInterface
{
protected InsertInput insertInput;
@ -70,6 +70,30 @@ public abstract class AbstractPreInsertCustomizer
/*******************************************************************************
**
*******************************************************************************/
@Override
public List<QRecord> preInsert(InsertInput insertInput, List<QRecord> records, boolean isPreview) throws QException
{
this.insertInput = insertInput;
this.isPreview = isPreview;
return (apply(records));
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public WhenToRun whenToRunPreInsert(InsertInput insertInput, boolean isPreview)
{
return getWhenToRun();
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -26,6 +26,7 @@ import java.io.Serializable;
import java.util.HashMap;
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.model.actions.tables.update.UpdateInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
@ -53,7 +54,7 @@ import com.kingsrook.qqq.backend.core.model.data.QRecord;
** available (if the backend supports it) - both as a list (`getOldRecordList`)
** and as a memoized (by this class) map of primaryKey to record (`getOldRecordMap`).
*******************************************************************************/
public abstract class AbstractPreUpdateCustomizer
public abstract class AbstractPreUpdateCustomizer implements TableCustomizerInterface
{
protected UpdateInput updateInput;
protected List<QRecord> oldRecordList;
@ -63,6 +64,20 @@ public abstract class AbstractPreUpdateCustomizer
/*******************************************************************************
**
*******************************************************************************/
@Override
public List<QRecord> preUpdate(UpdateInput updateInput, List<QRecord> records, boolean isPreview, Optional<List<QRecord>> oldRecordList) throws QException
{
this.updateInput = updateInput;
this.isPreview = isPreview;
this.oldRecordList = oldRecordList.orElse(null);
return apply(records);
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -22,6 +22,7 @@
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;
@ -34,42 +35,35 @@ 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;
/*******************************************************************************
** 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.
*******************************************************************************/
public class QCodeLoader
{
private static final QLogger LOG = QLogger.getLogger(QCodeLoader.class);
/*******************************************************************************
**
*******************************************************************************/
public static <T, R> Optional<Function<T, R>> getTableCustomizerFunction(QTableMetaData table, String customizerName)
{
Optional<QCodeReference> codeReference = table.getCustomizer(customizerName);
if(codeReference.isPresent())
{
return (Optional.ofNullable(QCodeLoader.getFunction(codeReference.get())));
}
return (Optional.empty());
}
private static Memoization<String, Constructor<?>> constructorMemoization = new Memoization<>();
/*******************************************************************************
**
*******************************************************************************/
public static <T> Optional<T> getTableCustomizer(Class<T> expectedClass, QTableMetaData table, String customizerName)
public static Optional<TableCustomizerInterface> getTableCustomizer(QTableMetaData table, String customizerName)
{
Optional<QCodeReference> codeReference = table.getCustomizer(customizerName);
if(codeReference.isPresent())
{
return (Optional.ofNullable(QCodeLoader.getAdHoc(expectedClass, codeReference.get())));
return (Optional.ofNullable(QCodeLoader.getAdHoc(TableCustomizerInterface.class, codeReference.get())));
}
return (Optional.empty());
}
@ -175,8 +169,21 @@ public class QCodeLoader
try
{
Class<?> customizerClass = Class.forName(codeReference.getName());
return ((T) customizerClass.getConstructor().newInstance());
Optional<Constructor<?>> constructor = constructorMemoization.getResultThrowing(codeReference.getName(), (UnsafeFunction<String, Constructor<?>, Exception>) s ->
{
Class<?> customizerClass = Class.forName(codeReference.getName());
return customizerClass.getConstructor();
});
if(constructor.isPresent())
{
return ((T) constructor.get().newInstance());
}
else
{
LOG.error("Could not get constructor for code reference", logPair("codeReference", codeReference));
return (null);
}
}
catch(Exception e)
{

View File

@ -0,0 +1,202 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.customizers;
import java.util.List;
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.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 static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
** Common interface used by all (core) TableCustomizer types (e.g., post-query,
** and {pre,post}-{insert,update,delete}.
**
** Note that the abstract-base classes for each action still exist, though have
** been back-ported to be implementors of this interface. The action classes
** will now expect this type, and call this type's methods.
**
*******************************************************************************/
public interface TableCustomizerInterface
{
QLogger LOG = QLogger.getLogger(TableCustomizerInterface.class);
/*******************************************************************************
** custom actions to run after a query (or get!) takes place.
**
*******************************************************************************/
default List<QRecord> postQuery(QueryOrGetInputInterface queryInput, List<QRecord> records) throws QException
{
LOG.info("A default implementation of postQuery is running... Probably not expected!", logPair("tableName", queryInput.getTableName()));
return (records);
}
/*******************************************************************************
** custom actions before an insert takes place.
**
** It's important for implementations to be aware of the isPreview field, which
** is set to true when the code is running to give users advice, e.g., on a review
** screen - vs. being false when the action is ACTUALLY happening. So, if you're doing
** things like storing data, you don't want to do that if isPreview is true!!
**
** General implementation would be, to iterate over the records (the inputs to
** the insert action), and look at their values:
** - possibly adding Errors (`addError`) or Warnings (`addWarning`) to the records
** - possibly manipulating values (`setValue`)
** - possibly throwing an exception - if you really don't want the insert operation to continue.
** - doing "whatever else" you may want to do.
** - returning the list of records (can be the input list) that you want to go on to the backend implementation class.
*******************************************************************************/
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);
}
/*******************************************************************************
**
*******************************************************************************/
default AbstractPreInsertCustomizer.WhenToRun whenToRunPreInsert(InsertInput insertInput, boolean isPreview)
{
return (AbstractPreInsertCustomizer.WhenToRun.AFTER_ALL_VALIDATIONS);
}
/*******************************************************************************
** custom actions after an insert takes place.
**
** General implementation would be, to iterate over the records (the outputs of
** the insert action), and look at their values:
** - possibly adding Errors (`addError`) or Warnings (`addWarning`) to the records
** - possibly throwing an exception - though doing so won't stop the update, and instead
** will just set a warning on all of the updated records...
** - doing "whatever else" you may want to do.
** - returning the list of records (can be the input list) that you want to go back to the caller.
*******************************************************************************/
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);
}
/*******************************************************************************
** custom actions before an update takes place.
**
** It's important for implementations to be aware of the isPreview field, which
** is set to true when the code is running to give users advice, e.g., on a review
** screen - vs. being false when the action is ACTUALLY happening. So, if you're doing
** things like storing data, you don't want to do that if isPreview is true!!
**
** General implementation would be, to iterate over the records (the inputs to
** the update action), and look at their values:
** - possibly adding Errors (`addError`) or Warnings (`addWarning`) to the records
** - possibly manipulating values (`setValue`)
** - possibly throwing an exception - if you really don't want the update operation to continue.
** - doing "whatever else" you may want to do.
** - returning the list of records (can be the input list) that you want to go on to the backend implementation class.
**
** Note, "old records" (e.g., with values freshly fetched from the backend) will be
** available (if the backend supports it)
*******************************************************************************/
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);
}
/*******************************************************************************
** custom actions after an update takes place.
**
** General implementation would be, to iterate over the records (the outputs of
** the update action), and look at their values:
** - possibly adding Errors (`addError`) or Warnings (`addWarning`) to the records?
** - possibly throwing an exception - though doing so won't stop the update, and instead
** will just set a warning on all of the updated records...
** - doing "whatever else" you may want to do.
** - returning the list of records (can be the input list) that you want to go back to the caller.
**
** Note, "old records" (e.g., with values freshly fetched from the backend) will be
** available (if the backend supports it).
*******************************************************************************/
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);
}
/*******************************************************************************
** Custom actions before a delete takes place.
**
** It's important for implementations to be aware of the isPreview param, which
** is set to true when the code is running to give users advice, e.g., on a review
** screen - vs. being false when the action is ACTUALLY happening. So, if you're doing
** things like storing data, you don't want to do that if isPreview is true!!
**
** General implementation would be, to iterate over the records (which the DeleteAction
** would look up based on the inputs to the delete action), and look at their values:
** - possibly adding Errors (`addError`) or Warnings (`addWarning`) to the records
** - possibly throwing an exception - if you really don't want the delete operation to continue.
** - doing "whatever else" you may want to do.
** - returning the list of records (can be the input list) - this is how errors
** and warnings are propagated to the DeleteAction. Note that any records with
** an error will NOT proceed to the backend's delete interface - but those with
** warnings will.
*******************************************************************************/
default List<QRecord> preDelete(DeleteInput deleteInput, List<QRecord> records, boolean isPreview) throws QException
{
LOG.info("A default implementation of preDelete is running... Probably not expected!", logPair("tableName", deleteInput.getTableName()));
return (records);
}
/*******************************************************************************
** Custom actions after a delete takes place.
**
** General implementation would be, to iterate over the records (ones which didn't
** have a delete error), and look at their values:
** - possibly adding Errors (`addError`) or Warnings (`addWarning`) to the records?
** - possibly throwing an exception - though doing so won't stop the delete, and instead
** will just set a warning on all of the deleted records...
** - doing "whatever else" you may want to do.
** - returning the list of records (can be the input list) that you want to go back
** to the caller - this is how errors and warnings are propagated .
*******************************************************************************/
default List<QRecord> postDelete(DeleteInput deleteInput, List<QRecord> records) throws QException
{
LOG.info("A default implementation of postDelete is running... Probably not expected!", logPair("tableName", deleteInput.getTableName()));
return (records);
}
}

View File

@ -29,13 +29,13 @@ package com.kingsrook.qqq.backend.core.actions.customizers;
*******************************************************************************/
public enum TableCustomizers
{
POST_QUERY_RECORD("postQueryRecord", AbstractPostQueryCustomizer.class),
PRE_INSERT_RECORD("preInsertRecord", AbstractPreInsertCustomizer.class),
POST_INSERT_RECORD("postInsertRecord", AbstractPostInsertCustomizer.class),
PRE_UPDATE_RECORD("preUpdateRecord", AbstractPreUpdateCustomizer.class),
POST_UPDATE_RECORD("postUpdateRecord", AbstractPostUpdateCustomizer.class),
PRE_DELETE_RECORD("preDeleteRecord", AbstractPreDeleteCustomizer.class),
POST_DELETE_RECORD("postDeleteRecord", AbstractPostDeleteCustomizer.class);
POST_QUERY_RECORD("postQueryRecord", TableCustomizerInterface.class),
PRE_INSERT_RECORD("preInsertRecord", TableCustomizerInterface.class),
POST_INSERT_RECORD("postInsertRecord", TableCustomizerInterface.class),
PRE_UPDATE_RECORD("preUpdateRecord", TableCustomizerInterface.class),
POST_UPDATE_RECORD("postUpdateRecord", TableCustomizerInterface.class),
PRE_DELETE_RECORD("preDeleteRecord", TableCustomizerInterface.class),
POST_DELETE_RECORD("postDeleteRecord", TableCustomizerInterface.class);
private final String role;

View File

@ -0,0 +1,202 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.dashboard.widgets;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import com.kingsrook.qqq.backend.core.actions.tables.AggregateAction;
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.aggregate.Aggregate;
import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.AggregateInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.AggregateOperator;
import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.AggregateOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.AggregateResult;
import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.GroupBy;
import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.QFilterOrderByAggregate;
import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.QFilterOrderByGroupBy;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
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.TableData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
/*******************************************************************************
** Generic widget that does an aggregate query, and presents its results
** as a table, using group-by values as both row & column labels.
*******************************************************************************/
public class Aggregate2DTableWidgetRenderer extends AbstractWidgetRenderer
{
private static final QLogger LOG = QLogger.getLogger(Aggregate2DTableWidgetRenderer.class);
/*******************************************************************************
**
*******************************************************************************/
@Override
public RenderWidgetOutput render(RenderWidgetInput input) throws QException
{
Map<String, Serializable> values = input.getWidgetMetaData().getDefaultValues();
String tableName = ValueUtils.getValueAsString(values.get("tableName"));
String valueField = ValueUtils.getValueAsString(values.get("valueField"));
String rowField = ValueUtils.getValueAsString(values.get("rowField"));
String columnField = ValueUtils.getValueAsString(values.get("columnField"));
QTableMetaData table = QContext.getQInstance().getTable(tableName);
AggregateInput aggregateInput = new AggregateInput();
aggregateInput.setTableName(tableName);
// todo - allow input of "list of columns" (e.g., in case some miss sometimes, or as a version of filter)
// todo - max rows, max cols?
// todo - from input map
QQueryFilter filter = new QQueryFilter();
aggregateInput.setFilter(filter);
Aggregate aggregate = new Aggregate(valueField, AggregateOperator.COUNT);
aggregateInput.withAggregate(aggregate);
GroupBy rowGroupBy = new GroupBy(table.getField(rowField));
GroupBy columnGroupBy = new GroupBy(table.getField(columnField));
aggregateInput.withGroupBy(rowGroupBy);
aggregateInput.withGroupBy(columnGroupBy);
String orderBys = ValueUtils.getValueAsString(values.get("orderBys"));
if(StringUtils.hasContent(orderBys))
{
for(String orderBy : orderBys.split(","))
{
switch(orderBy)
{
case "row" -> filter.addOrderBy(new QFilterOrderByGroupBy(rowGroupBy));
case "column" -> filter.addOrderBy(new QFilterOrderByGroupBy(columnGroupBy));
case "value" -> filter.addOrderBy(new QFilterOrderByAggregate(aggregate));
default -> LOG.warn("Unrecognized orderBy: " + orderBy);
}
}
}
AggregateOutput aggregateOutput = new AggregateAction().execute(aggregateInput);
Map<Serializable, Map<Serializable, Serializable>> data = new LinkedHashMap<>();
Set<Serializable> columnsSet = new LinkedHashSet<>();
for(AggregateResult result : aggregateOutput.getResults())
{
Serializable column = result.getGroupByValue(columnGroupBy);
Serializable row = result.getGroupByValue(rowGroupBy);
Serializable value = result.getAggregateValue(aggregate);
Map<Serializable, Serializable> rowMap = data.computeIfAbsent(row, (k) -> new LinkedHashMap<>());
rowMap.put(column, value);
columnsSet.add(column);
}
// todo - possible values from rows, cols
////////////////////////////////////
// setup datastructures for table //
////////////////////////////////////
List<Map<String, Object>> tableRows = new ArrayList<>();
List<TableData.Column> tableColumns = new ArrayList<>();
tableColumns.add(new TableData.Column("default", table.getField(rowField).getLabel(), "_row", "2fr", "left"));
for(Serializable column : columnsSet)
{
tableColumns.add(new TableData.Column("default", String.valueOf(column) /* todo display value */, String.valueOf(column), "1fr", "right"));
}
tableColumns.add(new TableData.Column("default", "Total", "_total", "1fr", "right"));
TableData tableData = new TableData(null, tableColumns, tableRows)
.withRowsPerPage(100)
.withFixedStickyLastRow(false)
.withHidePaginationDropdown(true);
Map<Serializable, Integer> columnSums = new HashMap<>();
int grandTotal = 0;
for(Map.Entry<Serializable, Map<Serializable, Serializable>> rowEntry : data.entrySet())
{
Map<String, Object> rowMap = new HashMap<>();
tableRows.add(rowMap);
rowMap.put("_row", rowEntry.getKey() /* todo display value */);
int rowTotal = 0;
for(Serializable column : columnsSet)
{
Serializable value = rowEntry.getValue().get(column);
if(value == null)
{
value = 0; // todo?
}
Integer valueAsInteger = Objects.requireNonNullElse(ValueUtils.getValueAsInteger(value), 0);
rowTotal += valueAsInteger;
columnSums.putIfAbsent(column, 0);
columnSums.put(column, columnSums.get(column) + valueAsInteger);
rowMap.put(String.valueOf(column), value); // todo format commas?
}
rowMap.put("_total", rowTotal);
grandTotal += rowTotal;
}
///////////////
// total row //
///////////////
Map<String, Object> totalRowMap = new HashMap<>();
tableRows.add(totalRowMap);
totalRowMap.put("_row", "Total");
int rowTotal = 0;
for(Serializable column : columnsSet)
{
Serializable value = columnSums.get(column);
if(value == null)
{
value = 0; // todo?
}
totalRowMap.put(String.valueOf(column), value); // todo format commas?
}
totalRowMap.put("_total", grandTotal);
return (new RenderWidgetOutput(tableData));
}
}

View File

@ -137,7 +137,7 @@ public class ChildRecordListRenderer extends AbstractWidgetRenderer
*******************************************************************************/
public Builder withCanAddChildRecord(boolean b)
{
widgetMetaData.withDefaultValue("canAddChildRecord", true);
widgetMetaData.withDefaultValue("canAddChildRecord", b);
return (this);
}
@ -151,6 +151,17 @@ public class ChildRecordListRenderer extends AbstractWidgetRenderer
widgetMetaData.withDefaultValue("disabledFieldsForNewChildRecords", new HashSet<>(disabledFieldsForNewChildRecords));
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
public Builder withManageAssociationName(String manageAssociationName)
{
widgetMetaData.withDefaultValue("manageAssociationName", manageAssociationName);
return (this);
}
}
@ -178,52 +189,60 @@ public class ChildRecordListRenderer extends AbstractWidgetRenderer
maxRows = ValueUtils.getValueAsInteger(input.getWidgetMetaData().getDefaultValues().containsKey("maxRows"));
}
////////////////////////////////////////////////////////
// fetch the record that we're getting children for. //
// e.g., the left-side of the join, with the input id //
////////////////////////////////////////////////////////
GetInput getInput = new GetInput();
getInput.setTableName(join.getLeftTable());
getInput.setPrimaryKey(id);
GetOutput getOutput = new GetAction().execute(getInput);
QRecord record = getOutput.getRecord();
if(record == null)
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// fetch the record that we're getting children for. e.g., the left-side of the join, with the input id //
// but - only try this if we were given an id. note, this widget could be called for on an INSERT screen, where we don't have a record yet //
// but we still want to be able to return all the other data in here that otherwise comes from the widget meta data, join, etc. //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
int totalRows = 0;
QRecord primaryRecord = null;
QQueryFilter filter = null;
QueryOutput queryOutput = new QueryOutput(new QueryInput());
if(StringUtils.hasContent(id))
{
throw (new QNotFoundException("Could not find " + (leftTable == null ? "" : leftTable.getLabel()) + " with primary key " + id));
}
GetInput getInput = new GetInput();
getInput.setTableName(join.getLeftTable());
getInput.setPrimaryKey(id);
GetOutput getOutput = new GetAction().execute(getInput);
primaryRecord = getOutput.getRecord();
////////////////////////////////////////////////////////////////////
// set up the query - for the table on the right side of the join //
////////////////////////////////////////////////////////////////////
QQueryFilter filter = new QQueryFilter();
for(JoinOn joinOn : join.getJoinOns())
{
filter.addCriteria(new QFilterCriteria(joinOn.getRightField(), QCriteriaOperator.EQUALS, List.of(record.getValue(joinOn.getLeftField()))));
}
filter.setOrderBys(join.getOrderBys());
filter.setLimit(maxRows);
if(primaryRecord == null)
{
throw (new QNotFoundException("Could not find " + (leftTable == null ? "" : leftTable.getLabel()) + " with primary key " + id));
}
QueryInput queryInput = new QueryInput();
queryInput.setTableName(join.getRightTable());
queryInput.setShouldTranslatePossibleValues(true);
queryInput.setShouldGenerateDisplayValues(true);
queryInput.setFilter(filter);
QueryOutput queryOutput = new QueryAction().execute(queryInput);
////////////////////////////////////////////////////////////////////
// set up the query - for the table on the right side of the join //
////////////////////////////////////////////////////////////////////
filter = new QQueryFilter();
for(JoinOn joinOn : join.getJoinOns())
{
filter.addCriteria(new QFilterCriteria(joinOn.getRightField(), QCriteriaOperator.EQUALS, List.of(primaryRecord.getValue(joinOn.getLeftField()))));
}
filter.setOrderBys(join.getOrderBys());
filter.setLimit(maxRows);
QValueFormatter.setBlobValuesToDownloadUrls(rightTable, queryOutput.getRecords());
QueryInput queryInput = new QueryInput();
queryInput.setTableName(join.getRightTable());
queryInput.setShouldTranslatePossibleValues(true);
queryInput.setShouldGenerateDisplayValues(true);
queryInput.setFilter(filter);
queryOutput = new QueryAction().execute(queryInput);
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(join.getRightTable());
countInput.setFilter(filter);
totalRows = new CountAction().execute(countInput).getCount();
QValueFormatter.setBlobValuesToDownloadUrls(rightTable, queryOutput.getRecords());
totalRows = queryOutput.getRecords().size();
if(maxRows != null && (queryOutput.getRecords().size() == maxRows))
{
/////////////////////////////////////////////////////////////////////////////////////
// if the input said to only do some max, and the # of results we got is that max, //
// then do a count query, for displaying 1-n of <count> //
/////////////////////////////////////////////////////////////////////////////////////
CountInput countInput = new CountInput();
countInput.setTableName(join.getRightTable());
countInput.setFilter(filter);
totalRows = new CountAction().execute(countInput).getCount();
}
}
String tablePath = input.getInstance().getTablePath(rightTable.getName());
@ -239,10 +258,14 @@ public class ChildRecordListRenderer extends AbstractWidgetRenderer
// new child records must have values from the join-ons //
//////////////////////////////////////////////////////////
Map<String, Serializable> defaultValuesForNewChildRecords = new HashMap<>();
for(JoinOn joinOn : join.getJoinOns())
if(primaryRecord != null)
{
defaultValuesForNewChildRecords.put(joinOn.getRightField(), record.getValue(joinOn.getLeftField()));
for(JoinOn joinOn : join.getJoinOns())
{
defaultValuesForNewChildRecords.put(joinOn.getRightField(), primaryRecord.getValue(joinOn.getLeftField()));
}
}
widgetData.setDefaultValuesForNewChildRecords(defaultValuesForNewChildRecords);
Map<String, Serializable> widgetValues = input.getWidgetMetaData().getDefaultValues();
@ -250,6 +273,22 @@ public class ChildRecordListRenderer extends AbstractWidgetRenderer
{
widgetData.setDisabledFieldsForNewChildRecords((Set<String>) widgetValues.get("disabledFieldsForNewChildRecords"));
}
else
{
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if there are no disabled fields specified - then normally any fields w/ a default value get implicitly disabled //
// but - if we didn't look-up the primary record, then we'll want to explicit disable fields from joins //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
if(primaryRecord == null)
{
Set<String> implicitlyDisabledFields = new HashSet<>();
widgetData.setDisabledFieldsForNewChildRecords(implicitlyDisabledFields);
for(JoinOn joinOn : join.getJoinOns())
{
implicitlyDisabledFields.add(joinOn.getRightField());
}
}
}
}
return (new RenderWidgetOutput(widgetData));

View File

@ -34,15 +34,18 @@ import com.kingsrook.qqq.backend.core.exceptions.QPermissionDeniedException;
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.AbstractTableActionInput;
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.dashboard.QWidgetMetaDataInterface;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.permissions.DenyBehavior;
import com.kingsrook.qqq.backend.core.model.metadata.permissions.MetaDataWithName;
import com.kingsrook.qqq.backend.core.model.metadata.permissions.MetaDataWithPermissionRules;
import com.kingsrook.qqq.backend.core.model.metadata.permissions.PermissionLevel;
import com.kingsrook.qqq.backend.core.model.metadata.permissions.QPermissionRules;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.Capability;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
@ -333,9 +336,25 @@ public class PermissionsHelper
QPermissionRules rules = getEffectivePermissionRules(tableMetaData, instance);
String baseName = getEffectivePermissionBaseName(rules, tableMetaData.getName());
for(TablePermissionSubType permissionSubType : TablePermissionSubType.values())
QBackendMetaData backend = instance.getBackend(tableMetaData.getBackendName());
if(tableMetaData.isCapabilityEnabled(backend, Capability.TABLE_INSERT))
{
addEffectiveAvailablePermission(rules, permissionSubType, rs, baseName, tableMetaData, "Table");
addEffectiveAvailablePermission(rules, TablePermissionSubType.INSERT, rs, baseName, tableMetaData, "Table");
}
if(tableMetaData.isCapabilityEnabled(backend, Capability.TABLE_UPDATE))
{
addEffectiveAvailablePermission(rules, TablePermissionSubType.EDIT, rs, baseName, tableMetaData, "Table");
}
if(tableMetaData.isCapabilityEnabled(backend, Capability.TABLE_DELETE))
{
addEffectiveAvailablePermission(rules, TablePermissionSubType.DELETE, rs, baseName, tableMetaData, "Table");
}
if(tableMetaData.isCapabilityEnabled(backend, Capability.TABLE_QUERY) || tableMetaData.isCapabilityEnabled(backend, Capability.TABLE_GET))
{
addEffectiveAvailablePermission(rules, TablePermissionSubType.READ, rs, baseName, tableMetaData, "Table");
}
}
@ -369,7 +388,10 @@ public class PermissionsHelper
{
QPermissionRules rules = getEffectivePermissionRules(widgetMetaData, instance);
String baseName = getEffectivePermissionBaseName(rules, widgetMetaData.getName());
addEffectiveAvailablePermission(rules, PrivatePermissionSubType.HAS_ACCESS, rs, baseName, widgetMetaData, "Widget");
if(!rules.getLevel().equals(PermissionLevel.NOT_PROTECTED))
{
addEffectiveAvailablePermission(rules, PrivatePermissionSubType.HAS_ACCESS, rs, baseName, widgetMetaData, "Widget");
}
}
return (rs);

View File

@ -497,10 +497,10 @@ public class RunProcessAction
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if backend specifies that it uses variants, look for that data in the session and append to our basepull key //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
if(process.getSchedule() != null && process.getSchedule().getVariantBackend() != null)
if(process.getSchedule() != null && process.getVariantBackend() != null)
{
QSession session = QContext.getQSession();
QBackendMetaData backendMetaData = QContext.getQInstance().getBackend(process.getSchedule().getVariantBackend());
QBackendMetaData backendMetaData = QContext.getQInstance().getBackend(process.getVariantBackend());
if(session.getBackendVariants() == null || !session.getBackendVariants().containsKey(backendMetaData.getVariantOptionsTableTypeValue()))
{
LOG.info("Could not find Backend Variant information for Backend '" + backendMetaData.getName() + "'");

View File

@ -73,6 +73,7 @@ import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.aggregates.AggregatesInterface;
import com.kingsrook.qqq.backend.core.utils.aggregates.BigDecimalAggregates;
import com.kingsrook.qqq.backend.core.utils.aggregates.IntegerAggregates;
import com.kingsrook.qqq.backend.core.utils.aggregates.LongAggregates;
/*******************************************************************************
@ -273,7 +274,7 @@ public class GenerateReportAction
RunBackendStepOutput transformStepOutput = null;
if(tableView != null && tableView.getRecordTransformStep() != null)
{
transformStep = QCodeLoader.getBackendStep(AbstractTransformStep.class, tableView.getRecordTransformStep());
transformStep = QCodeLoader.getAdHoc(AbstractTransformStep.class, tableView.getRecordTransformStep());
transformStepInput = new RunBackendStepInput();
transformStepInput.setValues(reportInput.getInputValues());
@ -553,6 +554,12 @@ public class GenerateReportAction
AggregatesInterface<Integer> fieldAggregates = (AggregatesInterface<Integer>) aggregatesMap.computeIfAbsent(field.getName(), (name) -> new IntegerAggregates());
fieldAggregates.add(record.getValueInteger(field.getName()));
}
else if(field.getType().equals(QFieldType.LONG))
{
@SuppressWarnings("unchecked")
AggregatesInterface<Long> fieldAggregates = (AggregatesInterface<Long>) aggregatesMap.computeIfAbsent(field.getName(), (name) -> new LongAggregates());
fieldAggregates.add(record.getValueLong(field.getName()));
}
else if(field.getType().equals(QFieldType.DECIMAL))
{
@SuppressWarnings("unchecked")

View File

@ -44,7 +44,8 @@ public class RecordPipe
private static final long BLOCKING_SLEEP_MILLIS = 100;
private static final long MAX_SLEEP_LOOP_MILLIS = 300_000; // 5 minutes
private ArrayBlockingQueue<QRecord> queue = new ArrayBlockingQueue<>(1_000);
private int capacity = 1_000;
private ArrayBlockingQueue<QRecord> queue = new ArrayBlockingQueue<>(capacity);
private boolean isTerminated = false;
@ -72,6 +73,7 @@ public class RecordPipe
*******************************************************************************/
public RecordPipe(Integer overrideCapacity)
{
this.capacity = overrideCapacity;
queue = new ArrayBlockingQueue<>(overrideCapacity);
}
@ -213,4 +215,14 @@ public class RecordPipe
this.postRecordActions = postRecordActions;
}
/*******************************************************************************
** Getter for capacity
**
*******************************************************************************/
public int getCapacity()
{
return capacity;
}
}

View File

@ -35,9 +35,8 @@ import java.util.Set;
import java.util.stream.Collectors;
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
import com.kingsrook.qqq.backend.core.actions.audits.DMLAuditAction;
import com.kingsrook.qqq.backend.core.actions.customizers.AbstractPostDeleteCustomizer;
import com.kingsrook.qqq.backend.core.actions.customizers.AbstractPreDeleteCustomizer;
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
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.interfaces.DeleteInterface;
import com.kingsrook.qqq.backend.core.actions.tables.helpers.ValidateRecordSecurityLockHelper;
@ -250,7 +249,7 @@ public class DeleteAction
//////////////////////////////////////////////////////////////
// finally, run the post-delete customizer, if there is one //
//////////////////////////////////////////////////////////////
Optional<AbstractPostDeleteCustomizer> postDeleteCustomizer = QCodeLoader.getTableCustomizer(AbstractPostDeleteCustomizer.class, table, TableCustomizers.POST_DELETE_RECORD.getRole());
Optional<TableCustomizerInterface> postDeleteCustomizer = QCodeLoader.getTableCustomizer(table, TableCustomizers.POST_DELETE_RECORD.getRole());
if(postDeleteCustomizer.isPresent() && oldRecordList.isPresent())
{
////////////////////////////////////////////////////////////////////////////
@ -260,8 +259,7 @@ public class DeleteAction
try
{
postDeleteCustomizer.get().setDeleteInput(deleteInput);
List<QRecord> postCustomizerResult = postDeleteCustomizer.get().apply(recordsForCustomizer);
List<QRecord> postCustomizerResult = postDeleteCustomizer.get().postDelete(deleteInput, recordsForCustomizer);
///////////////////////////////////////////////////////
// check if any records got errors in the customizer //
@ -327,13 +325,11 @@ public class DeleteAction
///////////////////////////////////////////////////////////////////////////
// after all validations, run the pre-delete customizer, if there is one //
///////////////////////////////////////////////////////////////////////////
Optional<AbstractPreDeleteCustomizer> preDeleteCustomizer = QCodeLoader.getTableCustomizer(AbstractPreDeleteCustomizer.class, table, TableCustomizers.PRE_DELETE_RECORD.getRole());
Optional<TableCustomizerInterface> preDeleteCustomizer = QCodeLoader.getTableCustomizer(table, TableCustomizers.PRE_DELETE_RECORD.getRole());
List<QRecord> customizerResult = oldRecordList.get();
if(preDeleteCustomizer.isPresent())
{
preDeleteCustomizer.get().setDeleteInput(deleteInput);
preDeleteCustomizer.get().setIsPreview(isPreview);
customizerResult = preDeleteCustomizer.get().apply(oldRecordList.get());
customizerResult = preDeleteCustomizer.get().preDelete(deleteInput, oldRecordList.get(), isPreview);
}
/////////////////////////////////////////////////////////////////////////

View File

@ -27,8 +27,8 @@ import java.util.List;
import java.util.Map;
import java.util.Optional;
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
import com.kingsrook.qqq.backend.core.actions.customizers.AbstractPostQueryCustomizer;
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
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.interfaces.GetInterface;
import com.kingsrook.qqq.backend.core.actions.tables.helpers.GetActionCacheHelper;
@ -49,6 +49,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
import com.kingsrook.qqq.backend.core.utils.ObjectUtils;
/*******************************************************************************
@ -57,7 +58,7 @@ import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
*******************************************************************************/
public class GetAction
{
private Optional<AbstractPostQueryCustomizer> postGetRecordCustomizer;
private Optional<TableCustomizerInterface> postGetRecordCustomizer;
private GetInput getInput;
private QPossibleValueTranslator qPossibleValueTranslator;
@ -87,7 +88,7 @@ public class GetAction
throw (new QException("Requested to Get a record from an unrecognized table: " + getInput.getTableName()));
}
postGetRecordCustomizer = QCodeLoader.getTableCustomizer(AbstractPostQueryCustomizer.class, table, TableCustomizers.POST_QUERY_RECORD.getRole());
postGetRecordCustomizer = QCodeLoader.getTableCustomizer(table, TableCustomizers.POST_QUERY_RECORD.getRole());
this.getInput = getInput;
QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher();
@ -107,9 +108,11 @@ public class GetAction
}
GetOutput getOutput;
boolean usingDefaultGetInterface = false;
if(getInterface == null)
{
getInterface = new DefaultGetInterface();
usingDefaultGetInterface = true;
}
getInterface.validateInput(getInput);
@ -123,10 +126,11 @@ public class GetAction
new GetActionCacheHelper().handleCaching(getInput, getOutput);
}
////////////////////////////////////////////////////////
// if the record is found, perform post-actions on it //
////////////////////////////////////////////////////////
if(getOutput.getRecord() != null)
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if the record is found, perform post-actions on it //
// unless the defaultGetInterface was used - as it just does a query, and the query will do the post-actions. //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
if(getOutput.getRecord() != null && !usingDefaultGetInterface)
{
getOutput.setRecord(postRecordActions(getOutput.getRecord()));
}
@ -202,7 +206,7 @@ public class GetAction
}
else
{
throw (new QException("No primaryKey or uniqueKey was passed to Get"));
throw (new QException("Unable to get " + ObjectUtils.tryElse(() -> queryInput.getTable().getLabel(), queryInput.getTableName()) + ". Missing required input."));
}
queryInput.setFilter(filter);
@ -216,12 +220,12 @@ public class GetAction
** Run the necessary actions on a record. This may include setting display values,
** translating possible values, and running post-record customizations.
*******************************************************************************/
public QRecord postRecordActions(QRecord record)
public QRecord postRecordActions(QRecord record) throws QException
{
QRecord returnRecord = record;
if(this.postGetRecordCustomizer.isPresent())
{
returnRecord = postGetRecordCustomizer.get().apply(List.of(record)).get(0);
returnRecord = postGetRecordCustomizer.get().postQuery(getInput, List.of(record)).get(0);
}
if(getInput.getShouldTranslatePossibleValues())

View File

@ -28,6 +28,7 @@ import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
@ -37,9 +38,9 @@ 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;
import com.kingsrook.qqq.backend.core.actions.customizers.AbstractPostInsertCustomizer;
import com.kingsrook.qqq.backend.core.actions.customizers.AbstractPreInsertCustomizer;
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
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.interfaces.InsertInterface;
import com.kingsrook.qqq.backend.core.actions.tables.helpers.UniqueKeyHelper;
@ -168,13 +169,12 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
//////////////////////////////////////////////////////////////
// finally, run the post-insert customizer, if there is one //
//////////////////////////////////////////////////////////////
Optional<AbstractPostInsertCustomizer> postInsertCustomizer = QCodeLoader.getTableCustomizer(AbstractPostInsertCustomizer.class, table, TableCustomizers.POST_INSERT_RECORD.getRole());
Optional<TableCustomizerInterface> postInsertCustomizer = QCodeLoader.getTableCustomizer(table, TableCustomizers.POST_INSERT_RECORD.getRole());
if(postInsertCustomizer.isPresent())
{
try
{
postInsertCustomizer.get().setInsertInput(insertInput);
insertOutput.setRecords(postInsertCustomizer.get().apply(insertOutput.getRecords()));
insertOutput.setRecords(postInsertCustomizer.get().postInsert(insertInput, insertOutput.getRecords()));
}
catch(Exception e)
{
@ -232,31 +232,29 @@ 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 //
///////////////////////////////////////////////////////////////////
Optional<AbstractPreInsertCustomizer> preInsertCustomizer = QCodeLoader.getTableCustomizer(AbstractPreInsertCustomizer.class, table, TableCustomizers.PRE_INSERT_RECORD.getRole());
Optional<TableCustomizerInterface> preInsertCustomizer = QCodeLoader.getTableCustomizer(table, TableCustomizers.PRE_INSERT_RECORD.getRole());
if(preInsertCustomizer.isPresent())
{
preInsertCustomizer.get().setInsertInput(insertInput);
preInsertCustomizer.get().setIsPreview(isPreview);
runPreInsertCustomizerIfItIsTime(insertInput, preInsertCustomizer, AbstractPreInsertCustomizer.WhenToRun.BEFORE_ALL_VALIDATIONS);
runPreInsertCustomizerIfItIsTime(insertInput, isPreview, preInsertCustomizer, AbstractPreInsertCustomizer.WhenToRun.BEFORE_ALL_VALIDATIONS);
}
setDefaultValuesInRecords(table, insertInput.getRecords());
ValueBehaviorApplier.applyFieldBehaviors(ValueBehaviorApplier.Action.INSERT, insertInput.getInstance(), table, insertInput.getRecords());
ValueBehaviorApplier.applyFieldBehaviors(ValueBehaviorApplier.Action.INSERT, insertInput.getInstance(), table, insertInput.getRecords(), null);
runPreInsertCustomizerIfItIsTime(insertInput, preInsertCustomizer, AbstractPreInsertCustomizer.WhenToRun.BEFORE_UNIQUE_KEY_CHECKS);
runPreInsertCustomizerIfItIsTime(insertInput, isPreview, preInsertCustomizer, AbstractPreInsertCustomizer.WhenToRun.BEFORE_UNIQUE_KEY_CHECKS);
setErrorsIfUniqueKeyErrors(insertInput, table);
runPreInsertCustomizerIfItIsTime(insertInput, preInsertCustomizer, AbstractPreInsertCustomizer.WhenToRun.BEFORE_REQUIRED_FIELD_CHECKS);
runPreInsertCustomizerIfItIsTime(insertInput, isPreview, preInsertCustomizer, AbstractPreInsertCustomizer.WhenToRun.BEFORE_REQUIRED_FIELD_CHECKS);
if(insertInput.getInputSource().shouldValidateRequiredFields())
{
validateRequiredFields(insertInput);
}
runPreInsertCustomizerIfItIsTime(insertInput, preInsertCustomizer, AbstractPreInsertCustomizer.WhenToRun.BEFORE_SECURITY_CHECKS);
runPreInsertCustomizerIfItIsTime(insertInput, isPreview, preInsertCustomizer, AbstractPreInsertCustomizer.WhenToRun.BEFORE_SECURITY_CHECKS);
ValidateRecordSecurityLockHelper.validateSecurityFields(insertInput.getTable(), insertInput.getRecords(), ValidateRecordSecurityLockHelper.Action.INSERT);
runPreInsertCustomizerIfItIsTime(insertInput, preInsertCustomizer, AbstractPreInsertCustomizer.WhenToRun.AFTER_ALL_VALIDATIONS);
runPreInsertCustomizerIfItIsTime(insertInput, isPreview, preInsertCustomizer, AbstractPreInsertCustomizer.WhenToRun.AFTER_ALL_VALIDATIONS);
}
@ -290,13 +288,13 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
/*******************************************************************************
**
*******************************************************************************/
private void runPreInsertCustomizerIfItIsTime(InsertInput insertInput, Optional<AbstractPreInsertCustomizer> preInsertCustomizer, AbstractPreInsertCustomizer.WhenToRun whenToRun) throws QException
private void runPreInsertCustomizerIfItIsTime(InsertInput insertInput, boolean isPreview, Optional<TableCustomizerInterface> preInsertCustomizer, AbstractPreInsertCustomizer.WhenToRun whenToRun) throws QException
{
if(preInsertCustomizer.isPresent())
{
if(whenToRun.equals(preInsertCustomizer.get().getWhenToRun()))
if(whenToRun.equals(preInsertCustomizer.get().whenToRunPreInsert(insertInput, isPreview)))
{
insertInput.setRecords(preInsertCustomizer.get().apply(insertInput.getRecords()));
insertInput.setRecords(preInsertCustomizer.get().preInsert(insertInput, insertInput.getRecords(), isPreview));
}
}
}
@ -321,7 +319,7 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
{
if(record.getValue(requiredField.getName()) == null || (requiredField.getType().isStringLike() && record.getValueString(requiredField.getName()).trim().equals("")))
{
record.addError(new BadInputStatusMessage("Missing value in required field: " + requiredField.getLabel()));
record.addError(new BadInputStatusMessage("Missing value in required field: " + Objects.requireNonNullElse(requiredField.getLabel(), requiredField.getName())));
}
}
}
@ -455,7 +453,7 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
private QBackendModuleInterface getBackendModuleInterface(QBackendMetaData backend) throws QException
{
QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher();
QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(backend);
QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(backend);
return (qModule);
}

View File

@ -31,8 +31,8 @@ import java.util.Map;
import java.util.Optional;
import java.util.Set;
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
import com.kingsrook.qqq.backend.core.actions.customizers.AbstractPostQueryCustomizer;
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
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.interfaces.QueryInterface;
import com.kingsrook.qqq.backend.core.actions.reporting.BufferedRecordPipe;
@ -73,7 +73,7 @@ public class QueryAction
{
private static final QLogger LOG = QLogger.getLogger(QueryAction.class);
private Optional<AbstractPostQueryCustomizer> postQueryRecordCustomizer;
private Optional<TableCustomizerInterface> postQueryRecordCustomizer;
private QueryInput queryInput;
private QueryInterface queryInterface;
@ -100,7 +100,7 @@ public class QueryAction
}
QBackendMetaData backend = queryInput.getBackend();
postQueryRecordCustomizer = QCodeLoader.getTableCustomizer(AbstractPostQueryCustomizer.class, table, TableCustomizers.POST_QUERY_RECORD.getRole());
postQueryRecordCustomizer = QCodeLoader.getTableCustomizer(table, TableCustomizers.POST_QUERY_RECORD.getRole());
this.queryInput = queryInput;
if(queryInput.getRecordPipe() != null)
@ -264,7 +264,7 @@ public class QueryAction
{
if(this.postQueryRecordCustomizer.isPresent())
{
records = postQueryRecordCustomizer.get().apply(records);
records = postQueryRecordCustomizer.get().postQuery(queryInput, records);
}
if(queryInput.getShouldTranslatePossibleValues())

View File

@ -136,19 +136,22 @@ public class ReplaceAction extends AbstractQActionFunction<ReplaceInput, Replace
UpdateOutput updateOutput = new UpdateAction().execute(updateInput);
output.setUpdateOutput(updateOutput);
QQueryFilter deleteFilter = new QQueryFilter(new QFilterCriteria(primaryKeyField, QCriteriaOperator.NOT_IN, primaryKeysToKeep));
if(input.getFilter() != null)
if(input.getPerformDeletes())
{
deleteFilter.addSubFilter(input.getFilter());
}
QQueryFilter deleteFilter = new QQueryFilter(new QFilterCriteria(primaryKeyField, QCriteriaOperator.NOT_IN, primaryKeysToKeep));
if(input.getFilter() != null)
{
deleteFilter.addSubFilter(input.getFilter());
}
DeleteInput deleteInput = new DeleteInput();
deleteInput.setTableName(table.getName());
deleteInput.setQueryFilter(deleteFilter);
deleteInput.setTransaction(transaction);
deleteInput.setOmitDmlAudit(input.getOmitDmlAudit());
DeleteOutput deleteOutput = new DeleteAction().execute(deleteInput);
output.setDeleteOutput(deleteOutput);
DeleteInput deleteInput = new DeleteInput();
deleteInput.setTableName(table.getName());
deleteInput.setQueryFilter(deleteFilter);
deleteInput.setTransaction(transaction);
deleteInput.setOmitDmlAudit(input.getOmitDmlAudit());
DeleteOutput deleteOutput = new DeleteAction().execute(deleteInput);
output.setDeleteOutput(deleteOutput);
}
if(weOwnTheTransaction)
{

View File

@ -34,9 +34,8 @@ import com.kingsrook.qqq.backend.core.actions.ActionHelper;
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;
import com.kingsrook.qqq.backend.core.actions.customizers.AbstractPostUpdateCustomizer;
import com.kingsrook.qqq.backend.core.actions.customizers.AbstractPreUpdateCustomizer;
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
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.interfaces.UpdateInterface;
import com.kingsrook.qqq.backend.core.actions.tables.helpers.ValidateRecordSecurityLockHelper;
@ -57,6 +56,8 @@ 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.fields.DynamicDefaultValueBehavior;
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.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn;
@ -72,6 +73,7 @@ import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import org.apache.commons.lang.BooleanUtils;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
@ -189,14 +191,12 @@ public class UpdateAction
//////////////////////////////////////////////////////////////
// finally, run the post-update customizer, if there is one //
//////////////////////////////////////////////////////////////
Optional<AbstractPostUpdateCustomizer> postUpdateCustomizer = QCodeLoader.getTableCustomizer(AbstractPostUpdateCustomizer.class, table, TableCustomizers.POST_UPDATE_RECORD.getRole());
Optional<TableCustomizerInterface> postUpdateCustomizer = QCodeLoader.getTableCustomizer(table, TableCustomizers.POST_UPDATE_RECORD.getRole());
if(postUpdateCustomizer.isPresent())
{
try
{
postUpdateCustomizer.get().setUpdateInput(updateInput);
oldRecordList.ifPresent(l -> postUpdateCustomizer.get().setOldRecordList(l));
updateOutput.setRecords(postUpdateCustomizer.get().apply(updateOutput.getRecords()));
updateOutput.setRecords(postUpdateCustomizer.get().postUpdate(updateInput, updateOutput.getRecords(), oldRecordList));
}
catch(Exception e)
{
@ -244,7 +244,13 @@ public class UpdateAction
/////////////////////////////
// run standard validators //
/////////////////////////////
ValueBehaviorApplier.applyFieldBehaviors(ValueBehaviorApplier.Action.UPDATE, updateInput.getInstance(), table, updateInput.getRecords());
Set<FieldBehavior<?>> behaviorsToOmit = null;
if(BooleanUtils.isTrue(updateInput.getOmitModifyDateUpdate()))
{
behaviorsToOmit = Set.of(DynamicDefaultValueBehavior.MODIFY_DATE);
}
ValueBehaviorApplier.applyFieldBehaviors(ValueBehaviorApplier.Action.UPDATE, updateInput.getInstance(), table, updateInput.getRecords(), behaviorsToOmit);
validatePrimaryKeysAreGiven(updateInput);
if(oldRecordList.isPresent())
@ -264,13 +270,10 @@ public class UpdateAction
///////////////////////////////////////////////////////////////////////////
// after all validations, run the pre-update customizer, if there is one //
///////////////////////////////////////////////////////////////////////////
Optional<AbstractPreUpdateCustomizer> preUpdateCustomizer = QCodeLoader.getTableCustomizer(AbstractPreUpdateCustomizer.class, table, TableCustomizers.PRE_UPDATE_RECORD.getRole());
Optional<TableCustomizerInterface> preUpdateCustomizer = QCodeLoader.getTableCustomizer(table, TableCustomizers.PRE_UPDATE_RECORD.getRole());
if(preUpdateCustomizer.isPresent())
{
preUpdateCustomizer.get().setUpdateInput(updateInput);
preUpdateCustomizer.get().setIsPreview(isPreview);
oldRecordList.ifPresent(l -> preUpdateCustomizer.get().setOldRecordList(l));
updateInput.setRecords(preUpdateCustomizer.get().apply(updateInput.getRecords()));
updateInput.setRecords(preUpdateCustomizer.get().preUpdate(updateInput, updateInput.getRecords(), isPreview, oldRecordList));
}
}

View File

@ -269,6 +269,10 @@ public class QPossibleValueTranslator
{
value = ValueUtils.getValueAsInteger(value);
}
if(field.getType().equals(QFieldType.LONG) && !(value instanceof Long))
{
value = ValueUtils.getValueAsLong(value);
}
}
catch(QValueException e)
{

View File

@ -490,6 +490,13 @@ public class QValueFormatter
{
adornmentValues = fileDownloadAdornment.get().getValues();
}
else
{
///////////////////////////////////////////////////////
// don't change blobs unless they are file-downloads //
///////////////////////////////////////////////////////
continue;
}
String fileNameField = ValueUtils.getValueAsString(adornmentValues.get(AdornmentType.FileDownloadValues.FILE_NAME_FIELD));
String fileNameFormat = ValueUtils.getValueAsString(adornmentValues.get(AdornmentType.FileDownloadValues.FILE_NAME_FORMAT));

View File

@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.core.actions.values;
import java.util.List;
import java.util.Set;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldBehavior;
@ -32,7 +33,7 @@ import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
/*******************************************************************************
** Utility class to apply value behaviors to records.
** Utility class to apply value behaviors to records.
*******************************************************************************/
public class ValueBehaviorApplier
{
@ -51,7 +52,7 @@ public class ValueBehaviorApplier
/*******************************************************************************
**
*******************************************************************************/
public static void applyFieldBehaviors(Action action, QInstance instance, QTableMetaData table, List<QRecord> recordList)
public static void applyFieldBehaviors(Action action, QInstance instance, QTableMetaData table, List<QRecord> recordList, Set<FieldBehavior<?>> behaviorsToOmit)
{
if(CollectionUtils.nullSafeIsEmpty(recordList))
{
@ -62,7 +63,7 @@ public class ValueBehaviorApplier
{
for(FieldBehavior<?> fieldBehavior : CollectionUtils.nonNullCollection(field.getBehaviors()))
{
fieldBehavior.apply(action, recordList, instance, table, field);
fieldBehavior.apply(action, recordList, instance, table, field, behaviorsToOmit);
}
}
}

View File

@ -34,5 +34,11 @@ import com.kingsrook.qqq.backend.core.model.session.QSession;
*******************************************************************************/
public record CapturedContext(QInstance qInstance, QSession qSession, QBackendTransaction qBackendTransaction, Stack<AbstractActionInput> actionStack)
{
/*******************************************************************************
** Simpler constructor
*******************************************************************************/
public CapturedContext(QInstance qInstance, QSession qSession)
{
this(qInstance, qSession, null, null);
}
}

View File

@ -22,6 +22,9 @@
package com.kingsrook.qqq.backend.core.context;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.Stack;
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
@ -31,6 +34,8 @@ import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.utils.lambdas.UnsafeVoidVoidMethod;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
@ -47,6 +52,8 @@ public class QContext
private static ThreadLocal<QBackendTransaction> qBackendTransactionThreadLocal = new ThreadLocal<>();
private static ThreadLocal<Stack<AbstractActionInput>> actionStackThreadLocal = new ThreadLocal<>();
private static ThreadLocal<Map<String, Serializable>> objectsThreadLocal = new ThreadLocal<>();
/*******************************************************************************
@ -100,6 +107,25 @@ public class QContext
/*******************************************************************************
**
*******************************************************************************/
public static <T extends Throwable> void withTemporaryContext(CapturedContext context, UnsafeVoidVoidMethod<T> method) throws T
{
CapturedContext originalContext = QContext.capture();
try
{
QContext.init(context);
method.run();
}
finally
{
QContext.init(originalContext);
}
}
/*******************************************************************************
** Init a new thread with the context captured from a different thread. e.g.,
** when starting some async task.
@ -132,6 +158,7 @@ public class QContext
qSessionThreadLocal.remove();
qBackendTransactionThreadLocal.remove();
actionStackThreadLocal.remove();
objectsThreadLocal.remove();
}
@ -259,4 +286,94 @@ public class QContext
return (Optional.of(actionStackThreadLocal.get().get(0)));
}
/*******************************************************************************
** get one named object from the Context for the current thread. may return null.
*******************************************************************************/
public static Serializable getObject(String key)
{
if(objectsThreadLocal.get() == null)
{
return null;
}
return objectsThreadLocal.get().get(key);
}
/*******************************************************************************
** get one named object from the Context for the current thread, cast to the
** specified type if possible. if not found, or wrong type, empty is returned.
*******************************************************************************/
public static <T extends Serializable> Optional<T> getObject(String key, Class<T> type)
{
Serializable object = getObject(key);
if(type.isInstance(object))
{
return Optional.of(type.cast(object));
}
else if(object == null)
{
return Optional.empty();
}
else
{
LOG.warn("Unexpected type of object found in session under key [" + key + "]",
logPair("expectedType", type.getName()),
logPair("actualType", object.getClass().getName())
);
return Optional.empty();
}
}
/*******************************************************************************
** put a named object into the Context for the current thread.
*******************************************************************************/
public static void setObject(String key, Serializable object)
{
if(objectsThreadLocal.get() == null)
{
objectsThreadLocal.set(new HashMap<>());
}
objectsThreadLocal.get().put(key, object);
}
/*******************************************************************************
** remove a named object from the Context of the current thread.
*******************************************************************************/
public static void removeObject(String key)
{
if(objectsThreadLocal.get() != null)
{
objectsThreadLocal.get().remove(key);
}
}
/*******************************************************************************
** get the full map of named objects for the current thread (possibly null).
*******************************************************************************/
public static Map<String, Serializable> getObjects()
{
return objectsThreadLocal.get();
}
/*******************************************************************************
** fully replace the map of named objets for the current thread.
*******************************************************************************/
public static void setObjects(Map<String, Serializable> objects)
{
objectsThreadLocal.set(objects);
}
}

View File

@ -1030,6 +1030,50 @@ public class QInstanceEnricher
/*******************************************************************************
** Do a default mapping from an underscore_style field name to a camelCase name.
**
** Examples:
** <ul>
** <li>word_another_word_more_words -> wordAnotherWordMoreWords</li>
** <li>l_ul_ul_ul -> lUlUlUl</li>
** <li>tla_first -> tlaFirst</li>
** <li>word_then_tla_in_middle -> wordThenTlaInMiddle</li>
** <li>end_with_tla -> endWithTla</li>
** <li>tla_and_another_tla -> tlaAndAnotherTla</li>
** <li>ALL_CAPS -> allCaps</li>
** </ul>
*******************************************************************************/
public static String inferNameFromBackendName(String backendName)
{
StringBuilder rs = new StringBuilder();
////////////////////////////////////////////////////////////////////////////////////////
// build a list of words in the name, then join them with _ and lower-case the result //
////////////////////////////////////////////////////////////////////////////////////////
String[] words = backendName.toLowerCase(Locale.ROOT).split("_");
for(int i = 0; i < words.length; i++)
{
String word = words[i];
if(i == 0)
{
rs.append(word);
}
else
{
rs.append(word.substring(0, 1).toUpperCase());
if(word.length() > 1)
{
rs.append(word.substring(1));
}
}
}
return (rs.toString());
}
/*******************************************************************************
** If a app didn't have any sections, generate "sensible defaults"
*******************************************************************************/

View File

@ -30,12 +30,14 @@ 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.dashboard.QWidgetMetaDataInterface;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.help.HelpFormat;
import com.kingsrook.qqq.backend.core.model.metadata.help.HelpRole;
@ -109,6 +111,8 @@ public class QInstanceHelpContentManager
String processName = nameValuePairs.get("process");
String fieldName = nameValuePairs.get("field");
String sectionName = nameValuePairs.get("section");
String widgetName = nameValuePairs.get("widget");
String slotName = nameValuePairs.get("slot");
///////////////////////////////////////////////////////////
// build a help content meta-data object from the record //
@ -137,89 +141,16 @@ public class QInstanceHelpContentManager
///////////////////////////////////////////////////////////////////////////////////
if(StringUtils.hasContent(tableName))
{
QTableMetaData table = qInstance.getTable(tableName);
if(table == null)
{
LOG.info("Unrecognized table in help content", logPair("key", key));
return;
}
if(StringUtils.hasContent(fieldName))
{
//////////////////////////
// handle a table field //
//////////////////////////
QFieldMetaData field = table.getFields().get(fieldName);
if(field == null)
{
LOG.info("Unrecognized table field in help content", logPair("key", key));
return;
}
if(helpContent != null)
{
field.withHelpContent(helpContent);
}
else
{
field.removeHelpContent(roles);
}
}
else if(StringUtils.hasContent(sectionName))
{
////////////////////////////
// handle a table section //
////////////////////////////
Optional<QFieldSection> optionalSection = table.getSections().stream().filter(s -> sectionName.equals(s.getName())).findFirst();
if(optionalSection.isEmpty())
{
LOG.info("Unrecognized table section in help content", logPair("key", key));
return;
}
if(helpContent != null)
{
optionalSection.get().withHelpContent(helpContent);
}
else
{
optionalSection.get().removeHelpContent(roles);
}
}
processHelpContentForTable(key, tableName, sectionName, fieldName, roles, helpContent);
}
else if(StringUtils.hasContent(processName))
{
QProcessMetaData process = qInstance.getProcess(processName);
if(process == null)
{
LOG.info("Unrecognized process in help content", logPair("key", key));
return;
}
processHelpContentForProcess(key, processName, fieldName, roles, helpContent);
}
else if(StringUtils.hasContent(widgetName))
{
processHelpContentForWidget(key, widgetName, slotName, helpContent);
if(StringUtils.hasContent(fieldName))
{
////////////////////////////
// handle a process field //
////////////////////////////
Optional<QFieldMetaData> optionalField = CollectionUtils.mergeLists(process.getInputFields(), process.getOutputFields())
.stream().filter(f -> fieldName.equals(f.getName()))
.findFirst();
if(optionalField.isEmpty())
{
LOG.info("Unrecognized process field in help content", logPair("key", key));
return;
}
if(helpContent != null)
{
optionalField.get().withHelpContent(helpContent);
}
else
{
optionalField.get().removeHelpContent(roles);
}
}
}
}
catch(Exception e)
@ -230,6 +161,131 @@ public class QInstanceHelpContentManager
/*******************************************************************************
**
*******************************************************************************/
private static void processHelpContentForTable(String key, String tableName, String sectionName, String fieldName, Set<HelpRole> roles, QHelpContent helpContent)
{
QTableMetaData table = QContext.getQInstance().getTable(tableName);
if(table == null)
{
LOG.info("Unrecognized table in help content", logPair("key", key));
}
else if(StringUtils.hasContent(fieldName))
{
//////////////////////////
// handle a table field //
//////////////////////////
QFieldMetaData field = table.getFields().get(fieldName);
if(field == null)
{
LOG.info("Unrecognized table field in help content", logPair("key", key));
}
else if(helpContent != null)
{
field.withHelpContent(helpContent);
}
else
{
field.removeHelpContent(roles);
}
}
else if(StringUtils.hasContent(sectionName))
{
////////////////////////////
// handle a table section //
////////////////////////////
Optional<QFieldSection> optionalSection = table.getSections().stream().filter(s -> sectionName.equals(s.getName())).findFirst();
if(optionalSection.isEmpty())
{
LOG.info("Unrecognized table section in help content", logPair("key", key));
}
else if(helpContent != null)
{
optionalSection.get().withHelpContent(helpContent);
}
else
{
optionalSection.get().removeHelpContent(roles);
}
}
}
/*******************************************************************************
**
*******************************************************************************/
private static void processHelpContentForProcess(String key, String processName, String fieldName, Set<HelpRole> roles, QHelpContent helpContent)
{
QProcessMetaData process = QContext.getQInstance().getProcess(processName);
if(process == null)
{
LOG.info("Unrecognized process in help content", logPair("key", key));
}
else if(StringUtils.hasContent(fieldName))
{
////////////////////////////
// handle a process field //
////////////////////////////
Optional<QFieldMetaData> optionalField = CollectionUtils.mergeLists(process.getInputFields(), process.getOutputFields())
.stream().filter(f -> fieldName.equals(f.getName()))
.findFirst();
if(optionalField.isEmpty())
{
LOG.info("Unrecognized process field in help content", logPair("key", key));
}
else if(helpContent != null)
{
optionalField.get().withHelpContent(helpContent);
}
else
{
optionalField.get().removeHelpContent(roles);
}
}
}
/*******************************************************************************
**
*******************************************************************************/
private static void processHelpContentForWidget(String key, String widgetName, String slotName, QHelpContent helpContent)
{
QWidgetMetaDataInterface widget = QContext.getQInstance().getWidget(widgetName);
if(!StringUtils.hasContent(slotName))
{
LOG.info("Missing slot name in help content", logPair("key", key));
}
else if(widget == null)
{
LOG.info("Unrecognized widget in help content", logPair("key", key));
}
else
{
Map<String, QHelpContent> widgetHelpContent = widget.getHelpContent();
if(widgetHelpContent == null)
{
widgetHelpContent = new HashMap<>();
}
if(helpContent != null)
{
widgetHelpContent.put(slotName, helpContent);
}
else
{
widgetHelpContent.remove(slotName);
}
widget.setHelpContent(widgetHelpContent);
}
}
/*******************************************************************************
** add a help content object to a list - replacing an entry in the list with the
** same roles if one is found.

View File

@ -24,8 +24,11 @@ package com.kingsrook.qqq.backend.core.instances;
import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
@ -33,15 +36,18 @@ import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.TimeZone;
import java.util.function.Supplier;
import java.util.stream.Stream;
import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationHandler;
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
import com.kingsrook.qqq.backend.core.actions.dashboard.widgets.AbstractWidgetRenderer;
import com.kingsrook.qqq.backend.core.actions.metadata.JoinGraph;
import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
import com.kingsrook.qqq.backend.core.actions.scripts.TestScriptActionInterface;
import com.kingsrook.qqq.backend.core.actions.values.QCustomPossibleValueProvider;
import com.kingsrook.qqq.backend.core.exceptions.QInstanceValidationException;
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.query.QFilterCriteria;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy;
@ -51,8 +57,11 @@ 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.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.QCodeType;
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.ParentWidgetMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaDataInterface;
import com.kingsrook.qqq.backend.core.model.metadata.fields.AdornmentType;
import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldAdornment;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
@ -62,16 +71,21 @@ 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;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QSupplementalProcessMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.queues.QQueueMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.queues.QQueueProviderMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.queues.SQSQueueProviderMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportDataSource;
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportField;
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportView;
import com.kingsrook.qqq.backend.core.model.metadata.scheduleing.QScheduleMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.security.FieldSecurityLock;
import com.kingsrook.qqq.backend.core.model.metadata.security.QSecurityKeyType;
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
import com.kingsrook.qqq.backend.core.model.metadata.tables.AssociatedScript;
import com.kingsrook.qqq.backend.core.model.metadata.tables.Association;
@ -88,9 +102,11 @@ 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.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.UnsafeLambda;
import org.quartz.CronExpression;
/*******************************************************************************
@ -109,6 +125,8 @@ public class QInstanceValidator
private boolean printWarnings = false;
private static ListingHash<Class<?>, QInstanceValidatorPluginInterface<?>> validatorPlugins = new ListingHash<>();
private List<String> errors = new ArrayList<>();
@ -161,6 +179,7 @@ public class QInstanceValidator
validateProcesses(qInstance);
validateReports(qInstance);
validateApps(qInstance);
validateWidgets(qInstance);
validatePossibleValueSources(qInstance);
validateQueuesAndProviders(qInstance);
validateJoins(qInstance);
@ -168,6 +187,8 @@ public class QInstanceValidator
validateSupplementalMetaData(qInstance);
validateUniqueTopLevelNames(qInstance);
runPlugins(QInstance.class, qInstance, qInstance);
}
catch(Exception e)
{
@ -186,6 +207,57 @@ public class QInstanceValidator
/*******************************************************************************
**
*******************************************************************************/
public static void addValidatorPlugin(QInstanceValidatorPluginInterface<?> plugin)
{
Optional<Method> validateMethod = Arrays.stream(plugin.getClass().getDeclaredMethods())
.filter(m -> m.getName().equals("validate")
&& m.getParameterCount() == 3
&& !m.getParameterTypes()[0].equals(Object.class)
&& m.getParameterTypes()[1].equals(QInstance.class)
&& m.getParameterTypes()[2].equals(QInstanceValidator.class)
).findFirst();
if(validateMethod.isPresent())
{
Class<?> parameterType = validateMethod.get().getParameterTypes()[0];
validatorPlugins.add(parameterType, plugin);
}
else
{
LOG.warn("Could not find validate method on validator plugin [" + plugin.getClass().getName() + "] (to infer type being validated) - this plugin will not be used.");
}
}
/*******************************************************************************
**
*******************************************************************************/
public static void removeAllValidatorPlugins()
{
validatorPlugins.clear();
}
/*******************************************************************************
**
*******************************************************************************/
private <T> void runPlugins(Class<T> c, T t, QInstance qInstance)
{
for(QInstanceValidatorPluginInterface<?> plugin : CollectionUtils.nonNullList(validatorPlugins.get(c)))
{
@SuppressWarnings("unchecked")
QInstanceValidatorPluginInterface<T> processPlugin = (QInstanceValidatorPluginInterface<T>) plugin;
processPlugin.validate(t, qInstance, this);
}
}
/*******************************************************************************
**
*******************************************************************************/
@ -194,6 +266,8 @@ public class QInstanceValidator
for(QSupplementalInstanceMetaData supplementalInstanceMetaData : CollectionUtils.nonNullMap(qInstance.getSupplementalMetaData()).values())
{
supplementalInstanceMetaData.validate(qInstance, this);
runPlugins(QSupplementalInstanceMetaData.class, supplementalInstanceMetaData, qInstance);
}
}
@ -231,6 +305,8 @@ public class QInstanceValidator
{
assertCondition(qInstance.getPossibleValueSource(securityKeyType.getPossibleValueSourceName()) != null, "Unrecognized possibleValueSourceName in securityKeyType: " + name);
}
runPlugins(QSecurityKeyType.class, securityKeyType, qInstance);
}
});
}
@ -277,6 +353,8 @@ public class QInstanceValidator
assertNoException(() -> qInstance.getTable(join.getRightTable()).getField(orderBy.getFieldName()), "Field name " + orderBy.getFieldName() + " in orderBy for join " + joinName + " is not a defined field in the right-table " + join.getRightTable());
}
}
runPlugins(QJoinMetaData.class, join, qInstance);
});
}
@ -340,6 +418,8 @@ public class QInstanceValidator
assertCondition(StringUtils.hasContent(sqsQueueProvider.getBaseURL()), "Missing baseURL for SQSQueueProvider: " + name);
assertCondition(StringUtils.hasContent(sqsQueueProvider.getRegion()), "Missing region for SQSQueueProvider: " + name);
}
runPlugins(QQueueProviderMetaData.class, queueProvider, qInstance);
});
}
@ -354,6 +434,13 @@ public class QInstanceValidator
{
assertCondition(qInstance.getProcesses() != null && qInstance.getProcess(queue.getProcessName()) != null, "Unrecognized processName for queue: " + name);
}
if(queue.getSchedule() != null)
{
validateScheduleMetaData(queue.getSchedule(), qInstance, "SQSQueueProvider " + name + ", schedule: ");
}
runPlugins(QQueueMetaData.class, queue, qInstance);
});
}
}
@ -372,6 +459,8 @@ public class QInstanceValidator
assertCondition(Objects.equals(backendName, backend.getName()), "Inconsistent naming for backend: " + backendName + "/" + backend.getName() + ".");
backend.performValidation(this);
runPlugins(QBackendMetaData.class, backend, qInstance);
});
}
}
@ -389,6 +478,8 @@ public class QInstanceValidator
{
assertCondition(Objects.equals(name, automationProvider.getName()), "Inconsistent naming for automationProvider: " + name + "/" + automationProvider.getName() + ".");
assertCondition(automationProvider.getType() != null, "Missing type for automationProvider: " + name);
runPlugins(QAutomationProviderMetaData.class, automationProvider, qInstance);
});
}
}
@ -407,6 +498,8 @@ public class QInstanceValidator
{
validateSimpleCodeReference("Instance Authentication meta data customizer ", authentication.getCustomizer(), QAuthenticationModuleCustomizerInterface.class);
}
runPlugins(QAuthenticationMetaData.class, authentication, qInstance);
}
}
@ -529,6 +622,8 @@ public class QInstanceValidator
{
supplementalTableMetaData.validate(qInstance, table, this);
}
runPlugins(QTableMetaData.class, table, qInstance);
});
}
}
@ -926,6 +1021,11 @@ public class QInstanceValidator
assertCondition(qInstance.getAutomationProvider(providerName) != null, " has an unrecognized providerName: " + providerName);
}
if(automationDetails.getSchedule() != null)
{
validateScheduleMetaData(automationDetails.getSchedule(), qInstance, prefix + " automationDetails, schedule: ");
}
//////////////////////////////////
// validate the status tracking //
//////////////////////////////////
@ -1313,13 +1413,17 @@ public class QInstanceValidator
if(process.getSchedule() != null)
{
QScheduleMetaData schedule = process.getSchedule();
assertCondition(schedule.getRepeatMillis() != null || schedule.getRepeatSeconds() != null, "Either repeat millis or repeat seconds must be set on schedule in process " + processName);
validateScheduleMetaData(schedule, qInstance, "Process " + processName + ", schedule: ");
}
if(schedule.getVariantBackend() != null)
{
assertCondition(qInstance.getBackend(schedule.getVariantBackend()) != null, "A variant backend was not found for " + schedule.getVariantBackend());
assertCondition(schedule.getVariantRunStrategy() != null, "A variant run strategy was not set for " + schedule.getVariantBackend() + " on schedule in process " + processName);
}
if(process.getVariantBackend() != null)
{
assertCondition(qInstance.getBackend(process.getVariantBackend()) != null, "Process " + processName + ", a variant backend was not found named " + process.getVariantBackend());
assertCondition(process.getVariantRunStrategy() != null, "A variant run strategy was not set for process " + processName + " (which does specify a variant backend)");
}
else
{
assertCondition(process.getVariantRunStrategy() == null, "A variant run strategy was set for process " + processName + " (which isn't allowed, since it does not specify a variant backend)");
}
for(QSupplementalProcessMetaData supplementalProcessMetaData : CollectionUtils.nonNullMap(process.getSupplementalMetaData()).values())
@ -1327,12 +1431,57 @@ public class QInstanceValidator
supplementalProcessMetaData.validate(qInstance, process, this);
}
runPlugins(QProcessMetaData.class, process, qInstance);
});
}
}
/*******************************************************************************
**
*******************************************************************************/
private void validateScheduleMetaData(QScheduleMetaData schedule, QInstance qInstance, String prefix)
{
boolean isRepeat = schedule.getRepeatMillis() != null || schedule.getRepeatSeconds() != null;
boolean isCron = StringUtils.hasContent(schedule.getCronExpression());
assertCondition(isRepeat || isCron, prefix + " either repeatMillis or repeatSeconds or cronExpression must be set");
assertCondition(!(isRepeat && isCron), prefix + " both a repeat time and cronExpression may not be set");
if(isCron)
{
boolean hasDelay = schedule.getInitialDelayMillis() != null || schedule.getInitialDelaySeconds() != null;
assertCondition(!hasDelay, prefix + " a cron schedule may not have an initial delay");
try
{
CronExpression.validateExpression(schedule.getCronExpression());
}
catch(ParseException pe)
{
errors.add(prefix + " invalid cron expression: " + pe.getMessage());
}
if(assertCondition(StringUtils.hasContent(schedule.getCronTimeZoneId()), prefix + " a cron schedule must specify a cronTimeZoneId"))
{
String[] availableIDs = TimeZone.getAvailableIDs();
Optional<String> first = Arrays.stream(availableIDs).filter(id -> id.equals(schedule.getCronTimeZoneId())).findFirst();
assertCondition(first.isPresent(), prefix + " unrecognized cronTimeZoneId: " + schedule.getCronTimeZoneId());
}
}
else
{
assertCondition(!StringUtils.hasContent(schedule.getCronTimeZoneId()), prefix + " a non-cron schedule must not specify a cronTimeZoneId");
}
if(assertCondition(StringUtils.hasContent(schedule.getSchedulerName()), prefix + " is missing a scheduler name"))
{
assertCondition(qInstance.getScheduler(schedule.getSchedulerName()) != null, prefix + " is referencing an unknown scheduler name: " + schedule.getSchedulerName());
}
}
/*******************************************************************************
**
*******************************************************************************/
@ -1433,6 +1582,8 @@ public class QInstanceValidator
// view.getTitleFormat(); view.getTitleFields(); // validate these match?
}
}
runPlugins(QReportMetaData.class, report, qInstance);
});
}
}
@ -1553,9 +1704,9 @@ public class QInstanceValidator
}
}
//////////////////////////////////////////
// validate field sections in the table //
//////////////////////////////////////////
////////////////////////////////////////
// validate field sections in the app //
////////////////////////////////////////
Set<String> childNamesInSections = new HashSet<>();
if(app.getSections() != null)
{
@ -1575,12 +1726,57 @@ public class QInstanceValidator
}
}
}
//////////////////////
// validate widgets //
//////////////////////
for(String widgetName : CollectionUtils.nonNullList(app.getWidgets()))
{
assertCondition(qInstance.getWidget(widgetName) != null, "App " + appName + " widget " + widgetName + " is not a recognized widget.");
}
runPlugins(QAppMetaData.class, app, qInstance);
});
}
}
/*******************************************************************************
**
*******************************************************************************/
private void validateWidgets(QInstance qInstance)
{
if(CollectionUtils.nullSafeHasContents(qInstance.getWidgets()))
{
qInstance.getWidgets().forEach((widgetName, widget) ->
{
assertCondition(Objects.equals(widgetName, widget.getName()), "Inconsistent naming for widget: " + widgetName + "/" + widget.getName() + ".");
if(assertCondition(widget.getCodeReference() != null, "Missing codeReference for widget: " + widgetName))
{
validateSimpleCodeReference("Widget " + widgetName + " code reference: ", widget.getCodeReference(), AbstractWidgetRenderer.class);
}
if(widget instanceof ParentWidgetMetaData parentWidgetMetaData)
{
if(assertCondition(CollectionUtils.nullSafeHasContents(parentWidgetMetaData.getChildWidgetNameList()), "Missing child widgets for parent widget: " + widget.getName()))
{
for(String childWidgetName : parentWidgetMetaData.getChildWidgetNameList())
{
assertCondition(qInstance.getWidget(childWidgetName) != null, "Unrecognized child widget name [" + childWidgetName + "] in parent widget: " + widget.getName());
}
}
}
runPlugins(QWidgetMetaDataInterface.class, widget, qInstance);
}
);
}
}
/*******************************************************************************
**
*******************************************************************************/
@ -1659,6 +1855,8 @@ public class QInstanceValidator
}
default -> errors.add("Unexpected possibleValueSource type: " + possibleValueSource.getType());
}
runPlugins(QPossibleValueSource.class, possibleValueSource, qInstance);
}
});
}

View File

@ -0,0 +1,87 @@
/*
* 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.validation.plugins;
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
import com.kingsrook.qqq.backend.core.instances.QInstanceValidator;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.processes.implementations.basepull.BasepullExtractStepInterface;
/*******************************************************************************
** instance validator plugin, to ensure that a process which is a basepull uses
** an extract step marked for basepulls.
*******************************************************************************/
public class BasepullExtractStepValidator implements QInstanceValidatorPluginInterface<QProcessMetaData>
{
/*******************************************************************************
**
*******************************************************************************/
@Override
public void validate(QProcessMetaData process, QInstance qInstance, QInstanceValidator qInstanceValidator)
{
///////////////////////////////////////////////////////////////////////////
// if there's no basepull config on the process, don't do any validation //
///////////////////////////////////////////////////////////////////////////
if(process.getBasepullConfiguration() == null)
{
return;
}
//////////////////////////////////////////////////////////////////////////////////////////
// try to find an input field in the process, w/ a defaultValue that's a QCodeReference //
// and is an instance of BasepullExtractStepInterface //
//////////////////////////////////////////////////////////////////////////////////////////
boolean foundBasepullExtractStep = false;
for(QFieldMetaData field : process.getInputFields())
{
if(field.getDefaultValue() != null && field.getDefaultValue() instanceof QCodeReference codeReference)
{
try
{
BasepullExtractStepInterface extractStep = QCodeLoader.getAdHoc(BasepullExtractStepInterface.class, codeReference);
if(extractStep != null)
{
foundBasepullExtractStep = true;
}
}
catch(Exception e)
{
//////////////////////////////////////////////////////
// ok, just means we haven't found our extract step //
//////////////////////////////////////////////////////
}
}
}
///////////////////////////////////////////////////////////
// validate we could find a BasepullExtractStepInterface //
///////////////////////////////////////////////////////////
qInstanceValidator.assertCondition(foundBasepullExtractStep, "Process [" + process.getName() + "] has a basepullConfiguration, but does not have a field with a default value that is a BasepullExtractStepInterface CodeReference");
}
}

View File

@ -0,0 +1,41 @@
/*
* 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.validation.plugins;
import com.kingsrook.qqq.backend.core.instances.QInstanceValidator;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
/*******************************************************************************
** Interface for additional / optional q instance validators. Some will be
** provided by QQQ - others can be defined by applications.
*******************************************************************************/
public interface QInstanceValidatorPluginInterface<T>
{
/*******************************************************************************
**
*******************************************************************************/
void validate(T object, QInstance qInstance, QInstanceValidator qInstanceValidator);
}

View File

@ -0,0 +1,164 @@
/*
* 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.logging;
import org.apache.logging.log4j.Level;
import org.json.JSONException;
import org.json.JSONObject;
/*******************************************************************************
** A log message, which can be "collected" by the QCollectingLogger.
*******************************************************************************/
public class CollectedLogMessage
{
private Level level;
private String message;
private Throwable exception;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public CollectedLogMessage()
{
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public String toString()
{
return "CollectedLogMessage{level=" + level + ", message='" + message + '\'' + ", exception=" + exception + '}';
}
/*******************************************************************************
** Getter for message
*******************************************************************************/
public String getMessage()
{
return (this.message);
}
/*******************************************************************************
** Setter for message
*******************************************************************************/
public void setMessage(String message)
{
this.message = message;
}
/*******************************************************************************
** Fluent setter for message
*******************************************************************************/
public CollectedLogMessage withMessage(String message)
{
this.message = message;
return (this);
}
/*******************************************************************************
** Getter for exception
*******************************************************************************/
public Throwable getException()
{
return (this.exception);
}
/*******************************************************************************
** Setter for exception
*******************************************************************************/
public void setException(Throwable exception)
{
this.exception = exception;
}
/*******************************************************************************
** Fluent setter for exception
*******************************************************************************/
public CollectedLogMessage withException(Throwable exception)
{
this.exception = exception;
return (this);
}
/*******************************************************************************
** Getter for level
**
*******************************************************************************/
public Level getLevel()
{
return level;
}
/*******************************************************************************
** Setter for level
**
*******************************************************************************/
public void setLevel(Level level)
{
this.level = level;
}
/*******************************************************************************
** Fluent setter for level
**
*******************************************************************************/
public CollectedLogMessage withLevel(Level level)
{
this.level = level;
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
public JSONObject getMessageAsJSONObject() throws JSONException
{
return (new JSONObject(getMessage()));
}
}

View File

@ -0,0 +1,155 @@
/*
* 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.logging;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.Marker;
import org.apache.logging.log4j.message.Message;
import org.apache.logging.log4j.message.ObjectMessage;
import org.apache.logging.log4j.message.SimpleMessageFactory;
import org.apache.logging.log4j.simple.SimpleLogger;
import org.apache.logging.log4j.util.PropertiesUtil;
/*******************************************************************************
** QQQ log4j implementation, used within a QLogger, to "collect" log messages
** in an internal list - the idea being - for tests, to assert that logs happened.
*******************************************************************************/
public class QCollectingLogger extends SimpleLogger
{
private List<CollectedLogMessage> collectedMessages = new ArrayList<>();
///////////////////////////////////////////////////////////////////////////////////////////////////////////
// just in case one of these gets activated, and left on, put a limit on how many messages we'll collect //
///////////////////////////////////////////////////////////////////////////////////////////////////////////
private int capacity = 100;
private Logger logger;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public QCollectingLogger(Logger logger)
{
super(logger.getName(), logger.getLevel(), false, false, true, false, "", new SimpleMessageFactory(), new PropertiesUtil(new Properties()), System.out);
this.logger = logger;
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void logMessage(String fqcn, Level level, Marker marker, Message message, Throwable throwable)
{
////////////////////////////////////////////
// add this log message to our collection //
////////////////////////////////////////////
collectedMessages.add(new CollectedLogMessage()
.withLevel(level)
.withMessage(message.getFormattedMessage())
.withException(throwable));
////////////////////////////////////////////////////////////////////////////////////////
// if we've gone over our capacity, remove the 1st entry until we're back at capacity //
////////////////////////////////////////////////////////////////////////////////////////
while(collectedMessages.size() > capacity)
{
collectedMessages.remove(0);
}
//////////////////////////////////////////////////////////////////////
// update the message that we log to indicate that we collected it. //
// if it looks like JSON, insert as a name:value pair; else text. //
//////////////////////////////////////////////////////////////////////
String formattedMessage = message.getFormattedMessage();
String updatedMessage;
if(formattedMessage.startsWith("{"))
{
updatedMessage = """
{"collected":true,""" + formattedMessage.substring(1);
}
else
{
updatedMessage = "[Collected] " + formattedMessage;
}
ObjectMessage myMessage = new ObjectMessage(updatedMessage);
///////////////////////////////////////////////////////////////////////////////////////
// log the message with the original log4j logger, with our slightly updated message //
///////////////////////////////////////////////////////////////////////////////////////
logger.logMessage(level, marker, fqcn, null, myMessage, throwable);
}
/*******************************************************************************
** Setter for logger
**
*******************************************************************************/
public void setLogger(Logger logger)
{
this.logger = logger;
}
/*******************************************************************************
** Getter for collectedMessages
**
*******************************************************************************/
public List<CollectedLogMessage> getCollectedMessages()
{
return collectedMessages;
}
/*******************************************************************************
**
*******************************************************************************/
public void clear()
{
this.collectedMessages.clear();
}
/*******************************************************************************
** Setter for capacity
**
*******************************************************************************/
public void setCapacity(int capacity)
{
this.capacity = capacity;
}
}

View File

@ -119,6 +119,34 @@ public class QLogger
/*******************************************************************************
**
*******************************************************************************/
public static QCollectingLogger activateCollectingLoggerForClass(Class<?> c)
{
Logger loggerFromLogManager = LogManager.getLogger(c);
QCollectingLogger collectingLogger = new QCollectingLogger(loggerFromLogManager);
QLogger qLogger = getLogger(c);
qLogger.setLogger(collectingLogger);
return collectingLogger;
}
/*******************************************************************************
**
*******************************************************************************/
public static void deactivateCollectingLoggerForClass(Class<?> c)
{
Logger loggerFromLogManager = LogManager.getLogger(c);
QLogger qLogger = getLogger(c);
qLogger.setLogger(loggerFromLogManager);
}
/*******************************************************************************
**
*******************************************************************************/
@ -518,7 +546,7 @@ public class QLogger
/*******************************************************************************
**
*******************************************************************************/
private String makeJsonString(String message, Throwable t, List<LogPair> logPairList)
protected String makeJsonString(String message, Throwable t, List<LogPair> logPairList)
{
if(logPairList == null)
{
@ -620,4 +648,15 @@ public class QLogger
exceptionList.get(0).setHasLoggedLevel(level);
return (level);
}
/*******************************************************************************
** Setter for logger
**
*******************************************************************************/
private void setLogger(Logger logger)
{
this.logger = logger;
}
}

View File

@ -0,0 +1,156 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.actions.audits;
import java.io.Serializable;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
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.data.QRecord;
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
** Object to accumulate multiple audit-details to be recorded under a single
** audit per-record, within a process step. Especially useful if/when the
** process step spreads its work out through multiple classes.
**
** Pattern of usage looks like:
**
** <pre>
** // declare as a field (or local) w/ message for the audit headers
** private AuditDetailAccumulator auditDetailAccumulator = new AuditDetailAccumulator("Audit header message");
**
** // put into thread context
** AuditDetailAccumulator.setInContext(auditDetailAccumulator);
**
** // add a detail message for a record
** auditDetailAccumulator.addAuditDetail(tableName, record, "Detail message");
**
** // in another class, get the accumulator from context and safely add a detail message
** AuditDetailAccumulator.getFromContext().ifPresent(ada -> ada.addAuditDetail(tableName, record, "More Details"));
**
** // at the end of a step run/runOnePage method, add the accumulated audit details to step output
** auditDetailAccumulator.getAccumulatedAuditSingleInputs().forEach(runBackendStepOutput::addAuditSingleInput);
** auditDetailAccumulator.clear();
** </pre>
*******************************************************************************/
public class AuditDetailAccumulator implements Serializable
{
private static final QLogger LOG = QLogger.getLogger(AuditDetailAccumulator.class);
private static final String objectKey = AuditDetailAccumulator.class.getSimpleName();
private String header;
private Map<TableNameAndPrimaryKey, AuditSingleInput> recordAuditInputMap = new HashMap<>();
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public AuditDetailAccumulator(String header)
{
this.header = header;
}
/*******************************************************************************
**
*******************************************************************************/
public void setInContext()
{
QContext.setObject(objectKey, this);
}
/*******************************************************************************
**
*******************************************************************************/
public static Optional<AuditDetailAccumulator> getFromContext()
{
return QContext.getObject(objectKey, AuditDetailAccumulator.class);
}
/*******************************************************************************
**
*******************************************************************************/
public void addAuditDetail(String tableName, QRecordEntity entity, String message)
{
if(entity != null)
{
addAuditDetail(tableName, entity.toQRecord(), message);
}
}
/*******************************************************************************
**
*******************************************************************************/
public void addAuditDetail(String tableName, QRecord record, String message)
{
QTableMetaData table = QContext.getQInstance().getTable(tableName);
Serializable primaryKey = record.getValue(table.getPrimaryKeyField());
if(primaryKey == null)
{
LOG.info("Missing primary key in input record - audit detail message will not be recorded.", logPair("message", message));
return;
}
AuditSingleInput auditSingleInput = recordAuditInputMap.computeIfAbsent(new TableNameAndPrimaryKey(tableName, primaryKey), (key) -> new AuditSingleInput(table, record, header));
auditSingleInput.addDetail(message);
}
/*******************************************************************************
**
*******************************************************************************/
public Collection<AuditSingleInput> getAccumulatedAuditSingleInputs()
{
return (recordAuditInputMap.values());
}
/*******************************************************************************
**
*******************************************************************************/
public void clear()
{
recordAuditInputMap.clear();
}
private record TableNameAndPrimaryKey(String tableName, Serializable primaryKey) {}
}

View File

@ -41,7 +41,7 @@ import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
/*******************************************************************************
** Input data to insert a single audit record (with optional child record)..
*******************************************************************************/
public class AuditSingleInput
public class AuditSingleInput implements Serializable
{
private String auditTableName;
private String auditUserName;

View File

@ -60,6 +60,11 @@ public interface QueryOrGetInputInterface
QBackendTransaction getTransaction();
/*******************************************************************************
**
*******************************************************************************/
String getTableName();
/*******************************************************************************
** Setter for transaction
*******************************************************************************/

View File

@ -24,6 +24,7 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.aggregate;
import java.io.Serializable;
import java.util.Objects;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
@ -38,6 +39,17 @@ public class GroupBy implements Serializable
/*******************************************************************************
**
*******************************************************************************/
public GroupBy(QFieldMetaData field)
{
this.type = field.getType();
this.fieldName = field.getName();
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -372,7 +372,7 @@ public class QQueryFilter implements Serializable, Cloneable
rs.append(")");
rs.append("OrderBy[");
for(QFilterOrderBy orderBy : orderBys)
for(QFilterOrderBy orderBy : CollectionUtils.nonNullList(orderBys))
{
rs.append(orderBy).append(",");
}

View File

@ -23,10 +23,12 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.query;
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.AbstractActionOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
/*******************************************************************************
@ -89,4 +91,18 @@ public class QueryOutput extends AbstractActionOutput implements Serializable
return storage.getRecords();
}
/*******************************************************************************
**
*******************************************************************************/
public <T extends QRecordEntity> List<T> getRecordEntities(Class<T> entityClass) throws QException
{
List<T> rs = new ArrayList<>();
for(QRecord record : storage.getRecords())
{
rs.add(QRecordEntity.fromQRecord(entityClass, record));
}
return (rs);
}
}

View File

@ -39,6 +39,7 @@ public class ReplaceInput extends AbstractTableActionInput
private UniqueKey key;
private List<QRecord> records;
private QQueryFilter filter;
private boolean performDeletes = true;
private boolean omitDmlAudit = false;
@ -207,4 +208,35 @@ public class ReplaceInput extends AbstractTableActionInput
return (this);
}
/*******************************************************************************
** Getter for performDeletes
*******************************************************************************/
public boolean getPerformDeletes()
{
return (this.performDeletes);
}
/*******************************************************************************
** Setter for performDeletes
*******************************************************************************/
public void setPerformDeletes(boolean performDeletes)
{
this.performDeletes = performDeletes;
}
/*******************************************************************************
** Fluent setter for performDeletes
*******************************************************************************/
public ReplaceInput withPerformDeletes(boolean performDeletes)
{
this.performDeletes = performDeletes;
return (this);
}
}

View File

@ -52,8 +52,9 @@ public class UpdateInput extends AbstractTableActionInput
private Boolean areAllValuesBeingUpdatedTheSame = null;
private boolean omitTriggeringAutomations = false;
private boolean omitDmlAudit = false;
private String auditContext = null;
private boolean omitDmlAudit = false;
private boolean omitModifyDateUpdate = false;
private String auditContext = null;
@ -353,4 +354,35 @@ public class UpdateInput extends AbstractTableActionInput
return (this);
}
/*******************************************************************************
** Getter for omitModifyDateUpdate
*******************************************************************************/
public boolean getOmitModifyDateUpdate()
{
return (this.omitModifyDateUpdate);
}
/*******************************************************************************
** Setter for omitModifyDateUpdate
*******************************************************************************/
public void setOmitModifyDateUpdate(boolean omitModifyDateUpdate)
{
this.omitModifyDateUpdate = omitModifyDateUpdate;
}
/*******************************************************************************
** Fluent setter for omitModifyDateUpdate
*******************************************************************************/
public UpdateInput withOmitModifyDateUpdate(boolean omitModifyDateUpdate)
{
this.omitModifyDateUpdate = omitModifyDateUpdate;
return (this);
}
}

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.model.common;
import java.util.ArrayList;
import java.util.List;
import java.util.TimeZone;
import java.util.function.Function;
import java.util.function.Predicate;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.PVSValueFormatAndFields;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValue;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSourceType;
/*******************************************************************************
**
*******************************************************************************/
public class TimeZonePossibleValueSourceMetaDataProvider
{
public static final String NAME = "timeZones";
/*******************************************************************************
**
*******************************************************************************/
public QPossibleValueSource produce()
{
return (produce(null, null));
}
/*******************************************************************************
**
*******************************************************************************/
public QPossibleValueSource produce(Predicate<String> filter, Function<String, String> labelMapper)
{
QPossibleValueSource possibleValueSource = new QPossibleValueSource()
.withName("timeZones")
.withType(QPossibleValueSourceType.ENUM)
.withValueFormatAndFields(PVSValueFormatAndFields.LABEL_ONLY);
List<QPossibleValue<?>> enumValues = new ArrayList<>();
for(String availableID : TimeZone.getAvailableIDs())
{
if(filter == null || filter.test(availableID))
{
String label = labelMapper == null ? availableID : labelMapper.apply(availableID);
enumValues.add(new QPossibleValue<>(availableID, label));
}
}
possibleValueSource.withEnumValues(enumValues);
return (possibleValueSource);
}
}

View File

@ -0,0 +1,220 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.dashboard.widgets;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.AbstractBlockWidgetData;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.base.BaseSlots;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.base.BaseStyles;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.base.BaseValues;
/*******************************************************************************
** Data used to render a Composite Widget - e.g., a collection of blocks
*******************************************************************************/
public class CompositeWidgetData extends AbstractBlockWidgetData<CompositeWidgetData, BaseValues, BaseSlots, BaseStyles>
{
private List<AbstractBlockWidgetData<?, ?, ?, ?>> blocks = new ArrayList<>();
private Map<String, Serializable> styleOverrides = new HashMap<>();
private Layout layout;
/*******************************************************************************
**
*******************************************************************************/
public enum Layout
{
/////////////////////////////////////////////////////////////
// note, these are used in QQQ FMD CompositeWidgetData.tsx //
/////////////////////////////////////////////////////////////
FLEX_ROW_WRAPPED,
FLEX_ROW_SPACE_BETWEEN,
TABLE_SUB_ROW_DETAILS,
BADGES_WRAPPER
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public String getBlockTypeName()
{
return "COMPOSITE";
}
/*******************************************************************************
** Getter for blocks
**
*******************************************************************************/
public List<AbstractBlockWidgetData<?, ?, ?, ?>> getBlocks()
{
return blocks;
}
/*******************************************************************************
** Setter for blocks
**
*******************************************************************************/
public void setBlocks(List<AbstractBlockWidgetData<?, ?, ?, ?>> blocks)
{
this.blocks = blocks;
}
/*******************************************************************************
** Fluent setter for blocks
**
*******************************************************************************/
public CompositeWidgetData withBlocks(List<AbstractBlockWidgetData<?, ?, ?, ?>> blocks)
{
this.blocks = blocks;
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
public CompositeWidgetData withBlock(AbstractBlockWidgetData<?, ?, ?, ?> block)
{
addBlock(block);
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
public void addBlock(AbstractBlockWidgetData<?, ?, ?, ?> block)
{
if(this.blocks == null)
{
this.blocks = new ArrayList<>();
}
this.blocks.add(block);
}
/*******************************************************************************
** Getter for styleOverrides
*******************************************************************************/
public Map<String, Serializable> getStyleOverrides()
{
return (this.styleOverrides);
}
/*******************************************************************************
** Setter for styleOverrides
*******************************************************************************/
public void setStyleOverrides(Map<String, Serializable> styleOverrides)
{
this.styleOverrides = styleOverrides;
}
/*******************************************************************************
** Fluent setter for styleOverrides
*******************************************************************************/
public CompositeWidgetData withStyleOverrides(Map<String, Serializable> styleOverrides)
{
this.styleOverrides = styleOverrides;
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
public CompositeWidgetData withStyleOverride(String key, Serializable value)
{
addStyleOverride(key, value);
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
public void addStyleOverride(String key, Serializable value)
{
if(this.styleOverrides == null)
{
this.styleOverrides = new HashMap<>();
}
this.styleOverrides.put(key, value);
}
/*******************************************************************************
** Getter for layout
*******************************************************************************/
public Layout getLayout()
{
return (this.layout);
}
/*******************************************************************************
** Setter for layout
*******************************************************************************/
public void setLayout(Layout layout)
{
this.layout = layout;
}
/*******************************************************************************
** Fluent setter for layout
*******************************************************************************/
public CompositeWidgetData withLayout(Layout layout)
{
this.layout = layout;
return (this);
}
}

View File

@ -35,6 +35,8 @@ public class ParentWidgetData extends QWidgetData
private List<String> childWidgetNameList;
private ParentWidgetMetaData.LayoutType layoutType = ParentWidgetMetaData.LayoutType.GRID;
private boolean isLabelPageTitle = false;
/*******************************************************************************
@ -121,4 +123,34 @@ public class ParentWidgetData extends QWidgetData
}
/*******************************************************************************
** Getter for isLabelPageTitle
*******************************************************************************/
public boolean getIsLabelPageTitle()
{
return (this.isLabelPageTitle);
}
/*******************************************************************************
** Setter for isLabelPageTitle
*******************************************************************************/
public void setIsLabelPageTitle(boolean isLabelPageTitle)
{
this.isLabelPageTitle = isLabelPageTitle;
}
/*******************************************************************************
** Fluent setter for isLabelPageTitle
*******************************************************************************/
public ParentWidgetData withIsLabelPageTitle(boolean isLabelPageTitle)
{
this.isLabelPageTitle = isLabelPageTitle;
return (this);
}
}

View File

@ -22,6 +22,7 @@
package com.kingsrook.qqq.backend.core.model.dashboard.widgets;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
@ -33,6 +34,7 @@ import java.util.Map;
public abstract class QWidgetData
{
private String label;
private String sublabel;
private String footerHTML;
private List<String> dropdownNameList;
private List<String> dropdownLabelList;
@ -47,6 +49,8 @@ public abstract class QWidgetData
private List<List<Map<String, String>>> dropdownDataList;
private String dropdownNeedsSelectedText;
private List<List<Serializable>> csvData;
/*******************************************************************************
@ -324,4 +328,65 @@ public abstract class QWidgetData
}
/*******************************************************************************
** Getter for csvData
*******************************************************************************/
public List<List<Serializable>> getCsvData()
{
return (this.csvData);
}
/*******************************************************************************
** Setter for csvData
*******************************************************************************/
public void setCsvData(List<List<Serializable>> csvData)
{
this.csvData = csvData;
}
/*******************************************************************************
** Fluent setter for csvData
*******************************************************************************/
public QWidgetData withCsvData(List<List<Serializable>> csvData)
{
this.csvData = csvData;
return (this);
}
/*******************************************************************************
** Getter for sublabel
*******************************************************************************/
public String getSublabel()
{
return (this.sublabel);
}
/*******************************************************************************
** Setter for sublabel
*******************************************************************************/
public void setSublabel(String sublabel)
{
this.sublabel = sublabel;
}
/*******************************************************************************
** Fluent setter for sublabel
*******************************************************************************/
public QWidgetData withSublabel(String sublabel)
{
this.sublabel = sublabel;
return (this);
}
}

View File

@ -22,19 +22,24 @@
package com.kingsrook.qqq.backend.core.model.dashboard.widgets;
import java.io.Serializable;
/*******************************************************************************
** Model containing datastructure expected by frontend statistics widget
**
*******************************************************************************/
public class StatisticsData extends QWidgetData
{
private Number count;
private String countFontSize;
private String countURL;
private Number percentageAmount;
private String percentageLabel;
private boolean isCurrency = false;
private boolean increaseIsGood = true;
private Serializable count;
private String countFontSize;
private String countURL;
private String countContext;
private Number percentageAmount;
private String percentageLabel;
private String percentageURL;
private boolean isCurrency = false;
private boolean increaseIsGood = true;
@ -50,7 +55,7 @@ public class StatisticsData extends QWidgetData
/*******************************************************************************
**
*******************************************************************************/
public StatisticsData(Number count, Number percentageAmount, String percentageLabel)
public StatisticsData(Serializable count, Number percentageAmount, String percentageLabel)
{
this.count = count;
this.percentageLabel = percentageLabel;
@ -142,7 +147,7 @@ public class StatisticsData extends QWidgetData
** Getter for count
**
*******************************************************************************/
public Number getCount()
public Serializable getCount()
{
return count;
}
@ -153,7 +158,7 @@ public class StatisticsData extends QWidgetData
** Setter for count
**
*******************************************************************************/
public void setCount(Number count)
public void setCount(Serializable count)
{
this.count = count;
}
@ -164,7 +169,7 @@ public class StatisticsData extends QWidgetData
** Fluent setter for count
**
*******************************************************************************/
public StatisticsData withCount(Number count)
public StatisticsData withCount(Serializable count)
{
this.count = count;
return (this);
@ -306,4 +311,66 @@ public class StatisticsData extends QWidgetData
return (this);
}
/*******************************************************************************
** Getter for countContext
*******************************************************************************/
public String getCountContext()
{
return (this.countContext);
}
/*******************************************************************************
** Setter for countContext
*******************************************************************************/
public void setCountContext(String countContext)
{
this.countContext = countContext;
}
/*******************************************************************************
** Fluent setter for countContext
*******************************************************************************/
public StatisticsData withCountContext(String countContext)
{
this.countContext = countContext;
return (this);
}
/*******************************************************************************
** Getter for percentageURL
*******************************************************************************/
public String getPercentageURL()
{
return (this.percentageURL);
}
/*******************************************************************************
** Setter for percentageURL
*******************************************************************************/
public void setPercentageURL(String percentageURL)
{
this.percentageURL = percentageURL;
}
/*******************************************************************************
** Fluent setter for percentageURL
*******************************************************************************/
public StatisticsData withPercentageURL(String percentageURL)
{
this.percentageURL = percentageURL;
return (this);
}
}

View File

@ -48,6 +48,7 @@ public enum WidgetType
STEPPER("stepper"),
TABLE("table"),
USA_MAP("usaMap"),
COMPOSITE("composite"),
DATA_BAG_VIEWER("dataBagViewer"),
SCRIPT_VIEWER("scriptViewer");

View File

@ -0,0 +1,419 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks;
import java.util.HashMap;
import java.util.Map;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.QWidgetData;
/*******************************************************************************
** Base class for the data returned in rendering a block of a specific type.
**
** The type parameters define the structure of the block's data, and should
** generally be defined along with a sub-class of this class, in a block-specific
** sub-package.
*******************************************************************************/
public abstract class AbstractBlockWidgetData<
T extends AbstractBlockWidgetData<T, V, S, SX>,
V extends BlockValuesInterface,
S extends BlockSlotsInterface,
SX extends BlockStylesInterface> extends QWidgetData
{
private String blockId;
private BlockTooltip tooltip;
private BlockLink link;
private Map<S, BlockTooltip> tooltipMap;
private Map<S, BlockLink> linkMap;
private V values;
private SX styles;
/*******************************************************************************
**
*******************************************************************************/
@Override
public final String getType()
{
return "block";
}
/*******************************************************************************
**
*******************************************************************************/
public abstract String getBlockTypeName();
/*******************************************************************************
**
*******************************************************************************/
public T withTooltip(S key, String value)
{
addTooltip(key, value);
return (T) (this);
}
/*******************************************************************************
**
*******************************************************************************/
public void addTooltip(S key, String value)
{
if(this.tooltipMap == null)
{
this.tooltipMap = new HashMap<>();
}
this.tooltipMap.put(key, new BlockTooltip().withTitle(value));
}
/*******************************************************************************
**
*******************************************************************************/
public T withTooltip(S key, BlockTooltip value)
{
addTooltip(key, value);
return (T) (this);
}
/*******************************************************************************
**
*******************************************************************************/
public void addTooltip(S key, BlockTooltip value)
{
if(this.tooltipMap == null)
{
this.tooltipMap = new HashMap<>();
}
this.tooltipMap.put(key, value);
}
/*******************************************************************************
** Getter for tooltipMap
*******************************************************************************/
public Map<S, BlockTooltip> getTooltipMap()
{
return (this.tooltipMap);
}
/*******************************************************************************
** Setter for tooltipMap
*******************************************************************************/
public void setTooltipMap(Map<S, BlockTooltip> tooltipMap)
{
this.tooltipMap = tooltipMap;
}
/*******************************************************************************
** Fluent setter for tooltipMap
*******************************************************************************/
public T withTooltipMap(Map<S, BlockTooltip> tooltipMap)
{
this.tooltipMap = tooltipMap;
return (T) (this);
}
/*******************************************************************************
** Getter for tooltip
**
*******************************************************************************/
public BlockTooltip getTooltip()
{
return tooltip;
}
/*******************************************************************************
** Setter for tooltip
**
*******************************************************************************/
public void setTooltip(BlockTooltip tooltip)
{
this.tooltip = tooltip;
}
/*******************************************************************************
** Fluent setter for tooltip
**
*******************************************************************************/
public T withTooltip(String tooltip)
{
this.tooltip = new BlockTooltip(tooltip);
return (T) (this);
}
/*******************************************************************************
** Fluent setter for tooltip
**
*******************************************************************************/
public T withTooltip(BlockTooltip tooltip)
{
this.tooltip = tooltip;
return (T) (this);
}
/*******************************************************************************
**
*******************************************************************************/
public T withLink(S key, String value)
{
addLink(key, value);
return (T) (this);
}
/*******************************************************************************
**
*******************************************************************************/
public void addLink(S key, String value)
{
if(this.linkMap == null)
{
this.linkMap = new HashMap<>();
}
this.linkMap.put(key, new BlockLink(value));
}
/*******************************************************************************
**
*******************************************************************************/
public T withLink(S key, BlockLink value)
{
addLink(key, value);
return (T) (this);
}
/*******************************************************************************
**
*******************************************************************************/
public void addLink(S key, BlockLink value)
{
if(this.linkMap == null)
{
this.linkMap = new HashMap<>();
}
this.linkMap.put(key, value);
}
/*******************************************************************************
** Getter for linkMap
*******************************************************************************/
public Map<S, BlockLink> getLinkMap()
{
return (this.linkMap);
}
/*******************************************************************************
** Setter for linkMap
*******************************************************************************/
public void setLinkMap(Map<S, BlockLink> linkMap)
{
this.linkMap = linkMap;
}
/*******************************************************************************
** Fluent setter for linkMap
*******************************************************************************/
public T withLinkMap(Map<S, BlockLink> linkMap)
{
this.linkMap = linkMap;
return (T) (this);
}
/*******************************************************************************
** Getter for link
**
*******************************************************************************/
public BlockLink getLink()
{
return link;
}
/*******************************************************************************
** Setter for link
**
*******************************************************************************/
public void setLink(BlockLink link)
{
this.link = link;
}
/*******************************************************************************
** Fluent setter for link
**
*******************************************************************************/
public T withLink(String link)
{
this.link = new BlockLink(link);
return (T) (this);
}
/*******************************************************************************
** Fluent setter for link
**
*******************************************************************************/
public T withLink(BlockLink link)
{
this.link = link;
return (T) this;
}
/*******************************************************************************
** Getter for values
*******************************************************************************/
public V getValues()
{
return (this.values);
}
/*******************************************************************************
** Setter for values
*******************************************************************************/
public void setValues(V values)
{
this.values = values;
}
/*******************************************************************************
** Fluent setter for values
*******************************************************************************/
public T withValues(V values)
{
this.values = values;
return (T) this;
}
/*******************************************************************************
** Getter for styles
*******************************************************************************/
public SX getStyles()
{
return (this.styles);
}
/*******************************************************************************
** Setter for styles
*******************************************************************************/
public void setStyles(SX styles)
{
this.styles = styles;
}
/*******************************************************************************
** Fluent setter for styles
*******************************************************************************/
public T withStyles(SX styles)
{
this.styles = styles;
return (T) this;
}
/*******************************************************************************
** Getter for blockId
*******************************************************************************/
public String getBlockId()
{
return (this.blockId);
}
/*******************************************************************************
** Setter for blockId
*******************************************************************************/
public void setBlockId(String blockId)
{
this.blockId = blockId;
}
/*******************************************************************************
** Fluent setter for blockId
*******************************************************************************/
public T withBlockId(String blockId)
{
this.blockId = blockId;
return (T) this;
}
}

View File

@ -0,0 +1,86 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks;
/*******************************************************************************
** A link used within a (widget) block.
**
** Right now, just a href - but target is an obvious next-thing to add here.
*******************************************************************************/
public class BlockLink
{
private String href;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public BlockLink()
{
}
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public BlockLink(String href)
{
this.href = href;
}
/*******************************************************************************
** Getter for href
*******************************************************************************/
public String getHref()
{
return (this.href);
}
/*******************************************************************************
** Setter for href
*******************************************************************************/
public void setHref(String href)
{
this.href = href;
}
/*******************************************************************************
** Fluent setter for href
*******************************************************************************/
public BlockLink withHref(String href)
{
this.href = href;
return (this);
}
}

View File

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

View File

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

View File

@ -0,0 +1,122 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks;
/*******************************************************************************
** A tooltip used within a (widget) block.
**
*******************************************************************************/
public class BlockTooltip
{
private String title;
private Placement placement = Placement.BOTTOM;
public enum Placement
{BOTTOM, LEFT, RIGHT, TOP}
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public BlockTooltip()
{
}
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public BlockTooltip(String title)
{
this.title = title;
}
/*******************************************************************************
** Getter for title
*******************************************************************************/
public String getTitle()
{
return (this.title);
}
/*******************************************************************************
** Setter for title
*******************************************************************************/
public void setTitle(String title)
{
this.title = title;
}
/*******************************************************************************
** Fluent setter for title
*******************************************************************************/
public BlockTooltip withTitle(String title)
{
this.title = title;
return (this);
}
/*******************************************************************************
** Getter for placement
*******************************************************************************/
public Placement getPlacement()
{
return (this.placement);
}
/*******************************************************************************
** Setter for placement
*******************************************************************************/
public void setPlacement(Placement placement)
{
this.placement = placement;
}
/*******************************************************************************
** Fluent setter for placement
*******************************************************************************/
public BlockTooltip withPlacement(Placement placement)
{
this.placement = placement;
return (this);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,131 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.bignumberblock;
import java.io.Serializable;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.BlockValuesInterface;
/*******************************************************************************
**
*******************************************************************************/
public class BigNumberValues implements BlockValuesInterface
{
private String heading;
private Serializable number;
private String context;
/*******************************************************************************
** Getter for heading
*******************************************************************************/
public String getHeading()
{
return (this.heading);
}
/*******************************************************************************
** Setter for heading
*******************************************************************************/
public void setHeading(String heading)
{
this.heading = heading;
}
/*******************************************************************************
** Fluent setter for heading
*******************************************************************************/
public BigNumberValues withHeading(String heading)
{
this.heading = heading;
return (this);
}
/*******************************************************************************
** Getter for number
*******************************************************************************/
public Serializable getNumber()
{
return (this.number);
}
/*******************************************************************************
** Setter for number
*******************************************************************************/
public void setNumber(Serializable number)
{
this.number = number;
}
/*******************************************************************************
** Fluent setter for number
*******************************************************************************/
public BigNumberValues withNumber(Serializable number)
{
this.number = number;
return (this);
}
/*******************************************************************************
** Getter for context
*******************************************************************************/
public String getContext()
{
return (this.context);
}
/*******************************************************************************
** Setter for context
*******************************************************************************/
public void setContext(String context)
{
this.context = context;
}
/*******************************************************************************
** Fluent setter for context
*******************************************************************************/
public BigNumberValues withContext(String context)
{
this.context = context;
return (this);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,131 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.progressbar;
import java.io.Serializable;
import java.math.BigDecimal;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.BlockValuesInterface;
/*******************************************************************************
**
*******************************************************************************/
public class ProgressBarValues implements BlockValuesInterface
{
private String heading;
private BigDecimal percent;
private Serializable value;
/*******************************************************************************
** Getter for heading
*******************************************************************************/
public String getHeading()
{
return (this.heading);
}
/*******************************************************************************
** Setter for heading
*******************************************************************************/
public void setHeading(String heading)
{
this.heading = heading;
}
/*******************************************************************************
** Fluent setter for heading
*******************************************************************************/
public ProgressBarValues withHeading(String heading)
{
this.heading = heading;
return (this);
}
/*******************************************************************************
** Getter for percent
*******************************************************************************/
public BigDecimal getPercent()
{
return (this.percent);
}
/*******************************************************************************
** Setter for percent
*******************************************************************************/
public void setPercent(BigDecimal percent)
{
this.percent = percent;
}
/*******************************************************************************
** Fluent setter for percent
*******************************************************************************/
public ProgressBarValues withPercent(BigDecimal percent)
{
this.percent = percent;
return (this);
}
/*******************************************************************************
** Getter for value
*******************************************************************************/
public Serializable getValue()
{
return (this.value);
}
/*******************************************************************************
** Setter for value
*******************************************************************************/
public void setValue(Serializable value)
{
this.value = value;
}
/*******************************************************************************
** Fluent setter for value
*******************************************************************************/
public ProgressBarValues withValue(Serializable value)
{
this.value = value;
return (this);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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