Compare commits

..

98 Commits

Author SHA1 Message Date
dc9a2f8698 CE-1107: added ability to use replaceAction specifying that null key values be treated as equal 2024-04-17 22:26:30 -05:00
0641ac41d6 CE-1107: added alert widget type and datepicker dropdown type 2024-04-12 15:28:33 -05:00
cf0c905dc6 Move publish_asciidoc call into deploy workflow (i think) 2024-04-09 19:01:01 -05:00
3440d45060 Remove usages of log4j's placeholders-based log formatting calls (some of which were accidental and wrong anyway) 2024-04-09 18:52:43 -05:00
c32d9110ce Remove usages of log4j's placeholders-based log formatting calls (some of which were accidental and wrong anyway) 2024-04-09 18:51:43 -05:00
b55631a767 Add liquibase as level INFO 2024-04-08 20:20:50 -05:00
9ab5a8b305 Wrap DateTimeDisplayValueBehavior's better 2024-04-05 15:39:00 -05:00
ff9add437d Update publish-asciidoc to only run on dev 2024-04-05 15:35:42 -05:00
a6bf474448 Add publishing of docs 2024-04-05 15:27:23 -05:00
f409d0bd19 Add field behaviors 2024-04-05 15:21:50 -05:00
b608bda83a CE-1072 feedback from code review & testing 2024-04-05 15:21:32 -05:00
8f503945cd CE-1072 Add fallbackZoneId in case zoneIdFromFieldName isn't set or valid; More tests & validation 2024-04-04 20:39:38 -05:00
512d73af34 checkstyle 2024-04-04 20:07:28 -05:00
e8b4368acc CE-1072 Add DateTimeDisplayValueBehavior as first version of FieldDisplayBehavior, sub-interface of FieldBehavior, called through ValueBehaviorApplier by QValueFormatter; 2024-04-04 20:02:42 -05:00
1e7e7cdd64 Merge pull request #81 from Kingsrook/feature/CE-978-crashing-nodes
Feature/ce 978 crashing nodes
2024-04-04 13:51:45 -05:00
a784e59c50 Change to run once an hour, after 6 hours, and to clean 6-hour jobs 2024-04-04 13:50:20 -05:00
4e6460c469 Merge pull request #80 from Kingsrook/hotfix/large-result-query-hint-exports
Hotfix/large result query hint exports
2024-04-04 10:53:00 -05:00
98fc34fb27 Turn on QueryHint.POTENTIALLY_LARGE_NUMBER_OF_RESULTS 2024-04-03 14:53:52 -05:00
df6bed2453 CE-881 - Add QueryHints enum & set to QueryInput; do mysql result set streaming based on the POTENTIALLY_LARGE_NUMBER_OF_RESULTS hint being present 2024-04-03 14:51:28 -05:00
0759085431 CE-978 - Initial commit of a clean thread/method to our InMemoryStateProvider to reduce memory leak 2024-04-02 16:52:22 -05:00
9257519462 Updated to try to fix sample material dashboard inclusion 2024-03-21 13:02:05 -05:00
e0134450f8 Updated to try to fix sample material dashboard inclusion 2024-03-21 12:51:29 -05:00
fc915a6d65 Merge pull request #77 from Kingsrook/feature/quartz-scheduler
Feature/quartz scheduler
2024-03-21 10:04:00 -05:00
d7dec3e560 CE-936 Add @DisallowConcurrentExecution 2024-03-20 17:42:05 -05:00
40e8a85977 Add ScheduledJobs doc 2024-03-20 16:13:39 -05:00
30ee8ce3bf Update quartz job runner to log when finished (and to include same pairs in exception) 2024-03-20 16:13:27 -05:00
0d6538593b CE-936 Add option to log the running of quartz jobs 2024-03-20 13:49:33 -05:00
7e6a3c528f CE-936 Update tests 2024-03-19 20:12:36 -05:00
02d068dad7 CE-936 Quartz test updates 2024-03-19 20:05:00 -05:00
c9f921c148 CE-936 add post-actions on scheduledJobParams table, to reschedule jobs 2024-03-19 20:04:43 -05:00
2545d03f20 CE-936 Switch scheduledJobTypes PVS to be based on schedulable types in the instance, not just the enum (e.g., in support of app-defined types) 2024-03-19 20:04:05 -05:00
92b8211f20 CE-936 Add mayUseInScheduledJobsTable to scheduler meta data; check that before including in Schedulers PVS 2024-03-19 20:02:13 -05:00
8afbbfb4da Avoid NPE on null value for custom-type PVS 2024-03-19 20:01:08 -05:00
24b1daa110 CE-936 - Move adding default schedulableTypes in instance to Enricher 2024-03-19 20:00:49 -05:00
17899c3fdc CE-936 - Move per-record pause & resume to run on quartzJob tables, not trigger; update previewMessage 2024-03-19 20:00:24 -05:00
605578d661 add overload of recordsToMap that takes key type 2024-03-19 19:59:34 -05:00
5963a706b0 CE-936 - Add error if cron or repeat-seconds isn't given; add warnings from scheduling. 2024-03-19 11:33:12 -05:00
c8c7051628 CE-936 - Update to send warnings from insert & update back not as an exception, but as a success, with warnings in the record. 2024-03-19 11:32:41 -05:00
7015322bf3 CE-936 - format epoc time values w/ commas and an instant in system time zone 2024-03-19 10:09:28 -05:00
d6edbfa06b CE-936 - Add exposed joins between these tables 2024-03-19 10:08:25 -05:00
a10992226a CE-936 - Cleanup from code review 2024-03-19 10:08:11 -05:00
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
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
5f0fe9ce27 Merged dev into feature/quartz-scheduler 2024-03-06 19:12:39 -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
175 changed files with 11852 additions and 1030 deletions

View File

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

View File

@ -26,6 +26,16 @@ include::metaData/Reports.adoc[leveloffset=+1]
include::metaData/Icons.adoc[leveloffset=+1]
include::metaData/PermissionRules.adoc[leveloffset=+1]
== Services
include::misc/ScheduledJobs.adoc[leveloffset=+1]
=== Web server (Javalin)
#todo#
=== API server (OpenAPI)
#todo#
== Custom Application Code
include::misc/QContext.adoc[leveloffset=+1]
include::misc/QRecords.adoc[leveloffset=+1]
@ -63,4 +73,4 @@ include::implementations/TableSync.adoc[leveloffset=+1]
// later... include::actions/RenderTemplateAction.adoc[leveloffset=+1]
== QQQ Utility Classes
include::utilities/RecordLookupHelper.adoc[leveloffset=+1]
include::utilities/RecordLookupHelper.adoc[leveloffset=+1]

View File

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

View File

@ -0,0 +1,141 @@
== Schedulers and Scheduled Jobs
include::../variables.adoc[]
QQQ has the ability to automatically run various types of jobs on schedules,
either defined in your instance's meta-data,
or optionally via data in your application, in a `scheduledJob` table.
=== Schedulers and QSchedulerMetaData
2 types of schedulers are included in QQQ by default (though an application can define its own schedulers):
* `SimpleScheduler` - is (as its name suggests) a simple class which uses java's `ScheduledExecutorService`
to run jobs on repeating intervals.
** Cannot run cron schedules - only repeating intervals.
** If multiple servers are running, each will potentially run the same job concurrently
** Has no configurations, e.g., to limit the number of threads.
* `QuartzScheduler` - uses the 3rd party https://www.quartz-scheduler.org/[Quartz Scheduler] library to provide
a much more capable, though admittedly more complex, scheduling solution.
** Can run both cron schedules and repeating intervals.
** By default, will not allow concurrent executions of the same job.
** Supports multiple configurations, e.g., to limit the number of threads.
An application can define its own scheduler by providing a class which implements the `QSchedulerInterface`.
A `QInstance` can work with 0 or more schedulers, as defined by adding `QSchedulerMetaData` objects
to the instance.
This meta-data class is `abstract`, and is extended by the 2 built-in schedulers
(e.g., `SimpleSchedulerMetaData` and `QuartzSchedulerMetaData`). As such,
these concrete subclasses are what you need to instantiate and add to your instance.
To configure a QuartzScheduler, you can add a `Properties` object to the `QuartzSchedulerMetaData` object.
See https://www.quartz-scheduler.org/documentation/[Quartz's documentation] for available configuration properties.
[source,java]
.Defining SchedulerMetaData
----
qInstance.addScheduler(new SimpleSchedulerMetaData().withName("mySimpleScheduler"));
qInstance.addScheduler(new QuartzSchedulerMetaData()
.withName("myQuartzScheduler")
.withProperties(myQuartzProperties);
----
=== SchedulableTypes
The types of jobs which can be scheduled in a QQQ application are defined in the `QInstance` by
instances of the `SchedulableType` meta-data class.
These objects contain a name, along with a `QCodeReference` to the `runner`,
which must be a class that implements the `SchedulableRunner` interface.
By default, (in the `QInstanceEnricher`), QQQ will make 3 `SchedulableType` options available:
* `PROCESS` - Any Process defined in the `QInstance` can be scheduled.
* `QUEUE_PROCESSOR` - A Queue defined in the `QInstance`, which requires polling (e.g., SQS), can be scheduled.
* `TABLE_AUTOMATIONS` - A Table in the `QInstance`, with `AutomationDetails` referring to an
AutomationProvider which requires polling, can be scheduled.
If an application only wants to use a subset of these `SchedulableType` options,
or to add custom `SchedulableType` options,
the `QInstance` will need to have 1 or more `SchedulableType` objects in it before the `QInstanceEnricher` runs.
=== User-defined Scheduled Jobs
To allow users to schedule jobs (rather than using programmer-defined schedules (in meta-data)),
you can add a set of tables to your `QInstance`, using the `ScheduledJobsMetaDataProvider` class:
[source,java]
.Adding the ScheduledJob tables and related meta-data to a QInstance
----
new ScheduledJobsMetaDataProvider().defineAll(
qInstance, backendName, table -> tableEnricher(table));
----
This meta-data provider adds a "scheduledJob" and "scheduledJobParameter" table, along with
some PossibleValueSources.
These tables include post-action customizers, which manage (re-, un-) scheduling jobs based on
changes made to records in this these tables.
Also, when `QScheduleManager` is started, it will query these tables,and will schedule jobs as defined therein.
_You can use a mix of user-defined and meta-data defined scheduled jobs in your instance.
However, if a ScheduledJob record references a process, queue, or table automation with a
meta-data defined schedule, the ScheduledJob will NOT be started by ScheduleManager --
rather, the meta-data definition will "win"._
[source,sql]
.Example of inserting scheduled jobs records directly into an SQL backend
----
-- A process:
INSERT INTO scheduled_job (label, scheduler_name, cron_expression, cron_time_zone_id, repeat_seconds, type, is_active) VALUES
('myProcess', 'QuartzScheduler', null, null, 300, 'PROCESS', 1);
INSERT INTO scheduled_job_parameter (scheduled_job_id, `key`, value) VALUES
((SELECT id FROM scheduled_job WHERE label = 'myProcess'), 'processName', 'myProcess');
-- A table's insert & update automations:
INSERT INTO scheduled_job (label, scheduler_name, cron_expression, cron_time_zone_id, repeat_seconds, type, is_active) VALUES
('myTable.PENDING_INSERT_AUTOMATIONS', 'QuartzScheduler', null, null, 15, 'TABLE_AUTOMATIONS', 1),
('myTable.PENDING_UPDATE_AUTOMATIONS', 'QuartzScheduler', null, null, 15, 'TABLE_AUTOMATIONS', 1);
INSERT INTO scheduled_job_parameter (scheduled_job_id, `key`, value) VALUES
((SELECT id FROM scheduled_job WHERE label = 'myTable.PENDING_INSERT_AUTOMATIONS'), 'tableName', 'myTable'),
((SELECT id FROM scheduled_job WHERE label = 'myTable.PENDING_INSERT_AUTOMATIONS'), 'automationStatus', 'PENDING_INSERT_AUTOMATIONS'),
((SELECT id FROM scheduled_job WHERE label = 'myTable.PENDING_UPDATE_AUTOMATIONS'), 'tableName', 'myTable'),
((SELECT id FROM scheduled_job WHERE label = 'myTable.PENDING_UPDATE_AUTOMATIONS'), 'automationStatus', 'PENDING_UPDATE_AUTOMATIONS');
-- A queue processor:
INSERT INTO scheduled_job (label, scheduler_name, cron_expression, cron_time_zone_id, repeat_seconds, type, is_active) VALUES
('mySqsQueue', 'QuartzScheduler', null, null, 60, 'QUEUE_PROCESSOR', 1);
INSERT INTO scheduled_job_parameter (scheduled_job_id, `key`, value) VALUES
((SELECT id FROM scheduled_job WHERE label = 'mySqsQueue'), 'queueName', 'mySqsQueue');
----
=== Running Scheduled Jobs
In a server running QQQ, if you wish to start running scheduled jobs, you need to initialize
the `QScheduleManger` singleton class, then call its `start()` method.
Note that internally, this class will check for a system property of `qqq.scheduleManager.enabled`
or an environment variable of `QQQ_SCHEDULE_MANAGER_ENABLED`, and if the first value found is
`"false"`, then the scheduler will not actually run its jobs (but, in the case of the `QuartzSchdeuler`,
it will be available for managing scheduled jobs).
The method `QScheduleManager.initInstance` requires 2 parameters: Your `QInstance`, and a
`Supplier<QSession>` lambda, which returns the session that will be used for scheduled jobs when they
are executed.
[source,java]
.Starting the Schedule Manager service
----
QScheduleManager.initInstance(qInstance, () -> systemUserSession).start();
----
=== Examples
[source,java]
.Attach a schedule in meta-data to a Process
----
QProcessMetaData myProcess = new QProcessMetaData()
// ...
.withSchedule(new QScheduleMetaData()
.withSchedulerName("myScheduler")
.withDescription("Run myProcess every five minutes")
.withRepeatSeconds(300))
----

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,7 @@ 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);
try
{
LOG.debug("Starting job " + uuidAndTypeStateKey.getUuid());

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

View File

@ -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());
}
@ -102,7 +96,7 @@ public class QCodeLoader
}
catch(Exception e)
{
LOG.error("Error initializing customizer", logPair("codeReference", codeReference), e);
LOG.error("Error initializing customizer", e, logPair("codeReference", codeReference));
//////////////////////////////////////////////////////////////////////////////////////////////////////////
// return null here - under the assumption that during normal run-time operations, we'll never hit here //
@ -141,7 +135,7 @@ public class QCodeLoader
}
catch(Exception e)
{
LOG.error("Error initializing customizer", logPair("codeReference", codeReference), e);
LOG.error("Error initializing customizer", e, logPair("codeReference", codeReference));
//////////////////////////////////////////////////////////////////////////////////////////////////////////
// return null here - under the assumption that during normal run-time operations, we'll never hit here //
@ -175,12 +169,25 @@ 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)
{
LOG.error("Error initializing customizer", logPair("codeReference", codeReference), e);
LOG.error("Error initializing customizer", e, logPair("codeReference", codeReference));
//////////////////////////////////////////////////////////////////////////////////////////////////////////
// return null here - under the assumption that during normal run-time operations, we'll never hit here //

View File

@ -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

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

View File

@ -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

@ -232,6 +232,7 @@ public class ExportAction
}
queryInput.getFilter().setLimit(exportInput.getLimit());
queryInput.setShouldTranslatePossibleValues(true);
queryInput.withQueryHint(QueryInput.QueryHint.POTENTIALLY_LARGE_NUMBER_OF_RESULTS);
/////////////////////////////////////////////////////////////////
// tell this query that it needs to put its output into a pipe //

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;
/*******************************************************************************
@ -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

@ -138,7 +138,7 @@ public class RecordPipe
{
if(now - sleepLoopStartTime > MAX_SLEEP_LOOP_MILLIS)
{
LOG.warn("Giving up adding record to pipe, due to pipe being full for more than {} millis", MAX_SLEEP_LOOP_MILLIS);
LOG.warn("Giving up adding record to pipe, due to pipe being full for more than " + MAX_SLEEP_LOOP_MILLIS + " millis");
throw (new IllegalStateException("Giving up adding record to pipe, due to pipe staying full too long."));
}
LOG.trace("Record pipe.add failed (due to full pipe). Blocking.");

View File

@ -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(), 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())));
}
}
}

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

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

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;
@ -192,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)
{
@ -273,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

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

View File

@ -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)
{
@ -366,6 +370,14 @@ public class QPossibleValueTranslator
*******************************************************************************/
private String translatePossibleValueCustom(Serializable value, QPossibleValueSource possibleValueSource)
{
/////////////////////////////////
// null input gets null output //
/////////////////////////////////
if(value == null)
{
return (null);
}
try
{
QCustomPossibleValueProvider customPossibleValueProvider = QCodeLoader.getCustomPossibleValueProvider(possibleValueSource);

View File

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

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

View File

@ -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

@ -34,6 +34,7 @@ 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;
@ -54,6 +55,7 @@ public class QContext
private static ThreadLocal<Map<String, Serializable>> objectsThreadLocal = new ThreadLocal<>();
/*******************************************************************************
** private constructor - class is not meant to be instantiated.
*******************************************************************************/
@ -105,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.
@ -267,6 +288,7 @@ public class QContext
}
/*******************************************************************************
** get one named object from the Context for the current thread. may return null.
*******************************************************************************/
@ -280,6 +302,7 @@ public class QContext
}
/*******************************************************************************
** 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.

View File

@ -77,6 +77,7 @@ import com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert.Bulk
import com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert.BulkInsertTransformStep;
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.ExtractViaQueryStep;
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.StreamedETLWithFrontendProcess;
import com.kingsrook.qqq.backend.core.scheduler.QScheduleManager;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.ListingHash;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
@ -159,6 +160,18 @@ public class QInstanceEnricher
}
enrichJoins();
//////////////////////////////////////////////////////////////////////////////
// if the instance DOES have 1 or more scheduler, but no schedulable types, //
// then go ahead and add the default set that qqq knows about //
//////////////////////////////////////////////////////////////////////////////
if(CollectionUtils.nullSafeHasContents(qInstance.getSchedulers()))
{
if(CollectionUtils.nullSafeIsEmpty(qInstance.getSchedulableTypes()))
{
QScheduleManager.defineDefaultSchedulableTypesInInstance(qInstance);
}
}
}
@ -1030,6 +1043,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

@ -26,6 +26,7 @@ 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;
@ -35,6 +36,7 @@ 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;
@ -62,6 +64,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.dashboard.ParentWidgetMetaD
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaDataInterface;
import com.kingsrook.qqq.backend.core.model.metadata.fields.AdornmentType;
import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldAdornment;
import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldBehavior;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior;
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn;
@ -104,6 +107,7 @@ 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;
/*******************************************************************************
@ -432,6 +436,11 @@ 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);
});
}
@ -802,7 +811,7 @@ public class QInstanceValidator
/*******************************************************************************
**
*******************************************************************************/
private void validateTableField(QInstance qInstance, String tableName, String fieldName, QTableMetaData table, QFieldMetaData field)
private <T extends FieldBehavior<T>> void validateTableField(QInstance qInstance, String tableName, String fieldName, QTableMetaData table, QFieldMetaData field)
{
assertCondition(Objects.equals(fieldName, field.getName()),
"Inconsistent naming in table " + tableName + " for field " + fieldName + "/" + field.getName() + ".");
@ -815,12 +824,32 @@ public class QInstanceValidator
String prefix = "Field " + fieldName + " in table " + tableName + " ";
///////////////////////////////////////////////////
// validate things we know about field behaviors //
///////////////////////////////////////////////////
ValueTooLongBehavior behavior = field.getBehaviorOrDefault(qInstance, ValueTooLongBehavior.class);
if(behavior != null && !behavior.equals(ValueTooLongBehavior.PASS_THROUGH))
{
assertCondition(field.getMaxLength() != null, prefix + "specifies a ValueTooLongBehavior, but not a maxLength.");
}
Set<Class<FieldBehavior<T>>> usedFieldBehaviorTypes = new HashSet<>();
if(field.getBehaviors() != null)
{
for(FieldBehavior<?> fieldBehavior : field.getBehaviors())
{
Class<FieldBehavior<T>> behaviorClass = (Class<FieldBehavior<T>>) fieldBehavior.getClass();
errors.addAll(fieldBehavior.validateBehaviorConfiguration(table, field));
if(!fieldBehavior.allowMultipleBehaviorsOfThisType())
{
assertCondition(!usedFieldBehaviorTypes.contains(behaviorClass), prefix + "has more than 1 fieldBehavior of type " + behaviorClass.getSimpleName() + ", which is not allowed for this type");
}
usedFieldBehaviorTypes.add(behaviorClass);
}
}
if(field.getMaxLength() != null)
{
assertCondition(field.getMaxLength() > 0, prefix + "has an invalid maxLength (" + field.getMaxLength() + ") - must be greater than 0.");
@ -1013,6 +1042,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 //
//////////////////////////////////
@ -1400,13 +1434,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())
@ -1421,6 +1459,50 @@ public class QInstanceValidator
/*******************************************************************************
**
*******************************************************************************/
private void validateScheduleMetaData(QScheduleMetaData schedule, QInstance qInstance, String prefix)
{
boolean isRepeat = schedule.getRepeatMillis() != null || schedule.getRepeatSeconds() != null;
boolean isCron = StringUtils.hasContent(schedule.getCronExpression());
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());
}
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -48,6 +48,17 @@ public class CollectedLogMessage
/*******************************************************************************
**
*******************************************************************************/
@Override
public String toString()
{
return "CollectedLogMessage{level=" + level + ", message='" + message + '\'' + ", exception=" + exception + '}';
}
/*******************************************************************************
** Getter for message
*******************************************************************************/

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

@ -24,6 +24,7 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.query;
import java.util.ArrayList;
import java.util.Collection;
import java.util.EnumSet;
import java.util.List;
import java.util.Set;
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
@ -68,6 +69,24 @@ public class QueryInput extends AbstractTableActionInput implements QueryOrGetIn
private boolean includeAssociations = false;
private Collection<String> associationNamesToInclude = null;
private EnumSet<QueryHint> queryHints = EnumSet.noneOf(QueryHint.class);
/*******************************************************************************
** Information about the query that an application (or qqq service) may know and
** want to tell the backend, that can help influence how the backend processes
** query.
**
** For example, a query with potentially a large result set, for MySQL backend,
** we may want to configure the result set to stream results rather than do its
** default in-memory thing. See RDBMSQueryAction for usage.
*******************************************************************************/
public enum QueryHint
{
POTENTIALLY_LARGE_NUMBER_OF_RESULTS
}
/*******************************************************************************
@ -569,4 +588,64 @@ public class QueryInput extends AbstractTableActionInput implements QueryOrGetIn
return (this);
}
/*******************************************************************************
** Getter for queryHints
*******************************************************************************/
public EnumSet<QueryHint> getQueryHints()
{
return (this.queryHints);
}
/*******************************************************************************
** Setter for queryHints
*******************************************************************************/
public void setQueryHints(EnumSet<QueryHint> queryHints)
{
this.queryHints = queryHints;
}
/*******************************************************************************
** Fluent setter for queryHints
*******************************************************************************/
public QueryInput withQueryHints(EnumSet<QueryHint> queryHints)
{
this.queryHints = queryHints;
return (this);
}
/*******************************************************************************
** Fluent setter for queryHints
*******************************************************************************/
public QueryInput withQueryHint(QueryHint queryHint)
{
if(this.queryHints == null)
{
this.queryHints = EnumSet.noneOf(QueryHint.class);
}
this.queryHints.add(queryHint);
return (this);
}
/*******************************************************************************
** Fluent setter for queryHints
*******************************************************************************/
public QueryInput withoutQueryHint(QueryHint queryHint)
{
if(this.queryHints != null)
{
this.queryHints.remove(queryHint);
}
return (this);
}
}

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,7 +39,8 @@ public class ReplaceInput extends AbstractTableActionInput
private UniqueKey key;
private List<QRecord> records;
private QQueryFilter filter;
private boolean performDeletes = true;
private boolean performDeletes = true;
private boolean allowNullKeyValuesToEqual = false;
private boolean omitDmlAudit = false;
@ -239,4 +240,35 @@ public class ReplaceInput extends AbstractTableActionInput
return (this);
}
/*******************************************************************************
** Getter for allowNullKeyValuesToEqual
*******************************************************************************/
public boolean getAllowNullKeyValuesToEqual()
{
return (this.allowNullKeyValuesToEqual);
}
/*******************************************************************************
** Setter for allowNullKeyValuesToEqual
*******************************************************************************/
public void setAllowNullKeyValuesToEqual(boolean allowNullKeyValuesToEqual)
{
this.allowNullKeyValuesToEqual = allowNullKeyValuesToEqual;
}
/*******************************************************************************
** Fluent setter for allowNullKeyValuesToEqual
*******************************************************************************/
public ReplaceInput withAllowNullKeyValuesToEqual(boolean allowNullKeyValuesToEqual)
{
this.allowNullKeyValuesToEqual = allowNullKeyValuesToEqual;
return (this);
}
}

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,139 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.dashboard.widgets;
/*******************************************************************************
** Model containing datastructure expected by frontend alert widget
**
*******************************************************************************/
public class AlertData extends QWidgetData
{
public enum AlertType
{
ERROR,
SUCCESS,
WARNING
}
private String html;
private AlertType alertType;
/*******************************************************************************
**
*******************************************************************************/
public AlertData()
{
}
/*******************************************************************************
**
*******************************************************************************/
public AlertData(AlertType alertType, String html)
{
setHtml(html);
setAlertType(alertType);
}
/*******************************************************************************
** Getter for type
**
*******************************************************************************/
public String getType()
{
return WidgetType.ALERT.getType();
}
/*******************************************************************************
** Getter for html
**
*******************************************************************************/
public String getHtml()
{
return html;
}
/*******************************************************************************
** Setter for html
**
*******************************************************************************/
public void setHtml(String html)
{
this.html = html;
}
/*******************************************************************************
** Fluent setter for html
**
*******************************************************************************/
public AlertData withHtml(String html)
{
this.html = html;
return (this);
}
/*******************************************************************************
** Getter for alertType
*******************************************************************************/
public AlertType getAlertType()
{
return (this.alertType);
}
/*******************************************************************************
** Setter for alertType
*******************************************************************************/
public void setAlertType(AlertType alertType)
{
this.alertType = alertType;
}
/*******************************************************************************
** Fluent setter for alertType
*******************************************************************************/
public AlertData withAlertType(AlertType alertType)
{
this.alertType = alertType;
return (this);
}
}

View File

@ -27,6 +27,7 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter;
import com.kingsrook.qqq.backend.core.instances.QInstanceEnricher;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
@ -147,7 +148,7 @@ public class FieldValueListData extends QWidgetData
}
}
QValueFormatter.setDisplayValuesInRecord(fields, record);
QValueFormatter.setDisplayValuesInRecord(null, fields.stream().collect(Collectors.toMap(f -> f.getName(), f -> f)), record);
}

View File

@ -27,6 +27,7 @@ package com.kingsrook.qqq.backend.core.model.dashboard.widgets;
*******************************************************************************/
public enum WidgetType
{
ALERT("alert"),
BAR_CHART("barChart"),
CHART("chart"),
CHILD_RECORD_LIST("childRecordList"),

View File

@ -462,6 +462,16 @@ public class QRecord implements Serializable
}
/*******************************************************************************
** Getter for a single field's value
**
*******************************************************************************/
public Long getValueLong(String fieldName)
{
return (ValueUtils.getValueAsLong(values.get(fieldName)));
}
/*******************************************************************************
**

View File

@ -150,7 +150,7 @@ public class MetaDataProducerHelper
}
catch(Exception e)
{
LOG.warn("error executing metaDataProducer", logPair("producer", producer.getClass().getSimpleName()), e);
LOG.warn("error executing metaDataProducer", e, logPair("producer", producer.getClass().getSimpleName()));
}
}
else

View File

@ -53,8 +53,10 @@ import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData;
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.reporting.QReportMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.scheduleing.QSchedulerMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.security.QSecurityKeyType;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.scheduler.schedulable.SchedulableType;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import io.github.cdimascio.dotenv.Dotenv;
@ -91,6 +93,9 @@ public class QInstance
private Map<String, QQueueProviderMetaData> queueProviders = new LinkedHashMap<>();
private Map<String, QQueueMetaData> queues = new LinkedHashMap<>();
private Map<String, QSchedulerMetaData> schedulers = new LinkedHashMap<>();
private Map<String, SchedulableType> schedulableTypes = new LinkedHashMap<>();
private Map<String, QSupplementalInstanceMetaData> supplementalMetaData = new LinkedHashMap<>();
private String deploymentMode;
@ -1224,4 +1229,106 @@ public class QInstance
metaData.addSelfToInstance(this);
}
/*******************************************************************************
**
*******************************************************************************/
public void addScheduler(QSchedulerMetaData scheduler)
{
String name = scheduler.getName();
if(!StringUtils.hasContent(name))
{
throw (new IllegalArgumentException("Attempted to add a scheduler without a name."));
}
if(this.schedulers.containsKey(name))
{
throw (new IllegalArgumentException("Attempted to add a second scheduler with name: " + name));
}
this.schedulers.put(name, scheduler);
}
/*******************************************************************************
**
*******************************************************************************/
public QSchedulerMetaData getScheduler(String name)
{
return (this.schedulers.get(name));
}
/*******************************************************************************
** Getter for schedulers
**
*******************************************************************************/
public Map<String, QSchedulerMetaData> getSchedulers()
{
return schedulers;
}
/*******************************************************************************
** Setter for schedulers
**
*******************************************************************************/
public void setSchedulers(Map<String, QSchedulerMetaData> schedulers)
{
this.schedulers = schedulers;
}
/*******************************************************************************
**
*******************************************************************************/
public void addSchedulableType(SchedulableType schedulableType)
{
String name = schedulableType.getName();
if(!StringUtils.hasContent(name))
{
throw (new IllegalArgumentException("Attempted to add a schedulableType without a name."));
}
if(this.schedulableTypes.containsKey(name))
{
throw (new IllegalArgumentException("Attempted to add a second schedulableType with name: " + name));
}
this.schedulableTypes.put(name, schedulableType);
}
/*******************************************************************************
**
*******************************************************************************/
public SchedulableType getSchedulableType(String name)
{
return (this.schedulableTypes.get(name));
}
/*******************************************************************************
** Getter for schedulableTypes
**
*******************************************************************************/
public Map<String, SchedulableType> getSchedulableTypes()
{
return schedulableTypes;
}
/*******************************************************************************
** Setter for schedulableTypes
**
*******************************************************************************/
public void setSchedulableTypes(Map<String, SchedulableType> schedulableTypes)
{
this.schedulableTypes = schedulableTypes;
}
}

View File

@ -29,6 +29,11 @@ package com.kingsrook.qqq.backend.core.model.metadata;
public interface TopLevelMetaDataInterface
{
/*******************************************************************************
**
*******************************************************************************/
String getName();
/*******************************************************************************
**
*******************************************************************************/

View File

@ -24,7 +24,6 @@ package com.kingsrook.qqq.backend.core.model.metadata.automation;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.TopLevelMetaDataInterface;
import com.kingsrook.qqq.backend.core.model.metadata.scheduleing.QScheduleMetaData;
/*******************************************************************************
@ -35,8 +34,6 @@ public class QAutomationProviderMetaData implements TopLevelMetaDataInterface
private String name;
private QAutomationProviderType type;
private QScheduleMetaData schedule;
/*******************************************************************************
@ -107,40 +104,6 @@ public class QAutomationProviderMetaData implements TopLevelMetaDataInterface
/*******************************************************************************
** Getter for schedule
**
*******************************************************************************/
public QScheduleMetaData getSchedule()
{
return schedule;
}
/*******************************************************************************
** Setter for schedule
**
*******************************************************************************/
public void setSchedule(QScheduleMetaData schedule)
{
this.schedule = schedule;
}
/*******************************************************************************
** Fluent setter for schedule
**
*******************************************************************************/
public QAutomationProviderMetaData withSchedule(QScheduleMetaData schedule)
{
this.schedule = schedule;
return (this);
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -55,6 +55,17 @@ public class QBrandingMetaData implements TopLevelMetaDataInterface
/*******************************************************************************
**
*******************************************************************************/
@Override
public String getName()
{
return "Branding";
}
/*******************************************************************************
** Getter for companyName
**

View File

@ -28,6 +28,7 @@ package com.kingsrook.qqq.backend.core.model.metadata.dashboard;
*******************************************************************************/
public class WidgetDropdownData
{
private String name;
private String possibleValueSourceName;
private String foreignKeyFieldName;
private String label;
@ -44,6 +45,9 @@ public class WidgetDropdownData
////////////////////////////////////////////////////////////////////////////////////////////////
private String labelForNullValue;
private WidgetDropdownType type = WidgetDropdownType.POSSIBLE_VALUE_SOURCE;
/*******************************************************************************
** Getter for possibleValueSourceName
@ -366,4 +370,65 @@ public class WidgetDropdownData
}
/*******************************************************************************
** Getter for type
*******************************************************************************/
public WidgetDropdownType getType()
{
return (this.type);
}
/*******************************************************************************
** Setter for type
*******************************************************************************/
public void setType(WidgetDropdownType type)
{
this.type = type;
}
/*******************************************************************************
** Fluent setter for type
*******************************************************************************/
public WidgetDropdownData withType(WidgetDropdownType type)
{
this.type = type;
return (this);
}
/*******************************************************************************
** Getter for name
*******************************************************************************/
public String getName()
{
return (this.name);
}
/*******************************************************************************
** Setter for name
*******************************************************************************/
public void setName(String name)
{
this.name = name;
}
/*******************************************************************************
** Fluent setter for name
*******************************************************************************/
public WidgetDropdownData withName(String name)
{
this.name = name;
return (this);
}
}

View File

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

View File

@ -0,0 +1,331 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.metadata.fields;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.List;
import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter;
import com.kingsrook.qqq.backend.core.actions.values.ValueBehaviorApplier;
import com.kingsrook.qqq.backend.core.logging.QLogger;
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.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
** Field Display Behavior class for customizing the display values used
** in date-time fields
*******************************************************************************/
public class DateTimeDisplayValueBehavior implements FieldDisplayBehavior<DateTimeDisplayValueBehavior>
{
private static final QLogger LOG = QLogger.getLogger(DateTimeDisplayValueBehavior.class);
private String zoneIdFromFieldName;
private String fallbackZoneId;
private String defaultZoneId;
private static DateTimeDisplayValueBehavior NOOP = new DateTimeDisplayValueBehavior();
/*******************************************************************************
**
*******************************************************************************/
@Override
public DateTimeDisplayValueBehavior getDefault()
{
return NOOP;
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void apply(ValueBehaviorApplier.Action action, List<QRecord> recordList, QInstance instance, QTableMetaData table, QFieldMetaData field)
{
if(StringUtils.hasContent(defaultZoneId))
{
applyDefaultZoneId(recordList, table, field);
}
else if(StringUtils.hasContent(zoneIdFromFieldName))
{
applyZoneIdFromFieldName(recordList, table, field);
}
}
/*******************************************************************************
**
*******************************************************************************/
private void applyDefaultZoneId(List<QRecord> recordList, QTableMetaData table, QFieldMetaData field)
{
for(QRecord record : CollectionUtils.nonNullList(recordList))
{
try
{
Instant instant = record.getValueInstant(field.getName());
ZonedDateTime zonedDateTime = instant.atZone(ZoneId.of(defaultZoneId));
record.setDisplayValue(field.getName(), QValueFormatter.formatDateTimeWithZone(zonedDateTime));
}
catch(Exception e)
{
LOG.info("Error applying defaultZoneId DateTimeDisplayValueBehavior", logPair("table", table.getName()), logPair("field", field.getName()), logPair("id", record.getValue(table.getPrimaryKeyField())));
}
}
}
/*******************************************************************************
**
*******************************************************************************/
private void applyZoneIdFromFieldName(List<QRecord> recordList, QTableMetaData table, QFieldMetaData field)
{
for(QRecord record : CollectionUtils.nonNullList(recordList))
{
try
{
Instant instant = record.getValueInstant(field.getName());
String zoneString = record.getValueString(zoneIdFromFieldName);
ZoneId zoneId;
try
{
zoneId = ZoneId.of(zoneString);
}
catch(Exception e)
{
////////////////////////////////////////////////////////////////////////////////////////////////
// if the zone string from the other field isn't valid, and we have a fallback, try to use it //
////////////////////////////////////////////////////////////////////////////////////////////////
if(StringUtils.hasContent(fallbackZoneId))
{
zoneId = ZoneId.of(fallbackZoneId);
}
else
{
throw (e);
}
}
ZonedDateTime zonedDateTime = instant.atZone(zoneId);
record.setDisplayValue(field.getName(), QValueFormatter.formatDateTimeWithZone(zonedDateTime));
}
catch(Exception e)
{
LOG.info("Error applying zoneIdFromFieldName DateTimeDisplayValueBehavior", e, logPair("table", table.getName()), logPair("field", field.getName()), logPair("id", record.getValue(table.getPrimaryKeyField())));
}
}
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public List<String> validateBehaviorConfiguration(QTableMetaData tableMetaData, QFieldMetaData fieldMetaData)
{
List<String> errors = new ArrayList<>();
String errorSuffix = " field [" + fieldMetaData.getName() + "] in table [" + tableMetaData.getName() + "]";
if(!QFieldType.DATE_TIME.equals(fieldMetaData.getType()))
{
errors.add("A DateTimeDisplayValueBehavior was a applied to a non-DATE_TIME" + errorSuffix);
}
//////////////////////////////////////////////////
// validate rules if zoneIdFromFieldName is set //
//////////////////////////////////////////////////
if(StringUtils.hasContent(zoneIdFromFieldName))
{
if(StringUtils.hasContent(defaultZoneId))
{
errors.add("You may not specify both zoneIdFromFieldName and defaultZoneId in DateTimeDisplayValueBehavior on" + errorSuffix);
}
if(!tableMetaData.getFields().containsKey(zoneIdFromFieldName))
{
errors.add("Unrecognized field name [" + zoneIdFromFieldName + "] for [zoneIdFromFieldName] in DateTimeDisplayValueBehavior on" + errorSuffix);
}
else
{
QFieldMetaData zoneIdField = tableMetaData.getFields().get(zoneIdFromFieldName);
if(!QFieldType.STRING.equals(zoneIdField.getType()))
{
errors.add("A non-STRING type [" + zoneIdField.getType() + "] was specified as the zoneIdFromFieldName field [" + zoneIdFromFieldName + "] in DateTimeDisplayValueBehavior on" + errorSuffix);
}
}
}
////////////////////////////////////////////
// validate rules if defaultZoneId is set //
////////////////////////////////////////////
if(StringUtils.hasContent(defaultZoneId))
{
/////////////////////////////////////////////////////////////////////////////////////////////
// would check that you didn't specify from zoneIdFromFieldName - but that's covered above //
/////////////////////////////////////////////////////////////////////////////////////////////
if(StringUtils.hasContent(fallbackZoneId))
{
errors.add("You may not specify both defaultZoneId and fallbackZoneId in DateTimeDisplayValueBehavior on" + errorSuffix);
}
try
{
ZoneId.of(defaultZoneId);
}
catch(Exception e)
{
errors.add("Invalid ZoneId [" + defaultZoneId + "] for [defaultZoneId] in DateTimeDisplayValueBehavior on" + errorSuffix + "; " + e.getMessage());
}
}
/////////////////////////////////////////////
// validate rules if fallbackZoneId is set //
/////////////////////////////////////////////
if(StringUtils.hasContent(fallbackZoneId))
{
if(!StringUtils.hasContent(zoneIdFromFieldName))
{
errors.add("You may only set fallbackZoneId if using zoneIdFromFieldName in DateTimeDisplayValueBehavior on" + errorSuffix);
}
try
{
ZoneId.of(fallbackZoneId);
}
catch(Exception e)
{
errors.add("Invalid ZoneId [" + fallbackZoneId + "] for [fallbackZoneId] in DateTimeDisplayValueBehavior on" + errorSuffix + "; " + e.getMessage());
}
}
return (errors);
}
/*******************************************************************************
** Getter for zoneIdFromFieldName
*******************************************************************************/
public String getZoneIdFromFieldName()
{
return (this.zoneIdFromFieldName);
}
/*******************************************************************************
** Setter for zoneIdFromFieldName
*******************************************************************************/
public void setZoneIdFromFieldName(String zoneIdFromFieldName)
{
this.zoneIdFromFieldName = zoneIdFromFieldName;
}
/*******************************************************************************
** Fluent setter for zoneIdFromFieldName
*******************************************************************************/
public DateTimeDisplayValueBehavior withZoneIdFromFieldName(String zoneIdFromFieldName)
{
this.zoneIdFromFieldName = zoneIdFromFieldName;
return (this);
}
/*******************************************************************************
** Getter for defaultZoneId
*******************************************************************************/
public String getDefaultZoneId()
{
return (this.defaultZoneId);
}
/*******************************************************************************
** Setter for defaultZoneId
*******************************************************************************/
public void setDefaultZoneId(String defaultZoneId)
{
this.defaultZoneId = defaultZoneId;
}
/*******************************************************************************
** Fluent setter for defaultZoneId
*******************************************************************************/
public DateTimeDisplayValueBehavior withDefaultZoneId(String defaultZoneId)
{
this.defaultZoneId = defaultZoneId;
return (this);
}
/*******************************************************************************
** Getter for fallbackZoneId
*******************************************************************************/
public String getFallbackZoneId()
{
return (this.fallbackZoneId);
}
/*******************************************************************************
** Setter for fallbackZoneId
*******************************************************************************/
public void setFallbackZoneId(String fallbackZoneId)
{
this.fallbackZoneId = fallbackZoneId;
}
/*******************************************************************************
** Fluent setter for fallbackZoneId
*******************************************************************************/
public DateTimeDisplayValueBehavior withFallbackZoneId(String fallbackZoneId)
{
this.fallbackZoneId = fallbackZoneId;
return (this);
}
}

View File

@ -1,6 +1,6 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
@ -26,7 +26,6 @@ import java.io.Serializable;
import java.time.Instant;
import java.time.LocalDate;
import java.util.List;
import java.util.Set;
import com.kingsrook.qqq.backend.core.actions.values.ValueBehaviorApplier;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
@ -66,16 +65,12 @@ public enum DynamicDefaultValueBehavior implements FieldBehavior<DynamicDefaultV
**
*******************************************************************************/
@Override
public void apply(ValueBehaviorApplier.Action action, List<QRecord> recordList, QInstance instance, QTableMetaData table, QFieldMetaData field, Set<FieldBehavior<?>> behaviorsToOmit)
public void apply(ValueBehaviorApplier.Action action, List<QRecord> recordList, QInstance instance, QTableMetaData table, QFieldMetaData field)
{
if(this.equals(NONE))
{
return;
}
if(behaviorsToOmit != null && behaviorsToOmit.contains(this))
{
return;
}
switch(this)
{

View File

@ -1,6 +1,6 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
@ -22,8 +22,9 @@
package com.kingsrook.qqq.backend.core.model.metadata.fields;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.kingsrook.qqq.backend.core.actions.values.ValueBehaviorApplier;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
@ -34,8 +35,10 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
** Interface for (expected to be?) enums which define behaviors that get applied
** to fields.
**
** At the present, these behaviors get applied before a field is stored (insert
** or update), through the ValueBehaviorApplier class.
** Some of these behaviors get applied before a field is stored (insert
** or update), through the ValueBehaviorApplier class. Others can be used to
** do more advanced display formatting than the displayFormat string alone can
** do (see QValueFormatter).
**
*******************************************************************************/
public interface FieldBehavior<T extends FieldBehavior<T>>
@ -45,12 +48,13 @@ public interface FieldBehavior<T extends FieldBehavior<T>>
** In case a behavior of this type wasn't set on the field, what should the
** default of this type be?
*******************************************************************************/
@JsonIgnore
T getDefault();
/*******************************************************************************
** Apply this behavior to a list of records
*******************************************************************************/
void apply(ValueBehaviorApplier.Action action, List<QRecord> recordList, QInstance instance, QTableMetaData table, QFieldMetaData field, Set<FieldBehavior<?>> behaviorsToOmit);
void apply(ValueBehaviorApplier.Action action, List<QRecord> recordList, QInstance instance, QTableMetaData table, QFieldMetaData field);
/*******************************************************************************
** control if multiple behaviors of this type should be allowed together on a field.
@ -60,4 +64,14 @@ public interface FieldBehavior<T extends FieldBehavior<T>>
return (false);
}
/*******************************************************************************
** allow this behavior to be validated during QInstance validation.
**
** return a list of validation errors, if there are any.
*******************************************************************************/
default List<String> validateBehaviorConfiguration(QTableMetaData tableMetaData, QFieldMetaData fieldMetaData)
{
return (Collections.emptyList());
}
}

View File

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

View File

@ -716,6 +716,17 @@ public class QFieldMetaData implements Cloneable
{
return (behaviorType.getEnumConstants()[0].getDefault());
}
else
{
try
{
return (behaviorType.getConstructor().newInstance().getDefault());
}
catch(Exception e)
{
LOG.warn("Error getting default behaviorType for [" + behaviorType.getSimpleName() + "]", e);
}
}
return (null);
}

View File

@ -37,6 +37,7 @@ public enum QFieldType
{
STRING,
INTEGER,
LONG,
DECIMAL,
BOOLEAN,
DATE,
@ -65,6 +66,10 @@ public enum QFieldType
{
return (INTEGER);
}
if(c.equals(Long.class) || c.equals(long.class))
{
return (LONG);
}
if(c.equals(BigDecimal.class))
{
return (DECIMAL);
@ -110,7 +115,7 @@ public enum QFieldType
*******************************************************************************/
public boolean isNumeric()
{
return this == QFieldType.INTEGER || this == QFieldType.DECIMAL;
return this == QFieldType.INTEGER || this == QFieldType.LONG || this == QFieldType.DECIMAL;
}

View File

@ -1,6 +1,6 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
@ -23,7 +23,6 @@ package com.kingsrook.qqq.backend.core.model.metadata.fields;
import java.util.List;
import java.util.Set;
import com.kingsrook.qqq.backend.core.actions.values.ValueBehaviorApplier;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
@ -66,16 +65,12 @@ public enum ValueTooLongBehavior implements FieldBehavior<ValueTooLongBehavior>
**
*******************************************************************************/
@Override
public void apply(ValueBehaviorApplier.Action action, List<QRecord> recordList, QInstance instance, QTableMetaData table, QFieldMetaData field, Set<FieldBehavior<?>> behaviorsToOmit)
public void apply(ValueBehaviorApplier.Action action, List<QRecord> recordList, QInstance instance, QTableMetaData table, QFieldMetaData field)
{
if(this.equals(PASS_THROUGH))
{
return;
}
if(behaviorsToOmit != null && behaviorsToOmit.contains(this))
{
return;
}
String fieldName = field.getName();
if(!QFieldType.STRING.equals(field.getType()))

View File

@ -22,6 +22,7 @@
package com.kingsrook.qqq.backend.core.model.metadata.frontend;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonInclude;
@ -60,6 +61,7 @@ public class QFrontendWidgetMetaData
protected Map<String, QIcon> icons;
protected Map<String, QHelpContent> helpContent;
protected Map<String, Serializable> defaultValues;
private final boolean hasPermission;
@ -95,6 +97,7 @@ public class QFrontendWidgetMetaData
}
this.helpContent = widgetMetaData.getHelpContent();
this.defaultValues = widgetMetaData.getDefaultValues();
hasPermission = PermissionsHelper.hasWidgetPermission(actionInput, name);
}
@ -274,4 +277,16 @@ public class QFrontendWidgetMetaData
{
return helpContent;
}
/*******************************************************************************
** Getter for defaultValues
**
*******************************************************************************/
public Map<String, Serializable> getDefaultValues()
{
return defaultValues;
}
}

View File

@ -23,6 +23,8 @@ package com.kingsrook.qqq.backend.core.model.metadata.layout;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -363,11 +365,11 @@ public class QAppMetaData implements QAppChildMetaData, MetaDataWithPermissionRu
/*******************************************************************************
**
*******************************************************************************/
public QAppMetaData withSectionOfChildren(QAppSection section, QAppChildMetaData... children)
public QAppMetaData withSectionOfChildren(QAppSection section, Collection<? extends QAppChildMetaData> children)
{
this.addSection(section);
for(QAppChildMetaData child : children)
for(QAppChildMetaData child : CollectionUtils.nonNullCollection(children))
{
withChild(child);
if(child instanceof QTableMetaData)
@ -392,6 +394,15 @@ public class QAppMetaData implements QAppChildMetaData, MetaDataWithPermissionRu
}
/*******************************************************************************
**
*******************************************************************************/
public QAppMetaData withSectionOfChildren(QAppSection section, QAppChildMetaData... children)
{
return (withSectionOfChildren(section, children == null ? null : Arrays.stream(children).toList()));
}
/*******************************************************************************
** Getter for permissionRules

View File

@ -155,4 +155,26 @@ public class AbstractProcessMetaDataBuilder
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
public AbstractProcessMetaDataBuilder withVariantRunStrategy(VariantRunStrategy variantRunStrategy)
{
processMetaData.setVariantRunStrategy(variantRunStrategy);
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
public AbstractProcessMetaDataBuilder withVariantBackend(String variantBackend)
{
processMetaData.setVariantBackend(variantBackend);
return (this);
}
}

View File

@ -64,6 +64,9 @@ public class QProcessMetaData implements QAppChildMetaData, MetaDataWithPermissi
private QScheduleMetaData schedule;
private VariantRunStrategy variantRunStrategy;
private String variantBackend;
private Map<String, QSupplementalProcessMetaData> supplementalMetaData;
@ -671,4 +674,66 @@ public class QProcessMetaData implements QAppChildMetaData, MetaDataWithPermissi
return (this);
}
/*******************************************************************************
** Getter for variantRunStrategy
*******************************************************************************/
public VariantRunStrategy getVariantRunStrategy()
{
return (this.variantRunStrategy);
}
/*******************************************************************************
** Setter for variantRunStrategy
*******************************************************************************/
public void setVariantRunStrategy(VariantRunStrategy variantRunStrategy)
{
this.variantRunStrategy = variantRunStrategy;
}
/*******************************************************************************
** Fluent setter for variantRunStrategy
*******************************************************************************/
public QProcessMetaData withVariantRunStrategy(VariantRunStrategy variantRunStrategy)
{
this.variantRunStrategy = variantRunStrategy;
return (this);
}
/*******************************************************************************
** Getter for variantBackend
*******************************************************************************/
public String getVariantBackend()
{
return (this.variantBackend);
}
/*******************************************************************************
** Setter for variantBackend
*******************************************************************************/
public void setVariantBackend(String variantBackend)
{
this.variantBackend = variantBackend;
}
/*******************************************************************************
** Fluent setter for variantBackend
*******************************************************************************/
public QProcessMetaData withVariantBackend(String variantBackend)
{
this.variantBackend = variantBackend;
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.metadata.processes;
/*******************************************************************************
**
*******************************************************************************/
public enum VariantRunStrategy
{
PARALLEL,
SERIAL
}

View File

@ -22,9 +22,6 @@
package com.kingsrook.qqq.backend.core.model.metadata.queues;
import com.kingsrook.qqq.backend.core.model.metadata.scheduleing.QScheduleMetaData;
/*******************************************************************************
** Meta-data for an source of Amazon SQS queues (e.g, an aws account/credential
** set, with a common base URL).
@ -39,8 +36,6 @@ public class SQSQueueProviderMetaData extends QQueueProviderMetaData
private String region;
private String baseURL;
private QScheduleMetaData schedule;
/*******************************************************************************
@ -201,38 +196,4 @@ public class SQSQueueProviderMetaData extends QQueueProviderMetaData
return (this);
}
/*******************************************************************************
** Getter for schedule
**
*******************************************************************************/
public QScheduleMetaData getSchedule()
{
return schedule;
}
/*******************************************************************************
** Setter for schedule
**
*******************************************************************************/
public void setSchedule(QScheduleMetaData schedule)
{
this.schedule = schedule;
}
/*******************************************************************************
** Fluent setter for schedule
**
*******************************************************************************/
public SQSQueueProviderMetaData withSchedule(QScheduleMetaData schedule)
{
this.schedule = schedule;
return (this);
}
}

View File

@ -22,29 +22,41 @@
package com.kingsrook.qqq.backend.core.model.metadata.scheduleing;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
/*******************************************************************************
** Meta-data to define scheduled actions within QQQ.
**
** Initially, only supports repeating jobs, either on a given # of seconds or millis.
** Supports repeating jobs, either on a given # of seconds or millis, or cron
** expressions (though cron may not be supported by all schedulers!)
**
** Can also specify an initialDelay - e.g., to avoid all jobs starting up at the
** same moment.
**
** In the future we most likely would want to allow cron strings to be added here.
*******************************************************************************/
public class QScheduleMetaData
{
public enum RunStrategy
{PARALLEL, SERIAL}
private String schedulerName;
private String description;
private Integer repeatSeconds;
private Integer repeatMillis;
private Integer initialDelaySeconds;
private Integer initialDelayMillis;
private RunStrategy variantRunStrategy;
private String variantBackend;
private String cronExpression;
private String cronTimeZoneId;
/*******************************************************************************
**
*******************************************************************************/
public boolean isCron()
{
return StringUtils.hasContent(cronExpression);
}
@ -185,63 +197,125 @@ public class QScheduleMetaData
/*******************************************************************************
** Getter for variantBackend
** Getter for cronExpression
*******************************************************************************/
public String getVariantBackend()
public String getCronExpression()
{
return (this.variantBackend);
return (this.cronExpression);
}
/*******************************************************************************
** Setter for variantBackend
** Setter for cronExpression
*******************************************************************************/
public void setVariantBackend(String variantBackend)
public void setCronExpression(String cronExpression)
{
this.variantBackend = variantBackend;
this.cronExpression = cronExpression;
}
/*******************************************************************************
** Fluent setter for variantBackend
** Fluent setter for cronExpression
*******************************************************************************/
public QScheduleMetaData withBackendVariant(String backendVariant)
public QScheduleMetaData withCronExpression(String cronExpression)
{
this.variantBackend = backendVariant;
this.cronExpression = cronExpression;
return (this);
}
/*******************************************************************************
** Getter for variantRunStrategy
** Getter for cronTimeZoneId
*******************************************************************************/
public RunStrategy getVariantRunStrategy()
public String getCronTimeZoneId()
{
return (this.variantRunStrategy);
return (this.cronTimeZoneId);
}
/*******************************************************************************
** Setter for variantRunStrategy
** Setter for cronTimeZoneId
*******************************************************************************/
public void setVariantRunStrategy(RunStrategy variantRunStrategy)
public void setCronTimeZoneId(String cronTimeZoneId)
{
this.variantRunStrategy = variantRunStrategy;
this.cronTimeZoneId = cronTimeZoneId;
}
/*******************************************************************************
** Fluent setter for variantRunStrategy
** Fluent setter for cronTimeZoneId
*******************************************************************************/
public QScheduleMetaData withVariantRunStrategy(RunStrategy variantRunStrategy)
public QScheduleMetaData withCronTimeZoneId(String cronTimeZoneId)
{
this.variantRunStrategy = variantRunStrategy;
this.cronTimeZoneId = cronTimeZoneId;
return (this);
}
/*******************************************************************************
** Getter for schedulerName
*******************************************************************************/
public String getSchedulerName()
{
return (this.schedulerName);
}
/*******************************************************************************
** Setter for schedulerName
*******************************************************************************/
public void setSchedulerName(String schedulerName)
{
this.schedulerName = schedulerName;
}
/*******************************************************************************
** Fluent setter for schedulerName
*******************************************************************************/
public QScheduleMetaData withSchedulerName(String schedulerName)
{
this.schedulerName = schedulerName;
return (this);
}
/*******************************************************************************
** Getter for description
*******************************************************************************/
public String getDescription()
{
return (this.description);
}
/*******************************************************************************
** Setter for description
*******************************************************************************/
public void setDescription(String description)
{
this.description = description;
}
/*******************************************************************************
** Fluent setter for description
*******************************************************************************/
public QScheduleMetaData withDescription(String description)
{
this.description = description;
return (this);
}
}

View File

@ -0,0 +1,141 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.metadata.scheduleing;
import java.util.function.Supplier;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.TopLevelMetaDataInterface;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.scheduler.QSchedulerInterface;
/*******************************************************************************
**
*******************************************************************************/
public abstract class QSchedulerMetaData implements TopLevelMetaDataInterface
{
private String name;
private String type;
/*******************************************************************************
**
*******************************************************************************/
public boolean supportsCronSchedules()
{
return (false);
}
/*******************************************************************************
**
*******************************************************************************/
public boolean mayUseInScheduledJobsTable()
{
return (true);
}
/*******************************************************************************
**
*******************************************************************************/
public abstract QSchedulerInterface initSchedulerInstance(QInstance qInstance, Supplier<QSession> systemSessionSupplier) throws QException;
/*******************************************************************************
** Getter for name
*******************************************************************************/
public String getName()
{
return (this.name);
}
/*******************************************************************************
** Setter for name
*******************************************************************************/
public void setName(String name)
{
this.name = name;
}
/*******************************************************************************
** Fluent setter for name
*******************************************************************************/
public QSchedulerMetaData withName(String name)
{
this.name = name;
return (this);
}
/*******************************************************************************
** Getter for type
*******************************************************************************/
public String getType()
{
return (this.type);
}
/*******************************************************************************
** Setter for type
*******************************************************************************/
public void setType(String type)
{
this.type = type;
}
/*******************************************************************************
** Fluent setter for type
*******************************************************************************/
public QSchedulerMetaData withType(String type)
{
this.type = type;
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void addSelfToInstance(QInstance qInstance)
{
qInstance.addScheduler(this);
}
}

View File

@ -0,0 +1,130 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.metadata.scheduleing.quartz;
import java.util.Properties;
import java.util.function.Supplier;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.scheduleing.QSchedulerMetaData;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.scheduler.QSchedulerInterface;
import com.kingsrook.qqq.backend.core.scheduler.quartz.QuartzScheduler;
/*******************************************************************************
**
*******************************************************************************/
public class QuartzSchedulerMetaData extends QSchedulerMetaData
{
private static final QLogger LOG = QLogger.getLogger(QuartzSchedulerMetaData.class);
public static final String TYPE = "quartz";
private Properties properties;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public QuartzSchedulerMetaData()
{
setType(TYPE);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public boolean supportsCronSchedules()
{
return (true);
}
/*******************************************************************************
**
*******************************************************************************/
public boolean mayUseInScheduledJobsTable()
{
return (true);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public QSchedulerInterface initSchedulerInstance(QInstance qInstance, Supplier<QSession> systemSessionSupplier) throws QException
{
try
{
QuartzScheduler quartzScheduler = QuartzScheduler.initInstance(qInstance, getName(), getProperties(), systemSessionSupplier);
return (quartzScheduler);
}
catch(Exception e)
{
LOG.error("Error initializing quartz scheduler", e);
throw (new QException("Error initializing quartz scheduler", e));
}
}
/*******************************************************************************
** Getter for properties
*******************************************************************************/
public Properties getProperties()
{
return (this.properties);
}
/*******************************************************************************
** Setter for properties
*******************************************************************************/
public void setProperties(Properties properties)
{
this.properties = properties;
}
/*******************************************************************************
** Fluent setter for properties
*******************************************************************************/
public QuartzSchedulerMetaData withProperties(Properties properties)
{
this.properties = properties;
return (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.metadata.scheduleing.simple;
import java.util.function.Supplier;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.scheduleing.QSchedulerMetaData;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.scheduler.QSchedulerInterface;
import com.kingsrook.qqq.backend.core.scheduler.simple.SimpleScheduler;
/*******************************************************************************
**
*******************************************************************************/
public class SimpleSchedulerMetaData extends QSchedulerMetaData
{
public static final String TYPE = "simple";
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public SimpleSchedulerMetaData()
{
setType(TYPE);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public boolean supportsCronSchedules()
{
return (false);
}
/*******************************************************************************
**
*******************************************************************************/
public boolean mayUseInScheduledJobsTable()
{
return (false);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public QSchedulerInterface initSchedulerInstance(QInstance qInstance, Supplier<QSession> systemSessionSupplier)
{
SimpleScheduler simpleScheduler = SimpleScheduler.getInstance(qInstance);
simpleScheduler.setSessionSupplier(systemSessionSupplier);
simpleScheduler.setSchedulerName(getName());
return simpleScheduler;
}
}

View File

@ -22,6 +22,10 @@
package com.kingsrook.qqq.backend.core.model.metadata.tables;
import java.util.HashSet;
import java.util.Set;
/*******************************************************************************
** Things that can be done to tables, fields.
**
@ -38,5 +42,26 @@ public enum Capability
// keep these values in sync with Capability.ts in qqq-frontend-core //
///////////////////////////////////////////////////////////////////////
QUERY_STATS
QUERY_STATS;
/*******************************************************************************
**
*******************************************************************************/
public static Set<Capability> allReadCapabilities()
{
return (new HashSet<>(Set.of(TABLE_QUERY, TABLE_GET, TABLE_COUNT, QUERY_STATS)));
}
/*******************************************************************************
**
*******************************************************************************/
public static Set<Capability> allWriteCapabilities()
{
return (new HashSet<>(Set.of(TABLE_INSERT, TABLE_UPDATE, TABLE_DELETE)));
}
}

View File

@ -24,6 +24,7 @@ package com.kingsrook.qqq.backend.core.model.metadata.tables.automation;
import java.util.ArrayList;
import java.util.List;
import com.kingsrook.qqq.backend.core.model.metadata.scheduleing.QScheduleMetaData;
/*******************************************************************************
@ -37,6 +38,8 @@ public class QTableAutomationDetails
private Integer overrideBatchSize;
private QScheduleMetaData schedule;
private String shardByFieldName; // field in "this" table, to use for sharding
private String shardSourceTableName; // name of the table where the shards are defined as rows
private String shardLabelFieldName; // field in shard-source-table to use for labeling shards
@ -317,4 +320,35 @@ public class QTableAutomationDetails
return (this);
}
/*******************************************************************************
** Getter for schedule
*******************************************************************************/
public QScheduleMetaData getSchedule()
{
return (this.schedule);
}
/*******************************************************************************
** Setter for schedule
*******************************************************************************/
public void setSchedule(QScheduleMetaData schedule)
{
this.schedule = schedule;
}
/*******************************************************************************
** Fluent setter for schedule
*******************************************************************************/
public QTableAutomationDetails withSchedule(QScheduleMetaData schedule)
{
this.schedule = schedule;
return (this);
}
}

View File

@ -65,6 +65,7 @@ public class QueryStatMetaDataProvider
instance.addTable(defineStandardTable(QueryStatJoinTable.TABLE_NAME, QueryStatJoinTable.class, backendName, backendDetailEnricher));
instance.addTable(defineStandardTable(QueryStatCriteriaField.TABLE_NAME, QueryStatCriteriaField.class, backendName, backendDetailEnricher)
.withIcon(new QIcon().withName("filter_alt"))
.withExposedJoin(new ExposedJoin().withJoinTable(QueryStat.TABLE_NAME))
);
@ -115,6 +116,7 @@ public class QueryStatMetaDataProvider
QTableMetaData table = new QTableMetaData()
.withName(QueryStat.TABLE_NAME)
.withIcon(new QIcon().withName("query_stats"))
.withBackendName(backendName)
.withAuditRules(new QAuditRules().withAuditLevel(AuditLevel.NONE))
.withRecordLabelFormat("%s")

View File

@ -0,0 +1,496 @@
/*
* 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.scheduledjobs;
import java.time.Instant;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.common.TimeZonePossibleValueSourceMetaDataProvider;
import com.kingsrook.qqq.backend.core.model.data.QAssociation;
import com.kingsrook.qqq.backend.core.model.data.QField;
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.fields.DisplayFormat;
import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.collections.MutableMap;
/*******************************************************************************
**
*******************************************************************************/
public class ScheduledJob extends QRecordEntity
{
public static final String TABLE_NAME = "scheduledJob";
@QField(isEditable = false)
private Integer id;
@QField(isEditable = false)
private Instant createDate;
@QField(isEditable = false)
private Instant modifyDate;
@QField(isRequired = true, maxLength = 100, valueTooLongBehavior = ValueTooLongBehavior.TRUNCATE)
private String label;
@QField(maxLength = 250, valueTooLongBehavior = ValueTooLongBehavior.TRUNCATE_ELLIPSIS)
private String description;
@QField(isRequired = true, label = "Scheduler", maxLength = 100, valueTooLongBehavior = ValueTooLongBehavior.ERROR, possibleValueSourceName = SchedulersPossibleValueSource.NAME)
private String schedulerName;
@QField(maxLength = 100, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
private String cronExpression;
@QField(maxLength = 100, valueTooLongBehavior = ValueTooLongBehavior.ERROR, possibleValueSourceName = TimeZonePossibleValueSourceMetaDataProvider.NAME)
private String cronTimeZoneId;
@QField(displayFormat = DisplayFormat.COMMAS)
private Integer repeatSeconds;
@QField(isRequired = true, maxLength = 100, valueTooLongBehavior = ValueTooLongBehavior.ERROR, possibleValueSourceName = ScheduledJobTypePossibleValueSource.NAME)
private String type;
@QField(isRequired = true)
private Boolean isActive;
@QAssociation(name = ScheduledJobParameter.TABLE_NAME)
private List<ScheduledJobParameter> jobParameters;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public ScheduledJob()
{
}
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public ScheduledJob(QRecord qRecord) throws QException
{
populateFromQRecord(qRecord);
}
/*******************************************************************************
** Getter for id
*******************************************************************************/
public Integer getId()
{
return (this.id);
}
/*******************************************************************************
** Setter for id
*******************************************************************************/
public void setId(Integer id)
{
this.id = id;
}
/*******************************************************************************
** Fluent setter for id
*******************************************************************************/
public ScheduledJob withId(Integer id)
{
this.id = id;
return (this);
}
/*******************************************************************************
** Getter for createDate
*******************************************************************************/
public Instant getCreateDate()
{
return (this.createDate);
}
/*******************************************************************************
** Setter for createDate
*******************************************************************************/
public void setCreateDate(Instant createDate)
{
this.createDate = createDate;
}
/*******************************************************************************
** Fluent setter for createDate
*******************************************************************************/
public ScheduledJob withCreateDate(Instant createDate)
{
this.createDate = createDate;
return (this);
}
/*******************************************************************************
** Getter for modifyDate
*******************************************************************************/
public Instant getModifyDate()
{
return (this.modifyDate);
}
/*******************************************************************************
** Setter for modifyDate
*******************************************************************************/
public void setModifyDate(Instant modifyDate)
{
this.modifyDate = modifyDate;
}
/*******************************************************************************
** Fluent setter for modifyDate
*******************************************************************************/
public ScheduledJob withModifyDate(Instant modifyDate)
{
this.modifyDate = modifyDate;
return (this);
}
/*******************************************************************************
** 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 ScheduledJob withLabel(String label)
{
this.label = label;
return (this);
}
/*******************************************************************************
** Getter for description
*******************************************************************************/
public String getDescription()
{
return (this.description);
}
/*******************************************************************************
** Setter for description
*******************************************************************************/
public void setDescription(String description)
{
this.description = description;
}
/*******************************************************************************
** Fluent setter for description
*******************************************************************************/
public ScheduledJob withDescription(String description)
{
this.description = description;
return (this);
}
/*******************************************************************************
** Getter for cronExpression
*******************************************************************************/
public String getCronExpression()
{
return (this.cronExpression);
}
/*******************************************************************************
** Setter for cronExpression
*******************************************************************************/
public void setCronExpression(String cronExpression)
{
this.cronExpression = cronExpression;
}
/*******************************************************************************
** Fluent setter for cronExpression
*******************************************************************************/
public ScheduledJob withCronExpression(String cronExpression)
{
this.cronExpression = cronExpression;
return (this);
}
/*******************************************************************************
** Getter for cronTimeZoneId
*******************************************************************************/
public String getCronTimeZoneId()
{
return (this.cronTimeZoneId);
}
/*******************************************************************************
** Setter for cronTimeZoneId
*******************************************************************************/
public void setCronTimeZoneId(String cronTimeZoneId)
{
this.cronTimeZoneId = cronTimeZoneId;
}
/*******************************************************************************
** Fluent setter for cronTimeZoneId
*******************************************************************************/
public ScheduledJob withCronTimeZoneId(String cronTimeZoneId)
{
this.cronTimeZoneId = cronTimeZoneId;
return (this);
}
/*******************************************************************************
** Getter for isActive
*******************************************************************************/
public Boolean getIsActive()
{
return (this.isActive);
}
/*******************************************************************************
** Setter for isActive
*******************************************************************************/
public void setIsActive(Boolean isActive)
{
this.isActive = isActive;
}
/*******************************************************************************
** Fluent setter for isActive
*******************************************************************************/
public ScheduledJob withIsActive(Boolean isActive)
{
this.isActive = isActive;
return (this);
}
/*******************************************************************************
** Getter for schedulerName
*******************************************************************************/
public String getSchedulerName()
{
return (this.schedulerName);
}
/*******************************************************************************
** Setter for schedulerName
*******************************************************************************/
public void setSchedulerName(String schedulerName)
{
this.schedulerName = schedulerName;
}
/*******************************************************************************
** Fluent setter for schedulerName
*******************************************************************************/
public ScheduledJob withSchedulerName(String schedulerName)
{
this.schedulerName = schedulerName;
return (this);
}
/*******************************************************************************
** Getter for type
*******************************************************************************/
public String getType()
{
return (this.type);
}
/*******************************************************************************
** Setter for type
*******************************************************************************/
public void setType(String type)
{
this.type = type;
}
/*******************************************************************************
** Fluent setter for type
*******************************************************************************/
public ScheduledJob withType(String type)
{
this.type = type;
return (this);
}
/*******************************************************************************
** Getter for jobParameters
*******************************************************************************/
public List<ScheduledJobParameter> getJobParameters()
{
return (this.jobParameters);
}
/*******************************************************************************
** Getter for jobParameters - but a map of just the key=value pairs.
*******************************************************************************/
public Map<String, String> getJobParametersMap()
{
if(CollectionUtils.nullSafeIsEmpty(this.jobParameters))
{
return (new HashMap<>());
}
///////////////////////////////////////////////////////////////////////////////////////
// wrap in mutable map, just to avoid any immutable or other bs from toMap's default //
///////////////////////////////////////////////////////////////////////////////////////
return new MutableMap<>(jobParameters.stream().collect(Collectors.toMap(ScheduledJobParameter::getKey, ScheduledJobParameter::getValue)));
}
/*******************************************************************************
** Setter for jobParameters
*******************************************************************************/
public void setJobParameters(List<ScheduledJobParameter> jobParameters)
{
this.jobParameters = jobParameters;
}
/*******************************************************************************
** Fluent setter for jobParameters
*******************************************************************************/
public ScheduledJob withJobParameters(List<ScheduledJobParameter> jobParameters)
{
this.jobParameters = jobParameters;
return (this);
}
/*******************************************************************************
** Getter for repeatSeconds
*******************************************************************************/
public Integer getRepeatSeconds()
{
return (this.repeatSeconds);
}
/*******************************************************************************
** Setter for repeatSeconds
*******************************************************************************/
public void setRepeatSeconds(Integer repeatSeconds)
{
this.repeatSeconds = repeatSeconds;
}
/*******************************************************************************
** Fluent setter for repeatSeconds
*******************************************************************************/
public ScheduledJob withRepeatSeconds(Integer repeatSeconds)
{
this.repeatSeconds = repeatSeconds;
return (this);
}
}

View File

@ -0,0 +1,266 @@
/*
* 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.scheduledjobs;
import java.time.Instant;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.data.QField;
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.fields.ValueTooLongBehavior;
/*******************************************************************************
**
*******************************************************************************/
public class ScheduledJobParameter extends QRecordEntity
{
public static final String TABLE_NAME = "scheduledJobParameter";
@QField(isEditable = false)
private Integer id;
@QField(isEditable = false)
private Instant createDate;
@QField(isEditable = false)
private Instant modifyDate;
@QField(possibleValueSourceName = ScheduledJob.TABLE_NAME, isRequired = true)
private Integer scheduledJobId;
@QField(maxLength = 250, valueTooLongBehavior = ValueTooLongBehavior.ERROR, isRequired = true)
private String key;
@QField(maxLength = 250, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
private String value;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public ScheduledJobParameter()
{
}
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public ScheduledJobParameter(QRecord qRecord) throws QException
{
populateFromQRecord(qRecord);
}
/*******************************************************************************
** Getter for id
*******************************************************************************/
public Integer getId()
{
return (this.id);
}
/*******************************************************************************
** Setter for id
*******************************************************************************/
public void setId(Integer id)
{
this.id = id;
}
/*******************************************************************************
** Fluent setter for id
*******************************************************************************/
public ScheduledJobParameter withId(Integer id)
{
this.id = id;
return (this);
}
/*******************************************************************************
** Getter for createDate
*******************************************************************************/
public Instant getCreateDate()
{
return (this.createDate);
}
/*******************************************************************************
** Setter for createDate
*******************************************************************************/
public void setCreateDate(Instant createDate)
{
this.createDate = createDate;
}
/*******************************************************************************
** Fluent setter for createDate
*******************************************************************************/
public ScheduledJobParameter withCreateDate(Instant createDate)
{
this.createDate = createDate;
return (this);
}
/*******************************************************************************
** Getter for modifyDate
*******************************************************************************/
public Instant getModifyDate()
{
return (this.modifyDate);
}
/*******************************************************************************
** Setter for modifyDate
*******************************************************************************/
public void setModifyDate(Instant modifyDate)
{
this.modifyDate = modifyDate;
}
/*******************************************************************************
** Fluent setter for modifyDate
*******************************************************************************/
public ScheduledJobParameter withModifyDate(Instant modifyDate)
{
this.modifyDate = modifyDate;
return (this);
}
/*******************************************************************************
** Getter for scheduledJobId
*******************************************************************************/
public Integer getScheduledJobId()
{
return (this.scheduledJobId);
}
/*******************************************************************************
** Setter for scheduledJobId
*******************************************************************************/
public void setScheduledJobId(Integer scheduledJobId)
{
this.scheduledJobId = scheduledJobId;
}
/*******************************************************************************
** Fluent setter for scheduledJobId
*******************************************************************************/
public ScheduledJobParameter withScheduledJobId(Integer scheduledJobId)
{
this.scheduledJobId = scheduledJobId;
return (this);
}
/*******************************************************************************
** Getter for key
*******************************************************************************/
public String getKey()
{
return (this.key);
}
/*******************************************************************************
** Setter for key
*******************************************************************************/
public void setKey(String key)
{
this.key = key;
}
/*******************************************************************************
** Fluent setter for key
*******************************************************************************/
public ScheduledJobParameter withKey(String key)
{
this.key = key;
return (this);
}
/*******************************************************************************
** Getter for value
*******************************************************************************/
public String getValue()
{
return (this.value);
}
/*******************************************************************************
** Setter for value
*******************************************************************************/
public void setValue(String value)
{
this.value = value;
}
/*******************************************************************************
** Fluent setter for value
*******************************************************************************/
public ScheduledJobParameter withValue(String value)
{
this.value = value;
return (this);
}
}

View File

@ -0,0 +1,37 @@
/*
* 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.scheduledjobs;
/*******************************************************************************
** enum of core schedulable types that QQQ schedule manager directly knows about.
**
** note though, that applications can define their own schedulable types,
** by adding SchedulableType meta-data to the QInstance, and providing classes
** that implement SchedulableRunner.
*******************************************************************************/
public enum ScheduledJobType
{
PROCESS,
QUEUE_PROCESSOR,
TABLE_AUTOMATIONS
}

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.scheduledjobs;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import com.kingsrook.qqq.backend.core.actions.values.QCustomPossibleValueProvider;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.values.SearchPossibleValueSourceInput;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValue;
import com.kingsrook.qqq.backend.core.scheduler.schedulable.SchedulableType;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
/*******************************************************************************
**
*******************************************************************************/
public class ScheduledJobTypePossibleValueSource implements QCustomPossibleValueProvider<String>
{
public static final String NAME = "scheduledJobType";
/*******************************************************************************
**
*******************************************************************************/
@Override
public QPossibleValue<String> getPossibleValue(Serializable idValue)
{
SchedulableType schedulableType = QContext.getQInstance().getSchedulableType(String.valueOf(idValue));
if(schedulableType != null)
{
return schedulableTypeToPossibleValue(schedulableType);
}
return null;
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public List<QPossibleValue<String>> search(SearchPossibleValueSourceInput input) throws QException
{
List<QPossibleValue<String>> rs = new ArrayList<>();
for(SchedulableType schedulableType : CollectionUtils.nonNullMap(QContext.getQInstance().getSchedulableTypes()).values())
{
rs.add(schedulableTypeToPossibleValue(schedulableType));
}
return rs;
}
/*******************************************************************************
**
*******************************************************************************/
private static QPossibleValue<String> schedulableTypeToPossibleValue(SchedulableType schedulableType)
{
return new QPossibleValue<>(schedulableType.getName(), schedulableType.getName());
}
}

View File

@ -0,0 +1,249 @@
/*
* 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.scheduledjobs;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
import com.kingsrook.qqq.backend.core.actions.dashboard.widgets.ChildRecordListRenderer;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy;
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn;
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinType;
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
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.possiblevalues.QPossibleValueSource;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSourceType;
import com.kingsrook.qqq.backend.core.model.metadata.tables.Association;
import com.kingsrook.qqq.backend.core.model.metadata.tables.ExposedJoin;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier;
import com.kingsrook.qqq.backend.core.model.scheduledjobs.customizers.ScheduledJobParameterTableCustomizer;
import com.kingsrook.qqq.backend.core.model.scheduledjobs.customizers.ScheduledJobTableCustomizer;
/*******************************************************************************
**
*******************************************************************************/
public class ScheduledJobsMetaDataProvider
{
private static final String JOB_PARAMETER_JOIN_NAME = QJoinMetaData.makeInferredJoinName(ScheduledJob.TABLE_NAME, ScheduledJobParameter.TABLE_NAME);
/*******************************************************************************
**
*******************************************************************************/
public void defineAll(QInstance instance, String backendName, Consumer<QTableMetaData> backendDetailEnricher) throws QException
{
defineStandardTables(instance, backendName, backendDetailEnricher);
instance.addPossibleValueSource(QPossibleValueSource.newForTable(ScheduledJob.TABLE_NAME));
instance.addPossibleValueSource(defineScheduledJobTypePossibleValueSource());
instance.addPossibleValueSource(defineSchedulersPossibleValueSource());
defineStandardJoins(instance);
defineStandardWidgets(instance);
}
/*******************************************************************************
**
*******************************************************************************/
public void defineStandardWidgets(QInstance instance)
{
QJoinMetaData join = instance.getJoin(JOB_PARAMETER_JOIN_NAME);
instance.addWidget(ChildRecordListRenderer.widgetMetaDataBuilder(join)
.withCanAddChildRecord(true)
.withManageAssociationName(ScheduledJobParameter.TABLE_NAME)
.withLabel("Parameters")
.getWidgetMetaData()
.withPermissionRules(new QPermissionRules().withLevel(PermissionLevel.NOT_PROTECTED)));
}
/*******************************************************************************
**
*******************************************************************************/
public void defineStandardJoins(QInstance instance)
{
instance.addJoin(new QJoinMetaData()
.withType(JoinType.ONE_TO_MANY)
.withLeftTable(ScheduledJob.TABLE_NAME)
.withRightTable(ScheduledJobParameter.TABLE_NAME)
.withJoinOn(new JoinOn("id", "scheduledJobId"))
.withOrderBy(new QFilterOrderBy("id"))
.withInferredName());
}
/*******************************************************************************
**
*******************************************************************************/
public void defineStandardTables(QInstance instance, String backendName, Consumer<QTableMetaData> backendDetailEnricher) throws QException
{
for(QTableMetaData tableMetaData : defineStandardTables(backendName, backendDetailEnricher))
{
instance.addTable(tableMetaData);
}
}
/*******************************************************************************
**
*******************************************************************************/
private List<QTableMetaData> defineStandardTables(String backendName, Consumer<QTableMetaData> backendDetailEnricher) throws QException
{
List<QTableMetaData> rs = new ArrayList<>();
rs.add(enrich(backendDetailEnricher, defineScheduledJobTable(backendName)));
rs.add(enrich(backendDetailEnricher, defineScheduledJobParameterTable(backendName)));
return (rs);
}
/*******************************************************************************
**
*******************************************************************************/
private QTableMetaData enrich(Consumer<QTableMetaData> backendDetailEnricher, QTableMetaData table)
{
if(backendDetailEnricher != null)
{
backendDetailEnricher.accept(table);
}
return (table);
}
/*******************************************************************************
**
*******************************************************************************/
private QTableMetaData defineStandardTable(String backendName, String name, Class<? extends QRecordEntity> fieldsFromEntity) throws QException
{
return new QTableMetaData()
.withName(name)
.withBackendName(backendName)
.withPrimaryKeyField("id")
.withFieldsFromEntity(fieldsFromEntity);
}
/*******************************************************************************
**
*******************************************************************************/
private QTableMetaData defineScheduledJobTable(String backendName) throws QException
{
QTableMetaData tableMetaData = defineStandardTable(backendName, ScheduledJob.TABLE_NAME, ScheduledJob.class)
.withRecordLabelFormat("%s")
.withRecordLabelFields("label")
.withSection(new QFieldSection("identity", new QIcon().withName("badge"), Tier.T1, List.of("id", "label", "description")))
.withSection(new QFieldSection("schedule", new QIcon().withName("alarm"), Tier.T2, List.of("cronExpression", "cronTimeZoneId", "repeatSeconds")))
.withSection(new QFieldSection("settings", new QIcon().withName("tune"), Tier.T2, List.of("type", "isActive", "schedulerName")))
.withSection(new QFieldSection("parameters", new QIcon().withName("list"), Tier.T2).withWidgetName(JOB_PARAMETER_JOIN_NAME))
.withSection(new QFieldSection("dates", new QIcon().withName("calendar_month"), Tier.T3, List.of("createDate", "modifyDate")));
QCodeReference customizerReference = new QCodeReference(ScheduledJobTableCustomizer.class);
tableMetaData.withCustomizer(TableCustomizers.PRE_INSERT_RECORD, customizerReference);
tableMetaData.withCustomizer(TableCustomizers.POST_INSERT_RECORD, customizerReference);
tableMetaData.withCustomizer(TableCustomizers.POST_UPDATE_RECORD, customizerReference);
tableMetaData.withCustomizer(TableCustomizers.PRE_UPDATE_RECORD, customizerReference);
tableMetaData.withCustomizer(TableCustomizers.POST_DELETE_RECORD, customizerReference);
tableMetaData.withAssociation(new Association()
.withName(ScheduledJobParameter.TABLE_NAME)
.withAssociatedTableName(ScheduledJobParameter.TABLE_NAME)
.withJoinName(JOB_PARAMETER_JOIN_NAME));
tableMetaData.withExposedJoin(new ExposedJoin()
.withJoinTable(ScheduledJobParameter.TABLE_NAME)
.withJoinPath(List.of(JOB_PARAMETER_JOIN_NAME))
.withLabel("Parameters"));
return (tableMetaData);
}
/*******************************************************************************
**
*******************************************************************************/
private QTableMetaData defineScheduledJobParameterTable(String backendName) throws QException
{
QTableMetaData tableMetaData = defineStandardTable(backendName, ScheduledJobParameter.TABLE_NAME, ScheduledJobParameter.class)
.withRecordLabelFormat("%s - %s")
.withRecordLabelFields("scheduledJobId", "key")
.withSection(new QFieldSection("identity", new QIcon().withName("badge"), Tier.T1, List.of("id", "scheduledJobId", "key", "value")))
.withSection(new QFieldSection("dates", new QIcon().withName("calendar_month"), Tier.T3, List.of("createDate", "modifyDate")));
QCodeReference customizerReference = new QCodeReference(ScheduledJobParameterTableCustomizer.class);
tableMetaData.withCustomizer(TableCustomizers.POST_INSERT_RECORD, customizerReference);
tableMetaData.withCustomizer(TableCustomizers.POST_UPDATE_RECORD, customizerReference);
tableMetaData.withCustomizer(TableCustomizers.POST_DELETE_RECORD, customizerReference);
tableMetaData.withExposedJoin(new ExposedJoin()
.withJoinTable(ScheduledJob.TABLE_NAME)
.withJoinPath(List.of(JOB_PARAMETER_JOIN_NAME))
.withLabel("Scheduled Job"));
return (tableMetaData);
}
/*******************************************************************************
**
*******************************************************************************/
private QPossibleValueSource defineScheduledJobTypePossibleValueSource()
{
return (new QPossibleValueSource()
.withName(ScheduledJobTypePossibleValueSource.NAME)
.withType(QPossibleValueSourceType.CUSTOM)
.withCustomCodeReference(new QCodeReference(ScheduledJobTypePossibleValueSource.class)));
}
/*******************************************************************************
**
*******************************************************************************/
private QPossibleValueSource defineSchedulersPossibleValueSource()
{
return (new QPossibleValueSource()
.withName(SchedulersPossibleValueSource.NAME)
.withType(QPossibleValueSourceType.CUSTOM)
.withCustomCodeReference(new QCodeReference(SchedulersPossibleValueSource.class)));
}
}

View File

@ -0,0 +1,90 @@
/*
* 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.scheduledjobs;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import com.kingsrook.qqq.backend.core.actions.values.QCustomPossibleValueProvider;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.values.SearchPossibleValueSourceInput;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValue;
import com.kingsrook.qqq.backend.core.model.metadata.scheduleing.QSchedulerMetaData;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
/*******************************************************************************
**
*******************************************************************************/
public class SchedulersPossibleValueSource implements QCustomPossibleValueProvider<String>
{
public static final String NAME = "schedulers";
/*******************************************************************************
**
*******************************************************************************/
@Override
public QPossibleValue<String> getPossibleValue(Serializable idValue)
{
QSchedulerMetaData scheduler = QContext.getQInstance().getScheduler(String.valueOf(idValue));
if(scheduler != null)
{
return schedulerToPossibleValue(scheduler);
}
return null;
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public List<QPossibleValue<String>> search(SearchPossibleValueSourceInput input) throws QException
{
List<QPossibleValue<String>> rs = new ArrayList<>();
for(QSchedulerMetaData scheduler : CollectionUtils.nonNullMap(QContext.getQInstance().getSchedulers()).values())
{
if(scheduler.mayUseInScheduledJobsTable())
{
rs.add(schedulerToPossibleValue(scheduler));
}
}
return rs;
}
/*******************************************************************************
**
*******************************************************************************/
private static QPossibleValue<String> schedulerToPossibleValue(QSchedulerMetaData scheduler)
{
return new QPossibleValue<>(scheduler.getName(), scheduler.getName());
}
}

View File

@ -0,0 +1,215 @@
/*
* 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.scheduledjobs.customizers;
import java.time.Instant;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizerInterface;
import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
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.actions.tables.update.UpdateOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.scheduledjobs.ScheduledJob;
import com.kingsrook.qqq.backend.core.model.scheduledjobs.ScheduledJobParameter;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.ListingHash;
/*******************************************************************************
**
*******************************************************************************/
public class ScheduledJobParameterTableCustomizer implements TableCustomizerInterface
{
/*******************************************************************************
**
*******************************************************************************/
@Override
public List<QRecord> postInsert(InsertInput insertInput, List<QRecord> records) throws QException
{
///////////////////////////////////////////////////////////////////////////////////////
// if we're in this insert as a result of an insert (or update) on a different table //
// (e.g., under a manageAssociations call), then return with noop - assume that the //
// parent table's customizer will do what needed to be done. //
///////////////////////////////////////////////////////////////////////////////////////
if(!isThisAnActionDirectlyOnThisTable())
{
return (records);
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// else - this was an action directly on this table - so bump all of the parent records, to get them rescheduled //
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
bumpParentRecords(records, Optional.empty());
return (records);
}
/*******************************************************************************
**
*******************************************************************************/
private void bumpParentRecords(List<QRecord> records, Optional<List<QRecord>> oldRecordList) throws QException
{
try
{
///////////////////////////////////////////////////////////////////////////////////////////
// (listing) hash up the records by scheduledJobId - we'll use this to have a set of the //
// job ids, and in case we need to add warnings to them later //
///////////////////////////////////////////////////////////////////////////////////////////
ListingHash<Integer, QRecord> recordsByJobId = new ListingHash<>();
for(QRecord record : records)
{
recordsByJobId.add(record.getValueInteger("scheduledJobId"), record);
}
Set<Integer> scheduledJobIds = new HashSet<>(recordsByJobId.keySet());
////////////////////////////////////////////////////////////////////////////////
// if we have an old record list (e.g., is an edit), add any job ids that are //
// in those too, e.g., in case moving a param from one job to another... //
// note, we won't line these up for doing a proper warning on these... //
////////////////////////////////////////////////////////////////////////////////
if(oldRecordList.isPresent())
{
for(QRecord oldRecord : oldRecordList.get())
{
scheduledJobIds.add(oldRecord.getValueInteger("scheduledJobId"));
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////
// update the modify date on the scheduled jobs - to get their post-actions to run, to reschedule //
////////////////////////////////////////////////////////////////////////////////////////////////////
UpdateInput updateInput = new UpdateInput();
updateInput.setTableName(ScheduledJob.TABLE_NAME);
updateInput.setRecords(scheduledJobIds.stream()
.map(id -> new QRecord().withValue("id", id).withValue("modifyDate", Instant.now()))
.toList());
UpdateOutput updateOutput = new UpdateAction().execute(updateInput);
////////////////////////////////////////////////////////////////////////////////////////
// look for warnings on those jobs - and propagate them to the params we just stored. //
////////////////////////////////////////////////////////////////////////////////////////
for(QRecord updatedScheduledJob : updateOutput.getRecords())
{
if(CollectionUtils.nullSafeHasContents(updatedScheduledJob.getWarnings()))
{
for(QRecord paramToWarn : CollectionUtils.nonNullList(recordsByJobId.get(updatedScheduledJob.getValueInteger("id"))))
{
paramToWarn.setWarnings(updatedScheduledJob.getWarnings());
}
}
}
}
catch(Exception e)
{
LOG.warn("Error in scheduledJobParameter post-crud", e);
}
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public List<QRecord> postUpdate(UpdateInput updateInput, List<QRecord> records, Optional<List<QRecord>> oldRecordList) throws QException
{
/////////////////////////////////////////////////////////////////////////////
// if we're in this update as a result of an update on a different table //
// (e.g., under a manageAssociations call), then return with noop - assume //
// that the parent table's customizer will do what needed to be done. //
/////////////////////////////////////////////////////////////////////////////
if(!isThisAnActionDirectlyOnThisTable())
{
return (records);
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// else - this was an action directly on this table - so bump all of the parent records, to get them rescheduled //
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
bumpParentRecords(records, oldRecordList);
return (records);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public List<QRecord> postDelete(DeleteInput deleteInput, List<QRecord> records) throws QException
{
/////////////////////////////////////////////////////////////////////////////
// if we're in this update as a result of an update on a different table //
// (e.g., under a manageAssociations call), then return with noop - assume //
// that the parent table's customizer will do what needed to be done. //
/////////////////////////////////////////////////////////////////////////////
if(!isThisAnActionDirectlyOnThisTable())
{
return (records);
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// else - this was an action directly on this table - so bump all of the parent records, to get them rescheduled //
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
bumpParentRecords(records, Optional.empty());
return (records);
}
/*******************************************************************************
**
*******************************************************************************/
private static boolean isThisAnActionDirectlyOnThisTable()
{
Optional<AbstractActionInput> firstActionInStack = QContext.getFirstActionInStack();
if(firstActionInStack.isPresent())
{
if(firstActionInStack.get() instanceof AbstractTableActionInput tableActionInput)
{
if(!ScheduledJobParameter.TABLE_NAME.equals(tableActionInput.getTableName()))
{
return (false);
}
}
}
return (true);
}
}

View File

@ -0,0 +1,315 @@
/*
* 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.scheduledjobs.customizers;
import java.text.ParseException;
import java.util.Collections;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizerInterface;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.exceptions.QException;
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.query.QCriteriaOperator;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.scheduledjobs.ScheduledJob;
import com.kingsrook.qqq.backend.core.model.statusmessages.BadInputStatusMessage;
import com.kingsrook.qqq.backend.core.model.statusmessages.QWarningMessage;
import com.kingsrook.qqq.backend.core.scheduler.QScheduleManager;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import org.quartz.CronScheduleBuilder;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
**
*******************************************************************************/
public class ScheduledJobTableCustomizer implements TableCustomizerInterface
{
/*******************************************************************************
**
*******************************************************************************/
@Override
public List<QRecord> preInsert(InsertInput insertInput, List<QRecord> records, boolean isPreview) throws QException
{
validateConditionalFields(records, Collections.emptyMap());
return (records);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public List<QRecord> postInsert(InsertInput insertInput, List<QRecord> records) throws QException
{
scheduleJobsForRecordList(records);
return (records);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public List<QRecord> preUpdate(UpdateInput updateInput, List<QRecord> records, boolean isPreview, Optional<List<QRecord>> oldRecordList) throws QException
{
Map<Integer, QRecord> freshOldRecordsWithAssociationsMap = CollectionUtils.recordsToMap(freshlyQueryForRecordsWithAssociations(oldRecordList.get()), "id", Integer.class);
validateConditionalFields(records, freshOldRecordsWithAssociationsMap);
if(isPreview || oldRecordList.isEmpty())
{
return (records);
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// refresh the old-records w/ versions that have associations - so we can use those in the post-update to property unschedule things //
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
ListIterator<QRecord> iterator = oldRecordList.get().listIterator();
while(iterator.hasNext())
{
QRecord record = iterator.next();
QRecord freshRecord = freshOldRecordsWithAssociationsMap.get(record.getValue("id"));
if(freshRecord != null)
{
iterator.set(freshRecord);
}
}
return (records);
}
/*******************************************************************************
**
*******************************************************************************/
private static void validateConditionalFields(List<QRecord> records, Map<Integer, QRecord> freshOldRecordsWithAssociationsMap)
{
QRecord blankRecord = new QRecord();
for(QRecord record : records)
{
QRecord oldRecord = Objects.requireNonNullElse(freshOldRecordsWithAssociationsMap.get(record.getValueInteger("id")), blankRecord);
String cronExpression = record.getValues().containsKey("cronExpression") ? record.getValueString("cronExpression") : oldRecord.getValueString("cronExpression");
String cronTimeZoneId = record.getValues().containsKey("cronTimeZoneId") ? record.getValueString("cronTimeZoneId") : oldRecord.getValueString("cronTimeZoneId");
String repeatSeconds = record.getValues().containsKey("repeatSeconds") ? record.getValueString("repeatSeconds") : oldRecord.getValueString("repeatSeconds");
if(StringUtils.hasContent(cronExpression))
{
if(StringUtils.hasContent(repeatSeconds))
{
record.addError(new BadInputStatusMessage("Cron Expression and Repeat Seconds may not both be given."));
}
try
{
CronScheduleBuilder.cronScheduleNonvalidatedExpression(cronExpression);
}
catch(ParseException e)
{
record.addError(new BadInputStatusMessage("Cron Expression [" + cronExpression + "] is not valid: " + e.getMessage()));
}
if(!StringUtils.hasContent(cronTimeZoneId))
{
record.addError(new BadInputStatusMessage("If a Cron Expression is given, then a Cron Time Zone is required."));
}
}
else
{
if(!StringUtils.hasContent(repeatSeconds))
{
record.addError(new BadInputStatusMessage("Either Cron Expression or Repeat Seconds must be given."));
}
}
}
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public List<QRecord> postUpdate(UpdateInput updateInput, List<QRecord> records, Optional<List<QRecord>> oldRecordList) throws QException
{
if(oldRecordList.isPresent())
{
Set<Integer> idsWithErrors = getRecordIdsWithErrors(records);
unscheduleJobsForRecordList(oldRecordList.get(), idsWithErrors);
}
scheduleJobsForRecordList(records);
return (records);
}
/*******************************************************************************
**
*******************************************************************************/
private static Set<Integer> getRecordIdsWithErrors(List<QRecord> records)
{
return records.stream()
.filter(r -> !recordHasErrors().test(r))
.map(r -> r.getValueInteger("id"))
.collect(Collectors.toSet());
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public List<QRecord> postDelete(DeleteInput deleteInput, List<QRecord> records) throws QException
{
Set<Integer> idsWithErrors = getRecordIdsWithErrors(records);
unscheduleJobsForRecordList(records, idsWithErrors);
return (records);
}
/*******************************************************************************
**
*******************************************************************************/
private void scheduleJobsForRecordList(List<QRecord> records)
{
List<QRecord> recordsWithoutErrors = records.stream().filter(recordHasErrors()).toList();
if(CollectionUtils.nullSafeIsEmpty(recordsWithoutErrors))
{
return;
}
try
{
Map<Integer, QRecord> originalRecordMap = recordsWithoutErrors.stream().collect(Collectors.toMap(r -> r.getValueInteger("id"), r -> r));
List<QRecord> freshRecordListWithAssociations = freshlyQueryForRecordsWithAssociations(recordsWithoutErrors);
QScheduleManager scheduleManager = QScheduleManager.getInstance();
for(QRecord record : freshRecordListWithAssociations)
{
try
{
scheduleManager.setupScheduledJob(new ScheduledJob(record));
}
catch(Exception e)
{
LOG.warn("Caught exception while scheduling a job in post-action", e, logPair("id", record.getValue("id")));
if(originalRecordMap.containsKey(record.getValueInteger("id")))
{
originalRecordMap.get(record.getValueInteger("id")).addWarning(new QWarningMessage("Error scheduling job: " + e.getMessage()));
}
}
}
}
catch(Exception e)
{
LOG.warn("Error scheduling jobs in post-action", e);
}
}
/*******************************************************************************
**
*******************************************************************************/
private static Predicate<QRecord> recordHasErrors()
{
return r -> CollectionUtils.nullSafeIsEmpty(r.getErrors());
}
/*******************************************************************************
**
*******************************************************************************/
private static List<QRecord> freshlyQueryForRecordsWithAssociations(List<QRecord> records) throws QException
{
List<Integer> idList = records.stream().map(r -> r.getValueInteger("id")).toList();
return new QueryAction().execute(new QueryInput(ScheduledJob.TABLE_NAME)
.withIncludeAssociations(true)
.withFilter(new QQueryFilter(new QFilterCriteria("id", QCriteriaOperator.IN, idList))))
.getRecords();
}
/*******************************************************************************
**
*******************************************************************************/
private void unscheduleJobsForRecordList(List<QRecord> oldRecords, Set<Integer> exceptIdsWithErrors)
{
try
{
QScheduleManager scheduleManager = QScheduleManager.getInstance();
///////////////////////////////////////////////////////////////////////////////////////////////////////////
// for un-schedule - use the old records as they are - don't re-query them (they may not exist anymore!) //
///////////////////////////////////////////////////////////////////////////////////////////////////////////
for(QRecord record : oldRecords)
{
try
{
ScheduledJob scheduledJob = new ScheduledJob(record);
if(exceptIdsWithErrors.contains(scheduledJob.getId()))
{
LOG.info("Will not unschedule the job for a record that had an error", logPair("id", scheduledJob.getId()));
continue;
}
scheduleManager.unscheduleScheduledJob(scheduledJob);
}
catch(Exception e)
{
LOG.warn("Caught exception while un-scheduling a job in post-action", e, logPair("id", record.getValue("id")));
}
}
}
catch(Exception e)
{
LOG.warn("Error scheduling jobs in post-action", e);
}
}
}

View File

@ -27,6 +27,7 @@ import java.util.Map;
import com.kingsrook.qqq.backend.core.exceptions.QModuleDispatchException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
@ -89,7 +90,7 @@ public class QBackendModuleDispatcher
}
catch(Exception e)
{
LOG.debug("Backend module [{}] could not be loaded: {}", moduleClassName, e.getMessage());
LOG.debug("Backend module could not be loaded", e, logPair("moduleClassName", moduleClassName));
}
}

View File

@ -372,7 +372,7 @@ public class MemoryRecordStore
/////////////////////////////////////////////////
// set the next serial in the record if needed //
/////////////////////////////////////////////////
if(recordToInsert.getValue(primaryKeyField.getName()) == null && primaryKeyField.getType().equals(QFieldType.INTEGER))
if(recordToInsert.getValue(primaryKeyField.getName()) == null && (primaryKeyField.getType().equals(QFieldType.INTEGER) || primaryKeyField.getType().equals(QFieldType.LONG)))
{
recordToInsert.setValue(primaryKeyField.getName(), nextSerial++);
}
@ -384,6 +384,13 @@ public class MemoryRecordStore
{
nextSerial = recordToInsert.getValueInteger(primaryKeyField.getName()) + 1;
}
else if(primaryKeyField.getType().equals(QFieldType.LONG) && recordToInsert.getValueLong(primaryKeyField.getName()) > nextSerial)
{
//////////////////////////////////////
// todo - mmm, could overflow here? //
//////////////////////////////////////
nextSerial = recordToInsert.getValueInteger(primaryKeyField.getName()) + 1;
}
tableData.put(recordToInsert.getValue(primaryKeyField.getName()), recordToInsert);
if(returnInsertedRecords)
@ -773,7 +780,7 @@ public class MemoryRecordStore
{
// todo - joins probably?
QFieldMetaData field = table.getField(fieldName);
if(field.getType().equals(QFieldType.INTEGER) && (operator.equals(AggregateOperator.AVG)))
if((field.getType().equals(QFieldType.INTEGER) || field.getType().equals(QFieldType.LONG)) && (operator.equals(AggregateOperator.AVG)))
{
fieldType = QFieldType.DECIMAL;
}
@ -809,6 +816,10 @@ public class MemoryRecordStore
.filter(r -> r.getValue(fieldName) != null)
.mapToInt(r -> r.getValueInteger(fieldName))
.sum();
case LONG -> records.stream()
.filter(r -> r.getValue(fieldName) != null)
.mapToLong(r -> r.getValueLong(fieldName))
.sum();
case DECIMAL -> records.stream()
.filter(r -> r.getValue(fieldName) != null)
.map(r -> r.getValueBigDecimal(fieldName))
@ -823,6 +834,11 @@ public class MemoryRecordStore
.mapToInt(r -> r.getValueInteger(fieldName))
.min()
.stream().boxed().findFirst().orElse(null);
case LONG -> records.stream()
.filter(r -> r.getValue(fieldName) != null)
.mapToLong(r -> r.getValueLong(fieldName))
.min()
.stream().boxed().findFirst().orElse(null);
case DECIMAL, STRING, DATE, DATE_TIME ->
{
Optional<Serializable> serializable = records.stream()
@ -839,7 +855,12 @@ public class MemoryRecordStore
{
case INTEGER -> records.stream()
.filter(r -> r.getValue(fieldName) != null)
.mapToInt(r -> r.getValueInteger(fieldName))
.mapToLong(r -> r.getValueInteger(fieldName))
.max()
.stream().boxed().findFirst().orElse(null);
case LONG -> records.stream()
.filter(r -> r.getValue(fieldName) != null)
.mapToLong(r -> r.getValueLong(fieldName))
.max()
.stream().boxed().findFirst().orElse(null);
case DECIMAL, STRING, DATE, DATE_TIME ->
@ -861,6 +882,11 @@ public class MemoryRecordStore
.mapToInt(r -> r.getValueInteger(fieldName))
.average()
.stream().boxed().findFirst().orElse(null);
case LONG -> records.stream()
.filter(r -> r.getValue(fieldName) != null)
.mapToLong(r -> r.getValueLong(fieldName))
.average()
.stream().boxed().findFirst().orElse(null);
case DECIMAL -> records.stream()
.filter(r -> r.getValue(fieldName) != null)
.mapToDouble(r -> r.getValueBigDecimal(fieldName).doubleValue())

View File

@ -103,6 +103,7 @@ public class MockQueryAction implements QueryInterface
{
case STRING -> UUID.randomUUID().toString();
case INTEGER -> 42;
case LONG -> 42L;
case DECIMAL -> new BigDecimal("3.14159");
case DATE -> LocalDate.of(1970, Month.JANUARY, 1);
case DATE_TIME -> LocalDateTime.of(1970, Month.JANUARY, 1, 0, 0);

View File

@ -55,6 +55,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.DisplayFormat;
import com.kingsrook.qqq.backend.core.model.metadata.fields.DynamicDefaultValueBehavior;
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.layout.QIcon;
import com.kingsrook.qqq.backend.core.model.metadata.processes.NoCodeWidgetFrontendComponentMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QComponentType;
@ -129,6 +130,7 @@ public class HealBadRecordAutomationStatusesProcessStep implements BackendStep,
QProcessMetaData processMetaData = new QProcessMetaData()
.withName(NAME)
.withIcon(new QIcon().withName("healing"))
.withStepList(List.of(
new QFrontendStepMetaData()
.withName("input")

View File

@ -36,6 +36,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.automation.QAutomationProvi
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QComponentType;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendComponentMetaData;
@ -71,6 +72,7 @@ public class RunTableAutomationsProcessStep implements BackendStep, MetaDataProd
{
QProcessMetaData processMetaData = new QProcessMetaData()
.withName(NAME)
.withIcon(new QIcon().withName("directions_run"))
.withStepList(List.of(
new QFrontendStepMetaData()
.withName("input")

View File

@ -34,6 +34,7 @@ import java.util.Set;
import com.kingsrook.qqq.backend.core.actions.customizers.AbstractPreInsertCustomizer;
import com.kingsrook.qqq.backend.core.actions.customizers.AbstractPreInsertCustomizer.WhenToRun;
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.tables.InsertAction;
import com.kingsrook.qqq.backend.core.actions.tables.helpers.UniqueKeyHelper;
@ -137,15 +138,13 @@ public class BulkInsertTransformStep extends AbstractTransformStep
// we do this, in case it needs to, for example, adjust values that //
// are part of a unique key //
//////////////////////////////////////////////////////////////////////
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(true);
AbstractPreInsertCustomizer.WhenToRun whenToRun = preInsertCustomizer.get().getWhenToRun();
AbstractPreInsertCustomizer.WhenToRun whenToRun = preInsertCustomizer.get().whenToRunPreInsert(insertInput, true);
if(WhenToRun.BEFORE_ALL_VALIDATIONS.equals(whenToRun) || WhenToRun.BEFORE_UNIQUE_KEY_CHECKS.equals(whenToRun))
{
List<QRecord> recordsAfterCustomizer = preInsertCustomizer.get().apply(runBackendStepInput.getRecords());
List<QRecord> recordsAfterCustomizer = preInsertCustomizer.get().preInsert(insertInput, runBackendStepInput.getRecords(), true);
runBackendStepInput.setRecords(recordsAfterCustomizer);
///////////////////////////////////////////////////////////////////////////////////////

View File

@ -34,6 +34,7 @@ import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import com.kingsrook.qqq.backend.core.actions.dashboard.widgets.DateTimeGroupBy;
import com.kingsrook.qqq.backend.core.actions.permissions.PermissionsHelper;
import com.kingsrook.qqq.backend.core.actions.permissions.TablePermissionSubType;
@ -252,7 +253,7 @@ public class ColumnStatsStep implements BackendStep
QPossibleValueTranslator qPossibleValueTranslator = new QPossibleValueTranslator();
qPossibleValueTranslator.translatePossibleValuesInRecords(table, valueCounts, queryJoin == null ? null : List.of(queryJoin), null);
QValueFormatter.setDisplayValuesInRecords(Map.of(fieldName, field, "count", countField), valueCounts);
QValueFormatter.setDisplayValuesInRecords(table, Map.of(fieldName, field, "count", countField), valueCounts);
runBackendStepOutput.addValue("valueCounts", valueCounts);
@ -442,13 +443,13 @@ public class ColumnStatsStep implements BackendStep
}
QFieldMetaData percentField = new QFieldMetaData("percent", QFieldType.DECIMAL).withDisplayFormat(DisplayFormat.PERCENT_POINT2).withLabel("Percent");
QValueFormatter.setDisplayValuesInRecords(Map.of(fieldName, field, "percent", percentField), valueCounts);
QValueFormatter.setDisplayValuesInRecords(table, Map.of(fieldName, field, "percent", percentField), valueCounts);
}
QInstanceEnricher qInstanceEnricher = new QInstanceEnricher(null);
fields.forEach(qInstanceEnricher::enrichField);
QValueFormatter.setDisplayValuesInRecord(fields, statsRecord);
QValueFormatter.setDisplayValuesInRecord(table, fields.stream().collect(Collectors.toMap(f -> f.getName(), f -> f)), statsRecord);
runBackendStepOutput.addValue("statsFields", fields);
runBackendStepOutput.addValue("statsRecord", statsRecord);

View File

@ -104,12 +104,12 @@ public class BaseStreamedETLStep
*******************************************************************************/
protected void moveReviewStepAfterValidateStep(RunBackendStepOutput runBackendStepOutput)
{
LOG.info("Skipping to validation step");
LOG.debug("Skipping to validation step");
ArrayList<String> stepList = new ArrayList<>(runBackendStepOutput.getProcessState().getStepList());
LOG.debug("Step list pre: " + stepList);
LOG.trace("Step list pre: " + stepList);
stepList.removeIf(s -> s.equals(StreamedETLWithFrontendProcess.STEP_NAME_REVIEW));
stepList.add(stepList.indexOf(StreamedETLWithFrontendProcess.STEP_NAME_VALIDATE) + 1, StreamedETLWithFrontendProcess.STEP_NAME_REVIEW);
runBackendStepOutput.getProcessState().setStepList(stepList);
LOG.debug("Step list post: " + stepList);
LOG.trace("Step list post: " + stepList);
}
}

View File

@ -136,7 +136,7 @@ public class StreamedETLExecuteStep extends BaseStreamedETLStep implements Backe
asyncRecordPipeLoop.setMinRecordsToConsume(overrideRecordPipeCapacity);
}
int recordCount = asyncRecordPipeLoop.run("StreamedETL>Execute>ExtractStep", null, recordPipe, (status) ->
int recordCount = asyncRecordPipeLoop.run("StreamedETLExecute>Extract>" + runBackendStepInput.getProcessName(), null, recordPipe, (status) ->
{
extractStep.run(runBackendStepInput, runBackendStepOutput);
return (runBackendStepOutput);

View File

@ -125,7 +125,7 @@ public class StreamedETLPreviewStep extends BaseStreamedETLStep implements Backe
// }
List<QRecord> previewRecordList = new ArrayList<>();
new AsyncRecordPipeLoop().run("StreamedETL>Preview>ExtractStep", PROCESS_OUTPUT_RECORD_LIST_LIMIT, recordPipe, (status) ->
new AsyncRecordPipeLoop().run("StreamedETLPreview>Extract>" + runBackendStepInput.getProcessName(), PROCESS_OUTPUT_RECORD_LIST_LIMIT, recordPipe, (status) ->
{
runBackendStepInput.setAsyncJobCallback(status);
extractStep.run(runBackendStepInput, runBackendStepOutput);

View File

@ -119,7 +119,7 @@ public class StreamedETLValidateStep extends BaseStreamedETLStep implements Back
transformStep.preRun(runBackendStepInput, runBackendStepOutput);
List<QRecord> previewRecordList = new ArrayList<>();
int recordCount = new AsyncRecordPipeLoop().run("StreamedETL>Preview>ValidateStep", null, recordPipe, (status) ->
int recordCount = new AsyncRecordPipeLoop().run("StreamedETLValidate>Extract>" + runBackendStepInput.getProcessName(), null, recordPipe, (status) ->
{
extractStep.run(runBackendStepInput, runBackendStepOutput);
return (runBackendStepOutput);

View File

@ -41,6 +41,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionInputMet
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionOutputMetaData;
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.VariantRunStrategy;
import com.kingsrook.qqq.backend.core.model.metadata.scheduleing.QScheduleMetaData;
import com.kingsrook.qqq.backend.core.processes.implementations.basepull.BasepullConfiguration;
@ -490,5 +491,28 @@ public class StreamedETLWithFrontendProcess
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public Builder withVariantRunStrategy(VariantRunStrategy variantRunStrategy)
{
processMetaData.setVariantRunStrategy(variantRunStrategy);
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public Builder withVariantBackend(String variantBackend)
{
processMetaData.setVariantBackend(variantBackend);
return (this);
}
}
}

View File

@ -26,6 +26,7 @@ import java.time.Instant;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.ExtractViaQueryStep;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import com.kingsrook.qqq.backend.core.utils.collections.ListBuilder;
@ -57,4 +58,15 @@ public class GarbageCollectorExtractStep extends ExtractViaQueryStep
return super.getQueryFilter(runBackendStepInput);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
protected void customizeInputPreQuery(QueryInput queryInput)
{
queryInput.withQueryHint(QueryInput.QueryHint.POTENTIALLY_LARGE_NUMBER_OF_RESULTS);
}
}

View File

@ -56,7 +56,7 @@ public class MockBackendStep implements BackendStep
runBackendStepInput.getRecords().forEach(r ->
{
LOG.info("We are mocking {}: {}", r.getValueString("firstName"), r.getValue(FIELD_MOCK_VALUE));
LOG.info("We are mocking " + r.getValueString("firstName") + ": " + r.getValue(FIELD_MOCK_VALUE));
r.setValue(FIELD_MOCK_VALUE, "Ha ha!");
r.setValue("greetingMessage", runBackendStepInput.getValueString(FIELD_GREETING_PREFIX) + " " + r.getValueString("firstName") + " " + runBackendStepInput.getValueString(FIELD_GREETING_SUFFIX));
});

View File

@ -31,6 +31,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.VariantRunStrategy;
import com.kingsrook.qqq.backend.core.model.metadata.scheduleing.QScheduleMetaData;
import com.kingsrook.qqq.backend.core.processes.implementations.basepull.BasepullConfiguration;
import com.kingsrook.qqq.backend.core.processes.implementations.basepull.ExtractViaBasepullQueryStep;
@ -248,5 +249,28 @@ public class TableSyncProcess
super.withExtractStepClass(extractStepClass);
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public Builder withVariantRunStrategy(VariantRunStrategy variantRunStrategy)
{
processMetaData.setVariantRunStrategy(variantRunStrategy);
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public Builder withVariantBackend(String variantBackend)
{
processMetaData.setVariantBackend(variantBackend);
return (this);
}
}
}

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