mirror of
https://github.com/Kingsrook/qqq.git
synced 2025-07-20 14:10:44 +00:00
Compare commits
98 Commits
snapshot-f
...
snapshot-f
Author | SHA1 | Date | |
---|---|---|---|
dc9a2f8698 | |||
0641ac41d6 | |||
cf0c905dc6 | |||
3440d45060 | |||
c32d9110ce | |||
b55631a767 | |||
9ab5a8b305 | |||
ff9add437d | |||
a6bf474448 | |||
f409d0bd19 | |||
b608bda83a | |||
8f503945cd | |||
512d73af34 | |||
e8b4368acc | |||
1e7e7cdd64 | |||
a784e59c50 | |||
4e6460c469 | |||
98fc34fb27 | |||
df6bed2453 | |||
0759085431 | |||
9257519462 | |||
e0134450f8 | |||
fc915a6d65 | |||
d7dec3e560 | |||
40e8a85977 | |||
30ee8ce3bf | |||
0d6538593b | |||
7e6a3c528f | |||
02d068dad7 | |||
c9f921c148 | |||
2545d03f20 | |||
92b8211f20 | |||
8afbbfb4da | |||
24b1daa110 | |||
17899c3fdc | |||
605578d661 | |||
5963a706b0 | |||
c8c7051628 | |||
7015322bf3 | |||
d6edbfa06b | |||
a10992226a | |||
04103281af | |||
2088c5dab3 | |||
59a4ad7de6 | |||
7b457b4936 | |||
d1e4091eb4 | |||
753c224196 | |||
0130e34112 | |||
5ed21d1fed | |||
c5e381abdb | |||
1bffd4d46e | |||
c093c680c0 | |||
aa69f0e7d7 | |||
2fec4891d3 | |||
60ffac4646 | |||
2a97598309 | |||
1a1ebcbe02 | |||
5f72978528 | |||
3265d6d842 | |||
246984892a | |||
03b93658d5 | |||
b093ff5ece | |||
7181643abb | |||
178b902665 | |||
d09b12ca5b | |||
2c4fc6a0d4 | |||
11d33c6d06 | |||
3684101259 | |||
c054bf5ddd | |||
7155180a76 | |||
dabaafa482 | |||
a4cdbc429d | |||
03f1fc1436 | |||
d551ad71a6 | |||
891c567a8d | |||
3e604f4b6f | |||
5564e94ad7 | |||
58c15e6eaa | |||
f448cff5dd | |||
2e1bf399f9 | |||
621997efd9 | |||
87ef20aff4 | |||
12eb1804ad | |||
51945aa844 | |||
1ee4f67286 | |||
3c2a34291a | |||
5f0fe9ce27 | |||
0dd97d9dc1 | |||
b1e68017cc | |||
a37a0b489d | |||
940c5ca8de | |||
84093dfde5 | |||
9c7d94f764 | |||
346443996b | |||
2fc513891f | |||
7426aa36a5 | |||
9d8e8a74e2 | |||
455ab69104 |
@ -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/
|
||||
|
@ -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]
|
||||
|
@ -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"))
|
||||
----
|
||||
|
141
docs/misc/ScheduledJobs.adoc
Normal file
141
docs/misc/ScheduledJobs.adoc
Normal 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))
|
||||
----
|
||||
|
@ -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>
|
||||
|
@ -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());
|
||||
|
@ -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
|
||||
{
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
|
@ -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));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
|
@ -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 //
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
|
@ -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 //
|
||||
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
@ -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));
|
||||
|
@ -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);
|
||||
|
@ -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() + "'");
|
||||
|
@ -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 //
|
||||
|
@ -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")
|
||||
|
@ -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.");
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////
|
||||
|
@ -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())
|
||||
|
@ -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())));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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())
|
||||
|
@ -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()))
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
@ -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));
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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"
|
||||
*******************************************************************************/
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
|
@ -48,6 +48,17 @@ public class CollectedLogMessage
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return "CollectedLogMessage{level=" + level + ", message='" + message + '\'' + ", exception=" + exception + '}';
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for message
|
||||
*******************************************************************************/
|
||||
|
@ -60,6 +60,11 @@ public interface QueryOrGetInputInterface
|
||||
QBackendTransaction getTransaction();
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
String getTableName();
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for transaction
|
||||
*******************************************************************************/
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
@ -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"),
|
||||
|
@ -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)));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -29,6 +29,11 @@ package com.kingsrook.qqq.backend.core.model.metadata;
|
||||
public interface TopLevelMetaDataInterface
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
String getName();
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
|
@ -55,6 +55,17 @@ public class QBrandingMetaData implements TopLevelMetaDataInterface
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public String getName()
|
||||
{
|
||||
return "Branding";
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for companyName
|
||||
**
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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)
|
||||
{
|
||||
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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>
|
||||
{
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
@ -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()))
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -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
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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)));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -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")
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -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
|
||||
}
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
@ -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)));
|
||||
}
|
||||
|
||||
}
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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())
|
||||
|
@ -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);
|
||||
|
@ -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")
|
||||
|
@ -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")
|
||||
|
@ -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);
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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));
|
||||
});
|
||||
|
@ -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
Reference in New Issue
Block a user