Compare commits

..

2 Commits

334 changed files with 2968 additions and 41321 deletions

View File

@ -5,8 +5,8 @@ if [ -z "$CIRCLE_BRANCH" ] && [ -z "$CIRCLE_TAG" ]; then
exit 1; exit 1;
fi fi
if [ "$CIRCLE_BRANCH" == "dev" ] || [ "$CIRCLE_BRANCH" == "staging" ] || [ "$CIRCLE_BRANCH" == "main" ] || [ \! -z $(echo "$CIRCLE_TAG" | grep "^version-") ]; then if [ "$CIRCLE_BRANCH" == "dev" ] || [ "$CIRCLE_BRANCH" == "staging" ] || [ "$CIRCLE_BRANCH" == "main" ]; then
echo "On a primary branch or tag [${CIRCLE_BRANCH}${CIRCLE_TAG}] - will not edit the pom version."; echo "On a primary branch [$CIRCLE_BRANCH] - will not edit the pom version.";
exit 0; exit 0;
fi fi

View File

@ -102,14 +102,14 @@ jobs:
mvn_test: mvn_test:
executor: localstack/default executor: localstack/default
steps: steps:
## - localstack/startup - localstack/startup
- install_java17 - install_java17
- mvn_verify - mvn_verify
mvn_deploy: mvn_deploy:
executor: localstack/default executor: localstack/default
steps: steps:
## - localstack/startup - localstack/startup
- install_java17 - install_java17
- mvn_verify - mvn_verify
- mvn_jar_deploy - mvn_jar_deploy

View File

@ -10,7 +10,7 @@ The bundle contains all of the sub-jars. It is named:
```qqq-${version}.jar``` ```qqq-${version}.jar```
You can also use fine-grained jars: You can also use fine grained jars:
- `qqq-backend-core`: The core module. Useful if you're developing other modules. - `qqq-backend-core`: The core module. Useful if you're developing other modules.
- `qqq-backend-module-rdbms`: Backend module for working with Relational Databases. - `qqq-backend-module-rdbms`: Backend module for working with Relational Databases.
- `qqq-backend-module-filesystem`: Backend module for working with Filesystems (including AWS S3). - `qqq-backend-module-filesystem`: Backend module for working with Filesystems (including AWS S3).
@ -35,3 +35,4 @@ GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License 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/>. along with this program. If not, see <https://www.gnu.org/licenses/>.

View File

@ -1,8 +0,0 @@
= Introduction
QQQ is ...
- Framework
- Declarative
- Easy thing easy; Hard thing possible
- Customizable

File diff suppressed because it is too large Load Diff

View File

@ -1,179 +0,0 @@
== QueryAction
include::../variables.adoc[]
The `*QueryAction*` is the basic action that is used to get records from a {link-table}.
In SQL/RDBMS terms, it is analogous to a `SELECT` statement, where 0 or more records may be found and returned.
=== Examples
==== Basic Form
[source,java]
----
QueryInput input = new QueryInput(qInstance);
input.setSession(session);
input.setTableName("orders");
input.setFilter(new QQueryFilter()
.withCriteria(new QFilterCriteria("total", GREATER_THAN, new BigDecimal("3.50")))
.withOrderBy(new QFilterOrderBy("orderDate", false))
);
QueryOutput output = new QueryAction.execute(input);
List<QRecord> records = output.getRecords();
----
=== QueryInput
* `table` - *String, Required* - Name of the table being queried against.
* `filter` - *QQueryFilter object* - Specification for what records should be returned, based on *QFilterCriteria* objects, and how they should be sorted, based on *QFilterOrderBy* objects.
* `skip` - *Integer* - Optional number of records to be skipped at the beginning of the result set.
e.g., for implementing pagination.
* `limit` - *Integer* - Optional maximum number of records to be returned by the query.
* `transaction` - *QBackendTransaction object* - Optional transaction object.
** Behavior for this object is backend-dependant.
In an RDBMS backend, this object is generally needed if you want your query to see data that may have been modified within the same transaction.
* `recordPipe` - *RecordPipe object* - Optional object that records are placed into, for asynchronous processing.
** If a *recordPipe* is used, then records cannot be retrieved from the *QueryOutput*.
Rather, such records must be read from the pipe's `consumeAvailableRecords()` method.
** A *recordPipe* should only be used when a *QueryAction* is running in a separate Thread from the record's consumer.
* `shouldTranslatePossibleValues` - *boolean, default: false* - Controls whether any fields in the table with a *possibleValueSource* assigned to them should have those possible values looked up
(e.g., to provide text translations in the generated records' `displayValues` map).
** For example, if running a query to present results to a user, this would generally need to be *true*.
But if running a query to provide data as part of a process, then this can generally be left as *false*.
* `shouldGenerateDisplayValues` - *boolean, default: false* - Controls whether if field level *displayFormats* should be used to populate the generated records' `displayValues` map.
** For example, if running a query to present results to a user, this would generally need to be *true*.
But if running a query to provide data as part of a process, then this can generally be left as *false*.
* `queryJoins` - *List of QueryJoin objects* - Optional list of tables to be joined with the main *table* specified in the *QueryInput*.
See QueryJoin below for further details.
==== QQueryFilter
A key component of *QueryInput*, a *QQueryFilter* defines both what records should be included in a query's results (e.g., an SQL `WHERE`), as well as how those results should be sorted (SQL `ORDER BY`).
* `criteria` - *List of QFilterCriteria* - Individual conditions or clauses to filter records.
They are combined using the *booleanOperator* specified in the *QQueryFilter*. See below for further details.
* `orderBys` - *List of QFilterOrderBy* - List of fields (and directions) to control the sorting of query results.
In general, multiple *orderBys* can be given (depending on backend implementations).
* `booleanOperator` - *Enum of AND, OR, default: AND* - Specifies the logical joining operator used among individual criteria.
* `subFilters` - *List of QQueryFilter* - To build arbitrarily complex queries, with nested boolean logic, 0 or more *subFilters* may be provided.
** Each *subFilter* can include its own additional *subFilters*.
** Each *subFilter* can specify a different *booleanOperator*.
** For example, consider the following *QQueryFilter*, that uses two *subFilters*, and a mix of *booleanOperators*
[source,java]
----
queryInput.setFilter(new QQueryFilter()
.withBooleanOperator(OR)
.withSubFilters(List.of(
new QQueryFilter().withBooleanOperator(AND)
.withCriteria(new QFilterCriteria("firstName", EQUALS, "James"))
.withCriteria(new QFilterCriteria("lastName", EQUALS, "Maes")),
new QQueryFilter().withBooleanOperator(AND)
.withCriteria(new QFilterCriteria("firstName", EQUALS, "Darin"))
.withCriteria(new QFilterCriteria("lastName", EQUALS, "Kelkhoff"))
)));
// which would generate the following WHERE clause in an RDBMS backend:
WHERE (first_name='James' AND last_name='Maes') OR (first_name='Darin' AND last_name='Kelkhoff')
----
===== QFilterCriteria
* `fieldName` - *String, required* - Reference to a field on the table being queried.
** Or, in the case of a query with *queryJoins*, a qualified name of a field from a join-table (where the qualifier would be the joined table's name or alias, followed by a dot)
*** For example: `orderLine.sku` or `orderBillToCustomer.firstName`
* `operator` - *Enum of QCriteriaOperator, required* - Comparison operation to be applied to the field specified as *fieldName* and the *values* or *otherFieldName*.
** e.g., `EQUALS`, `NOT_IN`, `GREATER_THAN`, `BETWEEN`, `IS_BLANK`, etc.
* `values` - *List of values, conditional* - Provides the value(s) that the field is compared against.
The number of values (0, 1, 2, or more) be driven based on the *operator* being used.
If an *otherFieldName* is given, and the *operator* expects 1 value, then *values* is ignored, and *otherFieldName* is used.
* `otherFieldName` - *String, conditional* - Specifies that the *fieldName* should be compared against another field in the records, rather than the values in the *values* property.
Only used for *operators* that expect 1 value (e.g., `EQUALS` or `LESS_THAN_OR_EQUALS` - not `IS_NOT_BLANK` or `IN`).
QFilterCriteria definition examples:
[source,java]
----
// one-liners, via constructors that take (List<Serializable> values) or (Serializable... values) in 3rd position
new QFilterCriteria("id", IN, List.of(1, 2, 3))
new QFilterCriteria("name", IS_BLANK)
new QFilterCriteria("orderNo", IN, orderNoList)
new QFilterCriteria("state", EQUALS, "MO");
// long-form, with fluent setters
new QFilterCriteria()
.withFieldName("quantity")
.withOpeartor(QCriteriaOperator.GREATER_THAN)
.withValues(List.of(47));
// to use otherFieldName, long-form must be used
new QFilterCriteria()
.withFieldName("firstName")
.withOpeartor(QCriteriaOperator.EQUALS)
.withOtherFieldName("lastName");
// using otherFieldName to build a criterion that looks at two fields from join tables
new QFilterCriteria()
.withFieldName("billToCustomer.lastName")
.withOpeartor(QCriteriaOperator.NOT_EQUALS)
.withOtherFieldName("shipToCustomer.lastName");
----
===== QFilterOrderBy
* `fieldName` - *String, required* - Reference to a field on the table being queried.
** Or, in the case of a query with *queryJoins*, a qualified name of a field from a join-table (where the qualifier would be the joined table's name or alias, followed by a dot)
* `isAscending` - *boolean, default: true* - Specify if the sort is ascending or descending.
QFilterCriteria definition examples:
[source,java]
----
// short-form, via constructors
new QFilterOrderBy("id") // isAscending defaults to true.
new QFilterOrderBy("name", false)
// long-form, with fluent setters
new QFilterOrderBy()
.withFieldName("birthDate")
.withIsAscending(true);
----
==== QueryJoin
* `joinTable` - *String, required* - Name of the table that is being joined in to the existing query.
** Will be inferred from *joinMetaData*, if *joinTable* is not set when *joinMetaData* gets set.
* `baseTableOrAlias` - *String, required* - Name of a table (or an alias) already defined in the query, to which the *joinTable* will be joined.
** Will be inferred from *joinMetaData*, if *baseTableOrAlias* is not set when *joinMetaData* gets set (which will only use the leftTableName from the joinMetaData - never an alias).
* `joinMetaData` - *QJoinMetaData object* - Optional specification of a {link-join} in the current QInstance.
If not set, will be looked up at runtime based on *baseTableOrAlias* and *joinTable*.
** If set before *baseTableOrAlias* and *joinTable*, then they will be set based on the *leftTable* and *rightTable* in this object.
* `alias` - *String* - Optional (unless multiple instances of the same table are being joined together, when it becomes required).
Behavior based on SQL `FROM` clause aliases.
If given, must be used as the part before the dot in field name specifications throughout the rest of the query input.
* `select` - *boolean, default: false* - Specify whether fields from the *rightTable* should be selected by the query.
If *true*, then the `QRecord` objects returned by this query will have values with corresponding to the (table-or-alias `.` field-name) form.
* `type` - *Enum of INNER, LEFT, RIGHT, FULL, default: INNER* - specifies the SQL-style type of join being performed.
QueryJoin definition examples:
[source,java]
----
// selecting from an "orderLine" table - then join to its corresponding "order" table
queryInput.withTableName("orderLine");
queryInput.withQueryJoin(new QueryJoin("order").withSelect(true));
...
queryOutput.getRecords().get(0).getValueBigDecimal("order.grandTotal");
// given an "order" table with 2 foreign keys to a customer table (billToCustomerId and shipToCustomerId)
// Note, we must supply the JoinMetaData to the QueryJoin, to drive what fields to join on in each case.
queryInput.withTableName("order");
queryInput.withQueryJoins(List.of(
new QueryJoin(instance.getJoin("orderJoinShipToCustomer")
.withAlias("shipToCustomer")
.withSelect(true)),
new QueryJoin(instance.getJoin("orderJoinBillToCustomer")
.withAlias("billToCustomer")
.withSelect(true))));
...
record.getValueString("billToCustomer.firstName")
+ " placed an order for "
+ record.getValueString("shipToCustomer.firstName")
----
=== QueryOutput
* `records` - *List of QRecord* - List of 0 or more records that match the query filter.
** _Note: If a *recordPipe* was supplied to the QueryInput, then calling `queryOutput.getRecords()` will result in an `IllegalStateException` being thrown - as the records were placed into the pipe as they were fetched, and cannot all be accessed as a single list._

View File

@ -1,33 +0,0 @@
== RenderTemplateAction
include::../variables.adoc[]
The `*RenderTemplateAction*` performs the job of taking a template - that is, a string of code, in a templating language, such as https://velocity.apache.org/engine/1.7/user-guide.html[Velocity], and merging it with a set of data (known as a context), to produce some using-facing output, such as a String of HTML.
=== Examples
==== Canonical Form
[source,java]
----
RenderTemplateInput input = new RenderTemplateInput(qInstance);
input.setSession(session);
input.setCode("Hello, ${name}");
input.setTemplateType(TemplateType.VELOCITY);
input.setContext(Map.of("name", "Darin"));
RenderTemplateOutput output = new RenderTemplateAction.execute(input);
String result = output.getResult();
assertEquals("Hello, Darin", result);
----
==== Convenient Form
[source,java]
----
String result = RenderTemplateAction.renderVelocity(input, Map.of("name", "Darin"), "Hello, ${name}");
assertEquals("Hello, Darin", result);
----
=== RenderTemplateInput
* `code` - *String, Required* - String of template code to be rendered, in the templating language specified by the `type` parameter.
* `type` - *Enum of VELOCITY, Required* - Specifies the language of the template code.
* `context` - *Map of String → Object* - Data to be made available to the template during rendering.
=== RenderTemplateOutput
* `result` - *String* - Result of rendering the input template and context.

File diff suppressed because it is too large Load Diff

View File

@ -1,27 +0,0 @@
<!--
~ 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/>.
-->
<style>
pre
{
white-space: pre;
}
</style>

View File

@ -1,17 +0,0 @@
= QQQ
:doctype: book
:toc: left
:source-highlighter: coderay
include::Introduction.adoc[leveloffset=+1]
== Meta Data
include::metaData/Tables.adoc[leveloffset=+1]
''''
include::metaData/Reports.adoc[leveloffset=+1]
== Actions
include::actions/QueryAction.adoc[leveloffset=+1]
''''
include::actions/RenderTemplateAction.adoc[leveloffset=+1]
''''

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,24 +0,0 @@
== QQQ Fields
include::../variables.adoc[]
QQQ Fields define
=== QFieldMetaData
*QFieldMetaData Properties:*
* `name` - *String, Required* - Unique name for the field within its container (table, process, etc).
* `label` - *String* - User-facing label for the field, presented in User Interfaces.
* `type` - *enum of QFieldType, Required* - Data type for values in the field.
* `backendName` - *String* - Name of the field within its backend.
** For example, in an RDBMS-backed table, a field's `name` may be written in camel case, but its `backendName` written with underscores.
* `isRequired` - *boolean, default false* - Indicator that a value is required in this field.
* `isEditable` - *boolean, default true* - Indicator that users may edit values in this field.
* `displayFormat` - *String, default `%s`* - Java Format Specifier string, used to format values in the field for display in user interfaces.
Used to set values in the `displayValues` map within a `QRecord`.
** Recommended values for `displayFormat` come from the `DisplayFormat` interface, such as `DisplayFormat.CURRENCY`, `DisplayFormat.COMMAS`, or `DisplayFormat.DECIMAL2_COMMAS`.
* `defaultValue` - Value to use for the field if no other value is given. Type is based on the field's `type`.
* `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`.
* `

View File

@ -1,173 +0,0 @@
== QQQ Reports
include::../variables.adoc[]
QQQ can generate reports based on {link-tables} defined within a QQQ Instance.
Users can run reports, providing input values.
Alternatively, application code can run reports as needed, supplying input values.
=== QReportMetaData
Reports are defined in a QQQ Instance with a `*QReportMetaData*` object.
Reports are defined in terms of their sources of data (`QReportDataSource`), and their view(s) of that data (`QReportView`).
*QReportMetaData Properties:*
* `name` - *String, Required* - Unique name for the report within the QQQ Instance.
* `label` - *String* - User-facing label for the report, presented in User Interfaces.
Inferred from `name` if not set.
* `processName` - *String* - Name of a {link-process} used to run the report in a User Interface.
* `inputFields` - *List of {link-field}* - Optional list of fields used as input to the report.
** The values in these fields can be used via the syntax `${input.NAME}`, where `NAME` is the `name` attribute of the `inputField`.
** For example:
[source,java]
----
// given this inputField:
new QFieldMetaData("storeId", QFieldType.INTEGER)
// its run-time value can be accessed, e.g., in a query filter under a data source:
new QFilterCriteria("storeId", QCriteriaOperator.EQUALS, List.of("${input.storeId}"))
// or in a report view's title or field formulas:
.withTitleFields(List.of("${input.storeId}"))
new QReportField().withName("storeId").withFormula("${input.storeId}")
----
* `dataSources` - *List of QReportDataSource, Required* - Definitions of the sources of data for the report.
At least one is required.
==== QReportDataSource
Data sources for QQQ Reports can either reference {link-tables} within the QQQ Instance, or they can provide custom code in the form of a `CodeReference` to a `Supplier`, for use cases such as a static data tab in an Excel report.
*QReportDataSource Properties:*
* `name` - *String, Required* - Unique name for the data source within its containing Report.
* `sourceTable` - *String, Conditional* - Reference to a {link-table} in the QQQ Instance, which the data source queries data from.
* `queryFilter` - *QQueryFilter* - If a `sourceTable` is defined, then the filter specified here is used to filter and sort the records queried from that table when generating the report.
* `staticDataSupplier` - *QCodeReference, Conditional* - Reference to custom code which can be used to supply the data for the data source, as an alternative to querying a `sourceTable`.
** Must be a `JAVA` code type
** Must be a `REPORT_STATIC_DATA_SUPPLIER` code usage.
** The referenced class must implement the interface: `Supplier<List<List<Serializable>>>`.
==== QReportView
Report Views control how the source data for a report is organized and presented to the user in the output report file.
If a DataSource describes the rows for a report (e.g., what table provides what records), then a View may be thought of as describing the columns in the report.
A single report can have multiple views, specifically, for the use-case where an Excel file is being generated, in which case each View creates a tab or sheet within the `xlsx` file.
*QReportView Properties:*
* `name` - *String, Required* - Unique name for the view within its containing Report.
* `label` - *String* - Used as a sheet (tab) label in Excel formatted reports.
* `type` - *enum of TABLE, SUMMARY, PIVOT. Required* - Defines the type of view being defined.
** *TABLE* views are a simple listing of the records from the data source.
** *SUMMARY* views are essentially pre-computed Pivot Tables.
That is to say, the aggregation done by a Pivot Table in a spreadsheet file is done by QQQ while generating the report.
In this way, a non-spreadsheet report (e.g., PDF or CSV) can have summarized data, as though it were a Pivot Table in a live spreadsheet.
** *PIVOT* views produce actual Pivot Tables, and are only supported in Excel files _(and are not supported at the time of this writing)_.
* `dataSourceName` - *String, Required* - Reference to a DataSource within the report, that is used to provide the rows for the view.
* `varianceDataSourceName` - *String* - Optional reference to a second DataSource within the report, that is used in `*SUMMARY*` type views for computing variances.
** For example, given a Data Source with a filter that selects all sales records for a given year, a Variance Data Source may have a filter that selects the previous year, for doing comparissons.
* `pivotFields` - *List of String, Conditional* - For *SUMMARY* or *PIVOT* type views, specify the field(s) used as pivot rows.
** For example, in a summary view of orders, you may "pivot" on the *customerId* field, to produce one row per-customer, with aggregate data for that customer.
* `titleFormat` - *String* - Java Format String, used with `titleFields` (if given), to produce a title row, e.g., first row in the view (before any rows from the data source).
* `titleFields` - *List of String, Conditional* - Used with `titleFormat`, to provide values for any format specifiers in the format string.
Syntax to reference a field (e.g., from a report input field) is: `${input.NAME}`, where `NAME` is the `name` attribute of the inputField.
** Example of using `titleFormat` and `titleFields`:
[source,java]
----
// given these inputFields:
new QFieldMetaData("startDate", QFieldType.DATE)
new QFieldMetaData("endDate", QFieldType.DATE)
// a view can have a title row like this:
.withTitleFormat("Weekly Sales Report - %s - %s")
.withTitleFields(List.of("${input.startDate}", "${input.endDate}"))
----
* `includeHeaderRow` - *boolean, default true* - Indication that first row of the view should be the column labels.
** If true, then header row is put in the view.
** If false, then no header row is put in the view.
* `includeTotalRow` - *boolean, default false* - Indication that a totals row should be added to the view.
All numeric columns are summed to produce values in the totals row.
** If true, then totals row is put in the view.
** If false, then no totals row is put in the view.
* `includePivotSubTotals` - *boolean, default false* - For a *SUMMARY* or *PIVOT* type view, if there are more than 1 *pivotFields* being used, this field is an indication that each higher-level pivot should include sub-totals.
** #TODO - provide example#
* `columns` - *List of QReportField, required* - Definition of the columns to appear in the view. See section on QReportField for details.
* `orderByFields` - *List of QFilterOrderBy, optional* - For a *SUMMARY* or *PIVOT* type view, how to sort the rows.
* `recordTransformStep` - *QCodeReference, subclass of `AbstractTransformStep`* - Custom code reference that can be used to transform records after they are queried from the data source, and before they are placed into the view.
Can be used to transform or customize values, or to look up additional values to add to the report.
** #TODO - provide example#
* `viewCustomizer` - *QCodeReference, implementation of interface `Function<QReportView, QReportView>`* - Custom code reference that can be used to customize the report view, at runtime.
Can be used, for example, to dynamically define the report's *columns*.
** #TODO - provide example#
===== QReportField
Report Fields define the fields (AKA columns) of data that appear in a report view.
These fields can either be direct references to fields from the report's data sources, or values computed using formula defined in the QReportField.
*QReportField Properties:*
* `name` - *String, required* - Unique identifier for the field within its ReportView.
In general, will be a reference to a field from the ReportView's DataSource *unless a *formula* is given (for *SUMMARY* type views), the field is marked as *isVirtual*, or the field is marked as *showPossibleValueLabel*).
* `label` - *String* - Optional text label to identify the field, for example, in a header row.
If not given, may be derived from field, where possible.
* `type` - *QFieldType*
* `formula` - *String, conditional* - Required for *SUMMARY* type views.
Defines the formula to be used for computing the value in this field.
** For example:
[source,java]
----
.withName("reportEndDate").withFormula("${input.endDate}")
.withName("count").withFormula("${pivot.count.id}")
.withName("percentOfTotal").withFormula("=DIVIDE(${pivot.count.id},${total.count.id})")
.withName("sumCost").withFormula("${pivot.sum.cost}")
.withName("sumCharge").withFormula("${pivot.sum.charge}")
.withName("profit").withFormula("=MINUS(${pivot.sum.charge},${pivot.sum.cost})")
.withName("totalCost").withFormula("=DIVIDE_SCALE(${pivot.sum.cost},${pivot.count.id},2)")
.withName("revenuePer").withFormula("=DIVIDE_SCALE(${pivot.sum.charge},${pivot.count.id},2)")
.withName("marginPer").withFormula("=MINUS(DIVIDE_SCALE(${pivot.sum.charge},${pivot.count.id},2),DIVIDE_SCALE(${pivot.sum.cost},${pivot.count.id},2))")
.withName("thisWeekMargin").withFormula("=SCALE(DIVIDE(${thisRow.profit},${pivot.sum.charge}),2)")
.withName("previousWeekProfit").withFormula("=MINUS(${variancePivot.sum.charge},${variancePivot.sum.cost})")
.withName("previousWeekMargin").withFormula("=SCALE(DIVIDE(${thisRow.previousWeekProfit},${variancePivot.sum.charge}),2)")
.withName("marginThisVsPrevious").withFormula("=SCALE(MINUS(${thisRow.margin},${thisRow.marginPrevious}),3)")
.withName("exception").withFormula("""
=IF(LT(${thisRow.margin},0),Negative Margin,IF(LT(${thisRow.marginThisVsPrevious},0),Margin Decreased,""))""")
----
* `displayFormat` *String*
* `isVirtual` *Boolean, default false* - (needs reviewed - may only be required for report views using a data source with a *staticDataSupplier*)
* `showPossibleValueLabel` *Boolean, default false* - To show a translated value for a Possible Value field (e.g., a name or other value meaningful to a user, instead of a foreign key).
* `sourceFieldName` *String* - Used for the scenario where a possibleValue field is included in a report both as the foreign key (raw, id value), and the translated "label" value.
In that case, the field marked with *showPossibleValueLabel* = true should be given a different name, and should use *sourceFieldName* to indicate the field that has the id value.
** For example:
[source,java]
----
// this field would have the "raw" warehouseId values
// e.g., integers - foreign keys to a warehouse table. Generally useful for machines to know.
new QReportField("warehouseId")
.withLabel("Warehouse Id"),
// this field would have the translated values from the warehouse PossibleValueSource
// for example, maybe the name field from the warehouse table. A string, useful for humans to read.
new QReportField("warehouseName")
.withSourceFieldName("warehouseId")
.withShowPossibleValueLabel(true)
.withLabel("Warehouse Name"),
----

View File

@ -1,49 +0,0 @@
== QQQ Tables
include::../variables.adoc[]
The core type of object in a QQQ Instance is the Table.
In the most common use-case, a QQQ Table may be the in-app representation of a Database table.
That is, it is a collection of records (or rows) of data, each of which has a set of fields (or columns).
QQQ also allows other types of data sources ({link-backends}) to be used as tables, such as File systems, API's, Java enums or objects, etc.
All of these backend types present the same interfaces (both user-interfaces, and application programming interfaces), regardless of their backend type.
=== QTableMetaData
Tables are defined in a QQQ Instance in a `*QTableMetaData*` object.
All tables must reference a {link-backend}, a list of fields that define the shape of records in the table, and additional data to describe how to work with the table within its backend.
*QTableMetaData Properties:*
* `name` - *String, Required* - Unique name for the table within the QQQ Instance.
* `label` - *String* - User-facing label for the table, presented in User Interfaces.
Inferred from `name` if not set.
* `backendName` - *String, Required* - Name of a {link-backend} in which this table's data is managed.
* `fields` - *Map of String → {link-field}, Required* - The columns of data that make up all records in this table.
* `primaryKeyField` - *String, Conditional* - Name of a {link-field} that serves as the primary key (e.g., unique identifier) for records in this table.
* `uniqueKeys` - *List of UniqueKey* - Definition of additional unique constraints (from an RDBMS point of view) from the table.
e.g., sets of columns which must have unique values for each record in the table.
* `backendDetails` - *QTableBackendDetails or subclass* - Additional data to configure the table within its {link-backend}.
* `automationDetails` - *QTableAutomationDetails* - Configuration of automated jobs that run against records in the table, e.g., upon insert or update.
* `customizers` - *Map of String → QCodeReference* - References to custom code that are injected into standard table actions, that allow applications to customize certain parts of how the table works.
* `parentAppName` - *String* - Name of a {link-app} that this table exists within.
* `icon` - *QIcon* - Icon associated with this table in certain user interfaces.
* `recordLabelFormat` - *String* - Java Format String, used with `recordLabelFields` to produce a label shown for records from the table.
* `recordLabelFields` - *List of String, Conditional* - Used with `recordLabelFormat` to provide values for any format specifiers in the format string.
These strings must be field names within the table.
** Example of using `recordLabelFormat` and `recordLabelFields`:
[source,java]
----
// given these fields in the table:
new QFieldMetaData("name", QFieldType.STRING)
new QFieldMetaData("birthDate", QFieldType.DATE)
// We can produce a record label such as "Darin Kelkhoff (1980-05-31)" via:
.withRecordLabelFormat("%s (%s)")
.withRecordLabelFields(List.of("name", "birthDate"))
----
* `sections` - *List of QFieldSection* - Mechanism to organize fields within user interfaces, into logical sections.
If any sections are present in the table meta data, then all fields in the table must be listed in exactly 1 section.
If no sections are defined, then instance enrichment will define default sections.
* `associatedScripts` - *List of AssociatedScript* - Definition of user-defined scripts that can be associated with records within the table.
* `enabledCapabilities` and `disabledCapabilities` - *Set of Capability enum values* - Overrides from the backend level, for capabilities that this table does or does not possess.

View File

@ -1,553 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="generator" content="Asciidoctor 2.0.18">
<title>QQQ Tables</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Open+Sans:300,300italic,400,400italic,600,600italic%7CNoto+Serif:400,400italic,700,700italic%7CDroid+Sans+Mono:400,700">
<style>
/*! Asciidoctor default stylesheet | MIT License | https://asciidoctor.org */
/* Uncomment the following line when using as a custom stylesheet */
/* @import "https://fonts.googleapis.com/css?family=Open+Sans:300,300italic,400,400italic,600,600italic%7CNoto+Serif:400,400italic,700,700italic%7CDroid+Sans+Mono:400,700"; */
html{font-family:sans-serif;-webkit-text-size-adjust:100%}
a{background:none}
a:focus{outline:thin dotted}
a:active,a:hover{outline:0}
h1{font-size:2em;margin:.67em 0}
b,strong{font-weight:bold}
abbr{font-size:.9em}
abbr[title]{cursor:help;border-bottom:1px dotted #dddddf;text-decoration:none}
dfn{font-style:italic}
hr{height:0}
mark{background:#ff0;color:#000}
code,kbd,pre,samp{font-family:monospace;font-size:1em}
pre{white-space:pre-wrap}
q{quotes:"\201C" "\201D" "\2018" "\2019"}
small{font-size:80%}
sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}
sup{top:-.5em}
sub{bottom:-.25em}
img{border:0}
svg:not(:root){overflow:hidden}
figure{margin:0}
audio,video{display:inline-block}
audio:not([controls]){display:none;height:0}
fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}
legend{border:0;padding:0}
button,input,select,textarea{font-family:inherit;font-size:100%;margin:0}
button,input{line-height:normal}
button,select{text-transform:none}
button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}
button[disabled],html input[disabled]{cursor:default}
input[type=checkbox],input[type=radio]{padding:0}
button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}
textarea{overflow:auto;vertical-align:top}
table{border-collapse:collapse;border-spacing:0}
*,::before,::after{box-sizing:border-box}
html,body{font-size:100%}
body{background:#fff;color:rgba(0,0,0,.8);padding:0;margin:0;font-family:"Noto Serif","DejaVu Serif",serif;line-height:1;position:relative;cursor:auto;-moz-tab-size:4;-o-tab-size:4;tab-size:4;word-wrap:anywhere;-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased}
a:hover{cursor:pointer}
img,object,embed{max-width:100%;height:auto}
object,embed{height:100%}
img{-ms-interpolation-mode:bicubic}
.left{float:left!important}
.right{float:right!important}
.text-left{text-align:left!important}
.text-right{text-align:right!important}
.text-center{text-align:center!important}
.text-justify{text-align:justify!important}
.hide{display:none}
img,object,svg{display:inline-block;vertical-align:middle}
textarea{height:auto;min-height:50px}
select{width:100%}
.subheader,.admonitionblock td.content>.title,.audioblock>.title,.exampleblock>.title,.imageblock>.title,.listingblock>.title,.literalblock>.title,.stemblock>.title,.openblock>.title,.paragraph>.title,.quoteblock>.title,table.tableblock>.title,.verseblock>.title,.videoblock>.title,.dlist>.title,.olist>.title,.ulist>.title,.qlist>.title,.hdlist>.title{line-height:1.45;color:#7a2518;font-weight:400;margin-top:0;margin-bottom:.25em}
div,dl,dt,dd,ul,ol,li,h1,h2,h3,#toctitle,.sidebarblock>.content>.title,h4,h5,h6,pre,form,p,blockquote,th,td{margin:0;padding:0}
a{color:#2156a5;text-decoration:underline;line-height:inherit}
a:hover,a:focus{color:#1d4b8f}
a img{border:0}
p{line-height:1.6;margin-bottom:1.25em;text-rendering:optimizeLegibility}
p aside{font-size:.875em;line-height:1.35;font-style:italic}
h1,h2,h3,#toctitle,.sidebarblock>.content>.title,h4,h5,h6{font-family:"Open Sans","DejaVu Sans",sans-serif;font-weight:300;font-style:normal;color:#ba3925;text-rendering:optimizeLegibility;margin-top:1em;margin-bottom:.5em;line-height:1.0125em}
h1 small,h2 small,h3 small,#toctitle small,.sidebarblock>.content>.title small,h4 small,h5 small,h6 small{font-size:60%;color:#e99b8f;line-height:0}
h1{font-size:2.125em}
h2{font-size:1.6875em}
h3,#toctitle,.sidebarblock>.content>.title{font-size:1.375em}
h4,h5{font-size:1.125em}
h6{font-size:1em}
hr{border:solid #dddddf;border-width:1px 0 0;clear:both;margin:1.25em 0 1.1875em}
em,i{font-style:italic;line-height:inherit}
strong,b{font-weight:bold;line-height:inherit}
small{font-size:60%;line-height:inherit}
code{font-family:"Droid Sans Mono","DejaVu Sans Mono",monospace;font-weight:400;color:rgba(0,0,0,.9)}
ul,ol,dl{line-height:1.6;margin-bottom:1.25em;list-style-position:outside;font-family:inherit}
ul,ol{margin-left:1.5em}
ul li ul,ul li ol{margin-left:1.25em;margin-bottom:0}
ul.circle{list-style-type:circle}
ul.disc{list-style-type:disc}
ul.square{list-style-type:square}
ul.circle ul:not([class]),ul.disc ul:not([class]),ul.square ul:not([class]){list-style:inherit}
ol li ul,ol li ol{margin-left:1.25em;margin-bottom:0}
dl dt{margin-bottom:.3125em;font-weight:bold}
dl dd{margin-bottom:1.25em}
blockquote{margin:0 0 1.25em;padding:.5625em 1.25em 0 1.1875em;border-left:1px solid #ddd}
blockquote,blockquote p{line-height:1.6;color:rgba(0,0,0,.85)}
@media screen and (min-width:768px){h1,h2,h3,#toctitle,.sidebarblock>.content>.title,h4,h5,h6{line-height:1.2}
h1{font-size:2.75em}
h2{font-size:2.3125em}
h3,#toctitle,.sidebarblock>.content>.title{font-size:1.6875em}
h4{font-size:1.4375em}}
table{background:#fff;margin-bottom:1.25em;border:1px solid #dedede;word-wrap:normal}
table thead,table tfoot{background:#f7f8f7}
table thead tr th,table thead tr td,table tfoot tr th,table tfoot tr td{padding:.5em .625em .625em;font-size:inherit;color:rgba(0,0,0,.8);text-align:left}
table tr th,table tr td{padding:.5625em .625em;font-size:inherit;color:rgba(0,0,0,.8)}
table tr.even,table tr.alt{background:#f8f8f7}
table thead tr th,table tfoot tr th,table tbody tr td,table tr td,table tfoot tr td{line-height:1.6}
h1,h2,h3,#toctitle,.sidebarblock>.content>.title,h4,h5,h6{line-height:1.2;word-spacing:-.05em}
h1 strong,h2 strong,h3 strong,#toctitle strong,.sidebarblock>.content>.title strong,h4 strong,h5 strong,h6 strong{font-weight:400}
.center{margin-left:auto;margin-right:auto}
.stretch{width:100%}
.clearfix::before,.clearfix::after,.float-group::before,.float-group::after{content:" ";display:table}
.clearfix::after,.float-group::after{clear:both}
:not(pre).nobreak{word-wrap:normal}
:not(pre).nowrap{white-space:nowrap}
:not(pre).pre-wrap{white-space:pre-wrap}
:not(pre):not([class^=L])>code{font-size:.9375em;font-style:normal!important;letter-spacing:0;padding:.1em .5ex;word-spacing:-.15em;background:#f7f7f8;border-radius:4px;line-height:1.45;text-rendering:optimizeSpeed}
pre{color:rgba(0,0,0,.9);font-family:"Droid Sans Mono","DejaVu Sans Mono",monospace;line-height:1.45;text-rendering:optimizeSpeed}
pre code,pre pre{color:inherit;font-size:inherit;line-height:inherit}
pre>code{display:block}
pre.nowrap,pre.nowrap pre{white-space:pre;word-wrap:normal}
em em{font-style:normal}
strong strong{font-weight:400}
.keyseq{color:rgba(51,51,51,.8)}
kbd{font-family:"Droid Sans Mono","DejaVu Sans Mono",monospace;display:inline-block;color:rgba(0,0,0,.8);font-size:.65em;line-height:1.45;background:#f7f7f7;border:1px solid #ccc;border-radius:3px;box-shadow:0 1px 0 rgba(0,0,0,.2),inset 0 0 0 .1em #fff;margin:0 .15em;padding:.2em .5em;vertical-align:middle;position:relative;top:-.1em;white-space:nowrap}
.keyseq kbd:first-child{margin-left:0}
.keyseq kbd:last-child{margin-right:0}
.menuseq,.menuref{color:#000}
.menuseq b:not(.caret),.menuref{font-weight:inherit}
.menuseq{word-spacing:-.02em}
.menuseq b.caret{font-size:1.25em;line-height:.8}
.menuseq i.caret{font-weight:bold;text-align:center;width:.45em}
b.button::before,b.button::after{position:relative;top:-1px;font-weight:400}
b.button::before{content:"[";padding:0 3px 0 2px}
b.button::after{content:"]";padding:0 2px 0 3px}
p a>code:hover{color:rgba(0,0,0,.9)}
#header,#content,#footnotes,#footer{width:100%;margin:0 auto;max-width:62.5em;*zoom:1;position:relative;padding-left:.9375em;padding-right:.9375em}
#header::before,#header::after,#content::before,#content::after,#footnotes::before,#footnotes::after,#footer::before,#footer::after{content:" ";display:table}
#header::after,#content::after,#footnotes::after,#footer::after{clear:both}
#content{margin-top:1.25em}
#content::before{content:none}
#header>h1:first-child{color:rgba(0,0,0,.85);margin-top:2.25rem;margin-bottom:0}
#header>h1:first-child+#toc{margin-top:8px;border-top:1px solid #dddddf}
#header>h1:only-child,body.toc2 #header>h1:nth-last-child(2){border-bottom:1px solid #dddddf;padding-bottom:8px}
#header .details{border-bottom:1px solid #dddddf;line-height:1.45;padding-top:.25em;padding-bottom:.25em;padding-left:.25em;color:rgba(0,0,0,.6);display:flex;flex-flow:row wrap}
#header .details span:first-child{margin-left:-.125em}
#header .details span.email a{color:rgba(0,0,0,.85)}
#header .details br{display:none}
#header .details br+span::before{content:"\00a0\2013\00a0"}
#header .details br+span.author::before{content:"\00a0\22c5\00a0";color:rgba(0,0,0,.85)}
#header .details br+span#revremark::before{content:"\00a0|\00a0"}
#header #revnumber{text-transform:capitalize}
#header #revnumber::after{content:"\00a0"}
#content>h1:first-child:not([class]){color:rgba(0,0,0,.85);border-bottom:1px solid #dddddf;padding-bottom:8px;margin-top:0;padding-top:1rem;margin-bottom:1.25rem}
#toc{border-bottom:1px solid #e7e7e9;padding-bottom:.5em}
#toc>ul{margin-left:.125em}
#toc ul.sectlevel0>li>a{font-style:italic}
#toc ul.sectlevel0 ul.sectlevel1{margin:.5em 0}
#toc ul{font-family:"Open Sans","DejaVu Sans",sans-serif;list-style-type:none}
#toc li{line-height:1.3334;margin-top:.3334em}
#toc a{text-decoration:none}
#toc a:active{text-decoration:underline}
#toctitle{color:#7a2518;font-size:1.2em}
@media screen and (min-width:768px){#toctitle{font-size:1.375em}
body.toc2{padding-left:15em;padding-right:0}
#toc.toc2{margin-top:0!important;background:#f8f8f7;position:fixed;width:15em;left:0;top:0;border-right:1px solid #e7e7e9;border-top-width:0!important;border-bottom-width:0!important;z-index:1000;padding:1.25em 1em;height:100%;overflow:auto}
#toc.toc2 #toctitle{margin-top:0;margin-bottom:.8rem;font-size:1.2em}
#toc.toc2>ul{font-size:.9em;margin-bottom:0}
#toc.toc2 ul ul{margin-left:0;padding-left:1em}
#toc.toc2 ul.sectlevel0 ul.sectlevel1{padding-left:0;margin-top:.5em;margin-bottom:.5em}
body.toc2.toc-right{padding-left:0;padding-right:15em}
body.toc2.toc-right #toc.toc2{border-right-width:0;border-left:1px solid #e7e7e9;left:auto;right:0}}
@media screen and (min-width:1280px){body.toc2{padding-left:20em;padding-right:0}
#toc.toc2{width:20em}
#toc.toc2 #toctitle{font-size:1.375em}
#toc.toc2>ul{font-size:.95em}
#toc.toc2 ul ul{padding-left:1.25em}
body.toc2.toc-right{padding-left:0;padding-right:20em}}
#content #toc{border:1px solid #e0e0dc;margin-bottom:1.25em;padding:1.25em;background:#f8f8f7;border-radius:4px}
#content #toc>:first-child{margin-top:0}
#content #toc>:last-child{margin-bottom:0}
#footer{max-width:none;background:rgba(0,0,0,.8);padding:1.25em}
#footer-text{color:hsla(0,0%,100%,.8);line-height:1.44}
#content{margin-bottom:.625em}
.sect1{padding-bottom:.625em}
@media screen and (min-width:768px){#content{margin-bottom:1.25em}
.sect1{padding-bottom:1.25em}}
.sect1:last-child{padding-bottom:0}
.sect1+.sect1{border-top:1px solid #e7e7e9}
#content h1>a.anchor,h2>a.anchor,h3>a.anchor,#toctitle>a.anchor,.sidebarblock>.content>.title>a.anchor,h4>a.anchor,h5>a.anchor,h6>a.anchor{position:absolute;z-index:1001;width:1.5ex;margin-left:-1.5ex;display:block;text-decoration:none!important;visibility:hidden;text-align:center;font-weight:400}
#content h1>a.anchor::before,h2>a.anchor::before,h3>a.anchor::before,#toctitle>a.anchor::before,.sidebarblock>.content>.title>a.anchor::before,h4>a.anchor::before,h5>a.anchor::before,h6>a.anchor::before{content:"\00A7";font-size:.85em;display:block;padding-top:.1em}
#content h1:hover>a.anchor,#content h1>a.anchor:hover,h2:hover>a.anchor,h2>a.anchor:hover,h3:hover>a.anchor,#toctitle:hover>a.anchor,.sidebarblock>.content>.title:hover>a.anchor,h3>a.anchor:hover,#toctitle>a.anchor:hover,.sidebarblock>.content>.title>a.anchor:hover,h4:hover>a.anchor,h4>a.anchor:hover,h5:hover>a.anchor,h5>a.anchor:hover,h6:hover>a.anchor,h6>a.anchor:hover{visibility:visible}
#content h1>a.link,h2>a.link,h3>a.link,#toctitle>a.link,.sidebarblock>.content>.title>a.link,h4>a.link,h5>a.link,h6>a.link{color:#ba3925;text-decoration:none}
#content h1>a.link:hover,h2>a.link:hover,h3>a.link:hover,#toctitle>a.link:hover,.sidebarblock>.content>.title>a.link:hover,h4>a.link:hover,h5>a.link:hover,h6>a.link:hover{color:#a53221}
details,.audioblock,.imageblock,.literalblock,.listingblock,.stemblock,.videoblock{margin-bottom:1.25em}
details{margin-left:1.25rem}
details>summary{cursor:pointer;display:block;position:relative;line-height:1.6;margin-bottom:.625rem;outline:none;-webkit-tap-highlight-color:transparent}
details>summary::-webkit-details-marker{display:none}
details>summary::before{content:"";border:solid transparent;border-left:solid;border-width:.3em 0 .3em .5em;position:absolute;top:.5em;left:-1.25rem;transform:translateX(15%)}
details[open]>summary::before{border:solid transparent;border-top:solid;border-width:.5em .3em 0;transform:translateY(15%)}
details>summary::after{content:"";width:1.25rem;height:1em;position:absolute;top:.3em;left:-1.25rem}
.admonitionblock td.content>.title,.audioblock>.title,.exampleblock>.title,.imageblock>.title,.listingblock>.title,.literalblock>.title,.stemblock>.title,.openblock>.title,.paragraph>.title,.quoteblock>.title,table.tableblock>.title,.verseblock>.title,.videoblock>.title,.dlist>.title,.olist>.title,.ulist>.title,.qlist>.title,.hdlist>.title{text-rendering:optimizeLegibility;text-align:left;font-family:"Noto Serif","DejaVu Serif",serif;font-size:1rem;font-style:italic}
table.tableblock.fit-content>caption.title{white-space:nowrap;width:0}
.paragraph.lead>p,#preamble>.sectionbody>[class=paragraph]:first-of-type p{font-size:1.21875em;line-height:1.6;color:rgba(0,0,0,.85)}
.admonitionblock>table{border-collapse:separate;border:0;background:none;width:100%}
.admonitionblock>table td.icon{text-align:center;width:80px}
.admonitionblock>table td.icon img{max-width:none}
.admonitionblock>table td.icon .title{font-weight:bold;font-family:"Open Sans","DejaVu Sans",sans-serif;text-transform:uppercase}
.admonitionblock>table td.content{padding-left:1.125em;padding-right:1.25em;border-left:1px solid #dddddf;color:rgba(0,0,0,.6);word-wrap:anywhere}
.admonitionblock>table td.content>:last-child>:last-child{margin-bottom:0}
.exampleblock>.content{border:1px solid #e6e6e6;margin-bottom:1.25em;padding:1.25em;background:#fff;border-radius:4px}
.exampleblock>.content>:first-child{margin-top:0}
.exampleblock>.content>:last-child{margin-bottom:0}
.sidebarblock{border:1px solid #dbdbd6;margin-bottom:1.25em;padding:1.25em;background:#f3f3f2;border-radius:4px}
.sidebarblock>:first-child{margin-top:0}
.sidebarblock>:last-child{margin-bottom:0}
.sidebarblock>.content>.title{color:#7a2518;margin-top:0;text-align:center}
.exampleblock>.content>:last-child>:last-child,.exampleblock>.content .olist>ol>li:last-child>:last-child,.exampleblock>.content .ulist>ul>li:last-child>:last-child,.exampleblock>.content .qlist>ol>li:last-child>:last-child,.sidebarblock>.content>:last-child>:last-child,.sidebarblock>.content .olist>ol>li:last-child>:last-child,.sidebarblock>.content .ulist>ul>li:last-child>:last-child,.sidebarblock>.content .qlist>ol>li:last-child>:last-child{margin-bottom:0}
.literalblock pre,.listingblock>.content>pre{border-radius:4px;overflow-x:auto;padding:1em;font-size:.8125em}
@media screen and (min-width:768px){.literalblock pre,.listingblock>.content>pre{font-size:.90625em}}
@media screen and (min-width:1280px){.literalblock pre,.listingblock>.content>pre{font-size:1em}}
.literalblock pre,.listingblock>.content>pre:not(.highlight),.listingblock>.content>pre[class=highlight],.listingblock>.content>pre[class^="highlight "]{background:#f7f7f8}
.literalblock.output pre{color:#f7f7f8;background:rgba(0,0,0,.9)}
.listingblock>.content{position:relative}
.listingblock code[data-lang]::before{display:none;content:attr(data-lang);position:absolute;font-size:.75em;top:.425rem;right:.5rem;line-height:1;text-transform:uppercase;color:inherit;opacity:.5}
.listingblock:hover code[data-lang]::before{display:block}
.listingblock.terminal pre .command::before{content:attr(data-prompt);padding-right:.5em;color:inherit;opacity:.5}
.listingblock.terminal pre .command:not([data-prompt])::before{content:"$"}
.listingblock pre.highlightjs{padding:0}
.listingblock pre.highlightjs>code{padding:1em;border-radius:4px}
.listingblock pre.prettyprint{border-width:0}
.prettyprint{background:#f7f7f8}
pre.prettyprint .linenums{line-height:1.45;margin-left:2em}
pre.prettyprint li{background:none;list-style-type:inherit;padding-left:0}
pre.prettyprint li code[data-lang]::before{opacity:1}
pre.prettyprint li:not(:first-child) code[data-lang]::before{display:none}
table.linenotable{border-collapse:separate;border:0;margin-bottom:0;background:none}
table.linenotable td[class]{color:inherit;vertical-align:top;padding:0;line-height:inherit;white-space:normal}
table.linenotable td.code{padding-left:.75em}
table.linenotable td.linenos,pre.pygments .linenos{border-right:1px solid;opacity:.35;padding-right:.5em;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}
pre.pygments span.linenos{display:inline-block;margin-right:.75em}
.quoteblock{margin:0 1em 1.25em 1.5em;display:table}
.quoteblock:not(.excerpt)>.title{margin-left:-1.5em;margin-bottom:.75em}
.quoteblock blockquote,.quoteblock p{color:rgba(0,0,0,.85);font-size:1.15rem;line-height:1.75;word-spacing:.1em;letter-spacing:0;font-style:italic;text-align:justify}
.quoteblock blockquote{margin:0;padding:0;border:0}
.quoteblock blockquote::before{content:"\201c";float:left;font-size:2.75em;font-weight:bold;line-height:.6em;margin-left:-.6em;color:#7a2518;text-shadow:0 1px 2px rgba(0,0,0,.1)}
.quoteblock blockquote>.paragraph:last-child p{margin-bottom:0}
.quoteblock .attribution{margin-top:.75em;margin-right:.5ex;text-align:right}
.verseblock{margin:0 1em 1.25em}
.verseblock pre{font-family:"Open Sans","DejaVu Sans",sans-serif;font-size:1.15rem;color:rgba(0,0,0,.85);font-weight:300;text-rendering:optimizeLegibility}
.verseblock pre strong{font-weight:400}
.verseblock .attribution{margin-top:1.25rem;margin-left:.5ex}
.quoteblock .attribution,.verseblock .attribution{font-size:.9375em;line-height:1.45;font-style:italic}
.quoteblock .attribution br,.verseblock .attribution br{display:none}
.quoteblock .attribution cite,.verseblock .attribution cite{display:block;letter-spacing:-.025em;color:rgba(0,0,0,.6)}
.quoteblock.abstract blockquote::before,.quoteblock.excerpt blockquote::before,.quoteblock .quoteblock blockquote::before{display:none}
.quoteblock.abstract blockquote,.quoteblock.abstract p,.quoteblock.excerpt blockquote,.quoteblock.excerpt p,.quoteblock .quoteblock blockquote,.quoteblock .quoteblock p{line-height:1.6;word-spacing:0}
.quoteblock.abstract{margin:0 1em 1.25em;display:block}
.quoteblock.abstract>.title{margin:0 0 .375em;font-size:1.15em;text-align:center}
.quoteblock.excerpt>blockquote,.quoteblock .quoteblock{padding:0 0 .25em 1em;border-left:.25em solid #dddddf}
.quoteblock.excerpt,.quoteblock .quoteblock{margin-left:0}
.quoteblock.excerpt blockquote,.quoteblock.excerpt p,.quoteblock .quoteblock blockquote,.quoteblock .quoteblock p{color:inherit;font-size:1.0625rem}
.quoteblock.excerpt .attribution,.quoteblock .quoteblock .attribution{color:inherit;font-size:.85rem;text-align:left;margin-right:0}
p.tableblock:last-child{margin-bottom:0}
td.tableblock>.content{margin-bottom:1.25em;word-wrap:anywhere}
td.tableblock>.content>:last-child{margin-bottom:-1.25em}
table.tableblock,th.tableblock,td.tableblock{border:0 solid #dedede}
table.grid-all>*>tr>*{border-width:1px}
table.grid-cols>*>tr>*{border-width:0 1px}
table.grid-rows>*>tr>*{border-width:1px 0}
table.frame-all{border-width:1px}
table.frame-ends{border-width:1px 0}
table.frame-sides{border-width:0 1px}
table.frame-none>colgroup+*>:first-child>*,table.frame-sides>colgroup+*>:first-child>*{border-top-width:0}
table.frame-none>:last-child>:last-child>*,table.frame-sides>:last-child>:last-child>*{border-bottom-width:0}
table.frame-none>*>tr>:first-child,table.frame-ends>*>tr>:first-child{border-left-width:0}
table.frame-none>*>tr>:last-child,table.frame-ends>*>tr>:last-child{border-right-width:0}
table.stripes-all>*>tr,table.stripes-odd>*>tr:nth-of-type(odd),table.stripes-even>*>tr:nth-of-type(even),table.stripes-hover>*>tr:hover{background:#f8f8f7}
th.halign-left,td.halign-left{text-align:left}
th.halign-right,td.halign-right{text-align:right}
th.halign-center,td.halign-center{text-align:center}
th.valign-top,td.valign-top{vertical-align:top}
th.valign-bottom,td.valign-bottom{vertical-align:bottom}
th.valign-middle,td.valign-middle{vertical-align:middle}
table thead th,table tfoot th{font-weight:bold}
tbody tr th{background:#f7f8f7}
tbody tr th,tbody tr th p,tfoot tr th,tfoot tr th p{color:rgba(0,0,0,.8);font-weight:bold}
p.tableblock>code:only-child{background:none;padding:0}
p.tableblock{font-size:1em}
ol{margin-left:1.75em}
ul li ol{margin-left:1.5em}
dl dd{margin-left:1.125em}
dl dd:last-child,dl dd:last-child>:last-child{margin-bottom:0}
li p,ul dd,ol dd,.olist .olist,.ulist .ulist,.ulist .olist,.olist .ulist{margin-bottom:.625em}
ul.checklist,ul.none,ol.none,ul.no-bullet,ol.no-bullet,ol.unnumbered,ul.unstyled,ol.unstyled{list-style-type:none}
ul.no-bullet,ol.no-bullet,ol.unnumbered{margin-left:.625em}
ul.unstyled,ol.unstyled{margin-left:0}
li>p:empty:only-child::before{content:"";display:inline-block}
ul.checklist>li>p:first-child{margin-left:-1em}
ul.checklist>li>p:first-child>.fa-square-o:first-child,ul.checklist>li>p:first-child>.fa-check-square-o:first-child{width:1.25em;font-size:.8em;position:relative;bottom:.125em}
ul.checklist>li>p:first-child>input[type=checkbox]:first-child{margin-right:.25em}
ul.inline{display:flex;flex-flow:row wrap;list-style:none;margin:0 0 .625em -1.25em}
ul.inline>li{margin-left:1.25em}
.unstyled dl dt{font-weight:400;font-style:normal}
ol.arabic{list-style-type:decimal}
ol.decimal{list-style-type:decimal-leading-zero}
ol.loweralpha{list-style-type:lower-alpha}
ol.upperalpha{list-style-type:upper-alpha}
ol.lowerroman{list-style-type:lower-roman}
ol.upperroman{list-style-type:upper-roman}
ol.lowergreek{list-style-type:lower-greek}
.hdlist>table,.colist>table{border:0;background:none}
.hdlist>table>tbody>tr,.colist>table>tbody>tr{background:none}
td.hdlist1,td.hdlist2{vertical-align:top;padding:0 .625em}
td.hdlist1{font-weight:bold;padding-bottom:1.25em}
td.hdlist2{word-wrap:anywhere}
.literalblock+.colist,.listingblock+.colist{margin-top:-.5em}
.colist td:not([class]):first-child{padding:.4em .75em 0;line-height:1;vertical-align:top}
.colist td:not([class]):first-child img{max-width:none}
.colist td:not([class]):last-child{padding:.25em 0}
.thumb,.th{line-height:0;display:inline-block;border:4px solid #fff;box-shadow:0 0 0 1px #ddd}
.imageblock.left{margin:.25em .625em 1.25em 0}
.imageblock.right{margin:.25em 0 1.25em .625em}
.imageblock>.title{margin-bottom:0}
.imageblock.thumb,.imageblock.th{border-width:6px}
.imageblock.thumb>.title,.imageblock.th>.title{padding:0 .125em}
.image.left,.image.right{margin-top:.25em;margin-bottom:.25em;display:inline-block;line-height:0}
.image.left{margin-right:.625em}
.image.right{margin-left:.625em}
a.image{text-decoration:none;display:inline-block}
a.image object{pointer-events:none}
sup.footnote,sup.footnoteref{font-size:.875em;position:static;vertical-align:super}
sup.footnote a,sup.footnoteref a{text-decoration:none}
sup.footnote a:active,sup.footnoteref a:active{text-decoration:underline}
#footnotes{padding-top:.75em;padding-bottom:.75em;margin-bottom:.625em}
#footnotes hr{width:20%;min-width:6.25em;margin:-.25em 0 .75em;border-width:1px 0 0}
#footnotes .footnote{padding:0 .375em 0 .225em;line-height:1.3334;font-size:.875em;margin-left:1.2em;margin-bottom:.2em}
#footnotes .footnote a:first-of-type{font-weight:bold;text-decoration:none;margin-left:-1.05em}
#footnotes .footnote:last-of-type{margin-bottom:0}
#content #footnotes{margin-top:-.625em;margin-bottom:0;padding:.75em 0}
div.unbreakable{page-break-inside:avoid}
.big{font-size:larger}
.small{font-size:smaller}
.underline{text-decoration:underline}
.overline{text-decoration:overline}
.line-through{text-decoration:line-through}
.aqua{color:#00bfbf}
.aqua-background{background:#00fafa}
.black{color:#000}
.black-background{background:#000}
.blue{color:#0000bf}
.blue-background{background:#0000fa}
.fuchsia{color:#bf00bf}
.fuchsia-background{background:#fa00fa}
.gray{color:#606060}
.gray-background{background:#7d7d7d}
.green{color:#006000}
.green-background{background:#007d00}
.lime{color:#00bf00}
.lime-background{background:#00fa00}
.maroon{color:#600000}
.maroon-background{background:#7d0000}
.navy{color:#000060}
.navy-background{background:#00007d}
.olive{color:#606000}
.olive-background{background:#7d7d00}
.purple{color:#600060}
.purple-background{background:#7d007d}
.red{color:#bf0000}
.red-background{background:#fa0000}
.silver{color:#909090}
.silver-background{background:#bcbcbc}
.teal{color:#006060}
.teal-background{background:#007d7d}
.white{color:#bfbfbf}
.white-background{background:#fafafa}
.yellow{color:#bfbf00}
.yellow-background{background:#fafa00}
span.icon>.fa{cursor:default}
a span.icon>.fa{cursor:inherit}
.admonitionblock td.icon [class^="fa icon-"]{font-size:2.5em;text-shadow:1px 1px 2px rgba(0,0,0,.5);cursor:default}
.admonitionblock td.icon .icon-note::before{content:"\f05a";color:#19407c}
.admonitionblock td.icon .icon-tip::before{content:"\f0eb";text-shadow:1px 1px 2px rgba(155,155,0,.8);color:#111}
.admonitionblock td.icon .icon-warning::before{content:"\f071";color:#bf6900}
.admonitionblock td.icon .icon-caution::before{content:"\f06d";color:#bf3400}
.admonitionblock td.icon .icon-important::before{content:"\f06a";color:#bf0000}
.conum[data-value]{display:inline-block;color:#fff!important;background:rgba(0,0,0,.8);border-radius:50%;text-align:center;font-size:.75em;width:1.67em;height:1.67em;line-height:1.67em;font-family:"Open Sans","DejaVu Sans",sans-serif;font-style:normal;font-weight:bold}
.conum[data-value] *{color:#fff!important}
.conum[data-value]+b{display:none}
.conum[data-value]::after{content:attr(data-value)}
pre .conum[data-value]{position:relative;top:-.125em}
b.conum *{color:inherit!important}
.conum:not([data-value]):empty{display:none}
dt,th.tableblock,td.content,div.footnote{text-rendering:optimizeLegibility}
h1,h2,p,td.content,span.alt,summary{letter-spacing:-.01em}
p strong,td.content strong,div.footnote strong{letter-spacing:-.005em}
p,blockquote,dt,td.content,span.alt,summary{font-size:1.0625rem}
p{margin-bottom:1.25rem}
.sidebarblock p,.sidebarblock dt,.sidebarblock td.content,p.tableblock{font-size:1em}
.exampleblock>.content{background:#fffef7;border-color:#e0e0dc;box-shadow:0 1px 4px #e0e0dc}
.print-only{display:none!important}
@page{margin:1.25cm .75cm}
@media print{*{box-shadow:none!important;text-shadow:none!important}
html{font-size:80%}
a{color:inherit!important;text-decoration:underline!important}
a.bare,a[href^="#"],a[href^="mailto:"]{text-decoration:none!important}
a[href^="http:"]:not(.bare)::after,a[href^="https:"]:not(.bare)::after{content:"(" attr(href) ")";display:inline-block;font-size:.875em;padding-left:.25em}
abbr[title]{border-bottom:1px dotted}
abbr[title]::after{content:" (" attr(title) ")"}
pre,blockquote,tr,img,object,svg{page-break-inside:avoid}
thead{display:table-header-group}
svg{max-width:100%}
p,blockquote,dt,td.content{font-size:1em;orphans:3;widows:3}
h2,h3,#toctitle,.sidebarblock>.content>.title{page-break-after:avoid}
#header,#content,#footnotes,#footer{max-width:none}
#toc,.sidebarblock,.exampleblock>.content{background:none!important}
#toc{border-bottom:1px solid #dddddf!important;padding-bottom:0!important}
body.book #header{text-align:center}
body.book #header>h1:first-child{border:0!important;margin:2.5em 0 1em}
body.book #header .details{border:0!important;display:block;padding:0!important}
body.book #header .details span:first-child{margin-left:0!important}
body.book #header .details br{display:block}
body.book #header .details br+span::before{content:none!important}
body.book #toc{border:0!important;text-align:left!important;padding:0!important;margin:0!important}
body.book #toc,body.book #preamble,body.book h1.sect0,body.book .sect1>h2{page-break-before:always}
.listingblock code[data-lang]::before{display:block}
#footer{padding:0 .9375em}
.hide-on-print{display:none!important}
.print-only{display:block!important}
.hide-for-print{display:none!important}
.show-for-print{display:inherit!important}}
@media amzn-kf8,print{#header>h1:first-child{margin-top:1.25rem}
.sect1{padding:0!important}
.sect1+.sect1{border:0}
#footer{background:none}
#footer-text{color:rgba(0,0,0,.6);font-size:.9em}}
@media amzn-kf8{#header,#content,#footnotes,#footer{padding:0}}
</style>
</head>
<body class="article">
<div id="header">
</div>
<div id="content">
<div class="sect1">
<h2 id="_qqq_tables">QQQ Tables</h2>
<div class="sectionbody">
<div class="paragraph">
<p>The core type of object in a QQQ Instance is the Table.
In the most common use-case, a QQQ Table may be the in-app representation of a Database table.
That is, it is a collection of records (or rows) of data, each of which has a set of fields (or columns).</p>
</div>
<div class="paragraph">
<p>QQQ also allows other types of data sources (<a href="Backends{relfilesuffix}">QQQ Backends</a>) to be used as tables, such as File systems, API&#8217;s, Java enums or objects, etc.
All of these backend types present the same interfaces (both user-interfaces, and application programming interfaces), regardless of their backend type.</p>
</div>
<div class="sect2">
<h3 id="_qtablemetadata">QTableMetaData</h3>
<div class="paragraph">
<p>Tables are defined in a QQQ Instance in a <code><strong>QTableMetaData</strong></code> object.
All tables must reference a <a href="Backends{relfilesuffix}">QQQ Backend</a>, a list of fields that define the shape of records in the table, and additional data to describe how to work with the table within its backend.</p>
</div>
<div class="paragraph">
<p><strong>QTableMetaData Properties:</strong></p>
</div>
<div class="ulist">
<ul>
<li>
<p><code>name</code> - <strong>String, Required</strong> - Unique name for the table within the QQQ Instance.</p>
</li>
<li>
<p><code>label</code> - <strong>String</strong> - User-facing label for the table, presented in User Interfaces.
Inferred from <code>name</code> if not set.</p>
</li>
<li>
<p><code>backendName</code> - <strong>String, Required</strong> - Name of a <a href="Backends{relfilesuffix}">QQQ Backend</a> in which this table&#8217;s data is managed.</p>
</li>
<li>
<p><code>fields</code> - <strong>Map of String → <a href="Fields{relfilesuffix}">QQQ Field</a>, Required</strong> - The columns of data that make up all records in this table.</p>
</li>
<li>
<p><code>primaryKeyField</code> - <strong>String, Conditional</strong> - Name of a <a href="Fields{relfilesuffix}">QQQ Field</a> that serves as the primary key (e.g., unique identifier) for records in this table.</p>
</li>
<li>
<p><code>uniqueKeys</code> - <strong>List of UniqueKey</strong> - Definition of additional unique constraints (from an RDBMS point of view) from the table.
e.g., sets of columns which must have unique values for each record in the table.</p>
</li>
<li>
<p><code>backendDetails</code> - <strong>QTableBackendDetails or subclass</strong> - Additional data to configure the table within its <a href="Backends{relfilesuffix}">QQQ Backend</a>.</p>
</li>
<li>
<p><code>automationDetails</code> - <strong>QTableAutomationDetails</strong> - Configuration of automated jobs that run against records in the table, e.g., upon insert or update.</p>
</li>
<li>
<p><code>customizers</code> - <strong>Map of String → QCodeReference</strong> - References to custom code that are injected into standard table actions, that allow applications to customize certain parts of how the table works.</p>
</li>
<li>
<p><code>parentAppName</code> - <strong>String</strong> - Name of a <a href="Apps{relfilesuffix}">QQQ App</a> that this table exists within.</p>
</li>
<li>
<p><code>icon</code> - <strong>QIcon</strong> - Icon associated with this table in certain user interfaces.</p>
</li>
<li>
<p><code>recordLabelFormat</code> - <strong>String</strong> - Java Format String, used with <code>recordLabelFields</code> to produce a label shown for records from the table.</p>
</li>
<li>
<p><code>recordLabelFields</code> - <strong>List of String, Conditional</strong> - Used with <code>recordLabelFormat</code> to provide values for any format specifiers in the format string.
These strings must be field names within the table.</p>
<div class="ulist">
<ul>
<li>
<p>Example of using <code>recordLabelFormat</code> and <code>recordLabelFields</code>:</p>
</li>
</ul>
</div>
</li>
</ul>
</div>
<div class="listingblock">
<div class="content">
<pre class="CodeRay highlight"><code data-lang="java"><span style="color:#777">// given these fields in the table:</span>
<span style="color:#080;font-weight:bold">new</span> QFieldMetaData(<span style="background-color:hsla(0,100%,50%,0.05)"><span style="color:#710">&quot;</span><span style="color:#D20">name</span><span style="color:#710">&quot;</span></span>, QFieldType.STRING)
<span style="color:#080;font-weight:bold">new</span> QFieldMetaData(<span style="background-color:hsla(0,100%,50%,0.05)"><span style="color:#710">&quot;</span><span style="color:#D20">birthDate</span><span style="color:#710">&quot;</span></span>, QFieldType.DATE)
<span style="color:#777">// We can produce a record label such as &quot;Darin Kelkhoff (1980-05-31)&quot; via:</span>
.withRecordLabelFormat(<span style="background-color:hsla(0,100%,50%,0.05)"><span style="color:#710">&quot;</span><span style="color:#D20">%s (%s)</span><span style="color:#710">&quot;</span></span>)
.withRecordLabelFields(<span style="color:#0a8;font-weight:bold">List</span>.of(<span style="background-color:hsla(0,100%,50%,0.05)"><span style="color:#710">&quot;</span><span style="color:#D20">name</span><span style="color:#710">&quot;</span></span>, <span style="background-color:hsla(0,100%,50%,0.05)"><span style="color:#710">&quot;</span><span style="color:#D20">birthDate</span><span style="color:#710">&quot;</span></span>))</code></pre>
</div>
</div>
<div class="ulist">
<ul>
<li>
<p><code>sections</code> - <strong>List of QFieldSection</strong> - Mechanism to organize fields within user interfaces, into logical sections.
If any sections are present in the table meta data, then all fields in the table must be listed in exactly 1 section.
If no sections are defined, then instance enrichment will define default sections.</p>
</li>
<li>
<p><code>associatedScripts</code> - <strong>List of AssociatedScript</strong> - Definition of user-defined scripts that can be associated with records within the table.</p>
</li>
<li>
<p><code>enabledCapabilities</code> and <code>disabledCapabilities</code> - <strong>Set of Capability enum values</strong> - Overrides from the backend level, for capabilities that this table does or does not possess.</p>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<div id="footer">
<div id="footer-text">
Last updated 2022-11-21 09:02:56 -0600
</div>
</div>
</body>
</html>

View File

@ -1,13 +0,0 @@
ifdef::env-name[:relfilesuffix: .adoc]
:link-backend: link:Backends{relfilesuffix}[QQQ Backend]
:link-backends: link:Backends{relfilesuffix}[QQQ Backends]
:link-table: link:Tables{relfilesuffix}[QQQ Table]
:link-tables: link:Tables{relfilesuffix}[QQQ Tables]
:link-join: link:Joins{relfilesuffix}[QQQ Join]
:link-joins: link:Joins{relfilesuffix}[QQQ Joins]
:link-field: link:Fields{relfilesuffix}[QQQ Field]
:link-fields: link:Fields{relfilesuffix}[QQQ Fields]
:link-process: link:Processes{relfilesuffix}[QQQ Process]
:link-processes: link:Processes{relfilesuffix}[QQQ Processes]
:link-app: link:Apps{relfilesuffix}[QQQ App]
:link-apps: link:Apps{relfilesuffix}[QQQ Apps]

View File

@ -44,7 +44,7 @@
</modules> </modules>
<properties> <properties>
<revision>0.20.0-SNAPSHOT</revision> <revision>0.16.0-SNAPSHOT</revision>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>

View File

@ -84,7 +84,7 @@
<dependency> <dependency>
<groupId>org.json</groupId> <groupId>org.json</groupId>
<artifactId>json</artifactId> <artifactId>json</artifactId>
<version>20231013</version> <version>20230227</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.apache.commons</groupId> <groupId>org.apache.commons</groupId>

View File

@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.core.actions;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executors;
import java.util.concurrent.Future; import java.util.concurrent.Future;
import com.kingsrook.qqq.backend.core.context.CapturedContext; import com.kingsrook.qqq.backend.core.context.CapturedContext;
import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.context.QContext;
@ -53,7 +54,7 @@ public abstract class AbstractQActionBiConsumer<I extends AbstractActionInput, O
{ {
CapturedContext capturedContext = QContext.capture(); CapturedContext capturedContext = QContext.capture();
CompletableFuture<Void> completableFuture = new CompletableFuture<>(); CompletableFuture<Void> completableFuture = new CompletableFuture<>();
ActionHelper.getExecutorService().submit(() -> Executors.newCachedThreadPool().submit(() ->
{ {
try try
{ {

View File

@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.core.actions;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executors;
import java.util.concurrent.Future; import java.util.concurrent.Future;
import com.kingsrook.qqq.backend.core.context.CapturedContext; import com.kingsrook.qqq.backend.core.context.CapturedContext;
import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.context.QContext;
@ -53,7 +54,7 @@ public abstract class AbstractQActionFunction<I extends AbstractActionInput, O e
{ {
CapturedContext capturedContext = QContext.capture(); CapturedContext capturedContext = QContext.capture();
CompletableFuture<O> completableFuture = new CompletableFuture<>(); CompletableFuture<O> completableFuture = new CompletableFuture<>();
ActionHelper.getExecutorService().submit(() -> Executors.newCachedThreadPool().submit(() ->
{ {
try try
{ {

View File

@ -24,10 +24,6 @@ package com.kingsrook.qqq.backend.core.actions;
import java.io.Serializable; import java.io.Serializable;
import java.util.List; import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.function.Function; import java.util.function.Function;
import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QAuthenticationException; import com.kingsrook.qqq.backend.core.exceptions.QAuthenticationException;
@ -44,20 +40,6 @@ import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModu
*******************************************************************************/ *******************************************************************************/
public class ActionHelper public class ActionHelper
{ {
/////////////////////////////////////////////////////////////////////////////
// we would probably use Executors.newCachedThreadPool() - but - it has no //
// maxPoolSize... we think some limit is good, so that at a large number //
// of attempted concurrent jobs we'll have new jobs block, rather than //
// exhausting all server resources and locking up "everything" //
// also, it seems like keeping a handful of core-threads around is very //
// little actual waste, and better than ever wasting time starting a new //
// one, which we know we'll often be doing. //
/////////////////////////////////////////////////////////////////////////////
private static Integer CORE_THREADS = 8;
private static Integer MAX_THREADS = 500;
private static ExecutorService executorService = new ThreadPoolExecutor(CORE_THREADS, MAX_THREADS, 60L, TimeUnit.SECONDS, new SynchronousQueue<>());
/******************************************************************************* /*******************************************************************************
** **
@ -87,17 +69,6 @@ public class ActionHelper
/*******************************************************************************
** access an executor service for sharing among the executeAsync methods of all
** actions.
*******************************************************************************/
static ExecutorService getExecutorService()
{
return (executorService);
}
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/

View File

@ -28,9 +28,6 @@ import java.util.UUID;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException; import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeoutException;
import com.kingsrook.qqq.backend.core.context.CapturedContext; import com.kingsrook.qqq.backend.core.context.CapturedContext;
@ -54,24 +51,9 @@ public class AsyncJobManager
{ {
private static final QLogger LOG = QLogger.getLogger(AsyncJobManager.class); private static final QLogger LOG = QLogger.getLogger(AsyncJobManager.class);
/////////////////////////////////////////////////////////////////////////////
// we would probably use Executors.newCachedThreadPool() - but - it has no //
// maxPoolSize... we think some limit is good, so that at a large number //
// of attempted concurrent jobs we'll have new jobs block, rather than //
// exhausting all server resources and locking up "everything" //
// also, it seems like keeping a handful of core-threads around is very //
// little actual waste, and better than ever wasting time starting a new //
// one, which we know we'll often be doing. //
/////////////////////////////////////////////////////////////////////////////
private static Integer CORE_THREADS = 8;
private static Integer MAX_THREADS = 500;
private static ExecutorService executorService = new ThreadPoolExecutor(CORE_THREADS, MAX_THREADS, 60L, TimeUnit.SECONDS, new SynchronousQueue<>());
private String forcedJobUUID = null; private String forcedJobUUID = null;
/******************************************************************************* /*******************************************************************************
** Start a job - if it finishes within the specified timeout, get its results, ** Start a job - if it finishes within the specified timeout, get its results,
** else, get back an exception with the job id. ** else, get back an exception with the job id.
@ -102,7 +84,7 @@ public class AsyncJobManager
{ {
QContext.init(capturedContext); QContext.init(capturedContext);
return (runAsyncJob(jobName, asyncJob, uuidAndTypeStateKey, asyncJobStatus)); return (runAsyncJob(jobName, asyncJob, uuidAndTypeStateKey, asyncJobStatus));
}, executorService); });
if(timeout == 0) if(timeout == 0)
{ {

View File

@ -25,7 +25,6 @@ package com.kingsrook.qqq.backend.core.actions.async;
import java.io.Serializable; import java.io.Serializable;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import com.kingsrook.qqq.backend.core.actions.reporting.BufferedRecordPipe;
import com.kingsrook.qqq.backend.core.actions.reporting.RecordPipe; import com.kingsrook.qqq.backend.core.actions.reporting.RecordPipe;
import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger; import com.kingsrook.qqq.backend.core.logging.QLogger;
@ -143,11 +142,6 @@ public class AsyncRecordPipeLoop
jobState = asyncJobStatus.getState(); jobState = asyncJobStatus.getState();
} }
if(recordPipe instanceof BufferedRecordPipe bufferedRecordPipe)
{
bufferedRecordPipe.finalFlush();
}
LOG.debug("Job [" + jobUUID + "][" + jobName + "] completed with status: " + asyncJobStatus); LOG.debug("Job [" + jobUUID + "][" + jobName + "] completed with status: " + asyncJobStatus);
/////////////////////////////////// ///////////////////////////////////

View File

@ -29,7 +29,6 @@ import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Optional;
import com.kingsrook.qqq.backend.core.actions.AbstractQActionFunction; import com.kingsrook.qqq.backend.core.actions.AbstractQActionFunction;
import com.kingsrook.qqq.backend.core.actions.tables.GetAction; import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
@ -43,14 +42,13 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput; import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput; import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput;
import com.kingsrook.qqq.backend.core.model.audits.AuditsMetaDataProvider;
import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock; import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLockFilters;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.session.QUser; import com.kingsrook.qqq.backend.core.model.session.QUser;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.Pair; import com.kingsrook.qqq.backend.core.utils.Pair;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/******************************************************************************* /*******************************************************************************
@ -110,26 +108,12 @@ public class AuditAction extends AbstractQActionFunction<AuditInput, AuditOutput
/*******************************************************************************
** Simple overload that internally figures out primary key and security key values
**
** Be aware - if the record doesn't have its security key values set (say it's a
** partial record as part of an update), then those values won't be in the
** security key map... This should probably be considered a bug.
*******************************************************************************/
public static void appendToInput(AuditInput auditInput, QTableMetaData table, QRecord record, String auditMessage)
{
appendToInput(auditInput, table.getName(), record.getValueInteger(table.getPrimaryKeyField()), getRecordSecurityKeyValues(table, record, Optional.empty()), auditMessage);
}
/******************************************************************************* /*******************************************************************************
** Add 1 auditSingleInput to an AuditInput object - with no details (child records). ** Add 1 auditSingleInput to an AuditInput object - with no details (child records).
*******************************************************************************/ *******************************************************************************/
public static void appendToInput(AuditInput auditInput, String tableName, Integer recordId, Map<String, Serializable> securityKeyValues, String message) public static AuditInput appendToInput(AuditInput auditInput, String tableName, Integer recordId, Map<String, Serializable> securityKeyValues, String message)
{ {
appendToInput(auditInput, tableName, recordId, securityKeyValues, message, null); return (appendToInput(auditInput, tableName, recordId, securityKeyValues, message, null));
} }
@ -155,44 +139,6 @@ public class AuditAction extends AbstractQActionFunction<AuditInput, AuditOutput
/*******************************************************************************
** For a given record, from a given table, build a map of the record's security
** key values.
**
** If, in case, the record has null value(s), and the oldRecord is given (e.g.,
** for the case of an update, where the record may not have all fields set, and
** oldRecord should be known for doing field-diffs), then try to get the value(s)
** from oldRecord.
**
** Currently, will leave values null if they aren't found after that.
**
** An alternative could be to re-fetch the record from its source if needed...
*******************************************************************************/
public static Map<String, Serializable> getRecordSecurityKeyValues(QTableMetaData table, QRecord record, Optional<QRecord> oldRecord)
{
Map<String, Serializable> securityKeyValues = new HashMap<>();
for(RecordSecurityLock recordSecurityLock : RecordSecurityLockFilters.filterForReadLocks(CollectionUtils.nonNullList(table.getRecordSecurityLocks())))
{
Serializable keyValue = record == null ? null : record.getValue(recordSecurityLock.getFieldName());
if(keyValue == null && oldRecord.isPresent())
{
LOG.debug("Table with a securityLock, but value not found in field", logPair("table", table.getName()), logPair("field", recordSecurityLock.getFieldName()));
keyValue = oldRecord.get().getValue(recordSecurityLock.getFieldName());
}
if(keyValue == null)
{
LOG.debug("Table with a securityLock, but value not found in field", logPair("table", table.getName()), logPair("field", recordSecurityLock.getFieldName()), logPair("oldRecordIsPresent", oldRecord.isPresent()));
}
securityKeyValues.put(recordSecurityLock.getSecurityKeyType(), keyValue);
}
return securityKeyValues;
}
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
@ -221,7 +167,7 @@ public class AuditAction extends AbstractQActionFunction<AuditInput, AuditOutput
/////////////////////////////////////////////////// ///////////////////////////////////////////////////
// validate security keys on the table are given // // validate security keys on the table are given //
/////////////////////////////////////////////////// ///////////////////////////////////////////////////
for(RecordSecurityLock recordSecurityLock : RecordSecurityLockFilters.filterForReadLocks(CollectionUtils.nonNullList(table.getRecordSecurityLocks()))) for(RecordSecurityLock recordSecurityLock : CollectionUtils.nonNullList(table.getRecordSecurityLocks()))
{ {
if(auditSingleInput.getSecurityKeyValues() == null || !auditSingleInput.getSecurityKeyValues().containsKey(recordSecurityLock.getSecurityKeyType())) if(auditSingleInput.getSecurityKeyValues() == null || !auditSingleInput.getSecurityKeyValues().containsKey(recordSecurityLock.getSecurityKeyType()))
{ {
@ -232,8 +178,8 @@ public class AuditAction extends AbstractQActionFunction<AuditInput, AuditOutput
//////////////////////////////////////////////// ////////////////////////////////////////////////
// map names to ids and handle default values // // map names to ids and handle default values //
//////////////////////////////////////////////// ////////////////////////////////////////////////
Integer auditTableId = getIdForName("auditTable", auditSingleInput.getAuditTableName()); Integer auditTableId = getIdForName(AuditsMetaDataProvider.TABLE_NAME_AUDIT_TABLE, auditSingleInput.getAuditTableName());
Integer auditUserId = getIdForName("auditUser", Objects.requireNonNullElse(auditSingleInput.getAuditUserName(), getSessionUserName())); Integer auditUserId = getIdForName(AuditsMetaDataProvider.TABLE_NAME_AUDIT_USER, Objects.requireNonNullElse(auditSingleInput.getAuditUserName(), getSessionUserName()));
Instant timestamp = Objects.requireNonNullElse(auditSingleInput.getTimestamp(), Instant.now()); Instant timestamp = Objects.requireNonNullElse(auditSingleInput.getTimestamp(), Instant.now());
////////////////// //////////////////
@ -322,7 +268,7 @@ public class AuditAction extends AbstractQActionFunction<AuditInput, AuditOutput
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
private Integer getIdForName(String tableName, String nameValue) throws QException Integer getIdForName(String tableName, String nameValue) throws QException
{ {
Pair<String, String> key = new Pair<>(tableName, nameValue); Pair<String, String> key = new Pair<>(tableName, nameValue);
if(!cachedFetches.containsKey(key)) if(!cachedFetches.containsKey(key))
@ -342,7 +288,7 @@ public class AuditAction extends AbstractQActionFunction<AuditInput, AuditOutput
insertInput.setTableName(tableName); insertInput.setTableName(tableName);
QRecord record = new QRecord().withValue("name", nameValue); QRecord record = new QRecord().withValue("name", nameValue);
if(tableName.equals("auditTable")) if(tableName.equals(AuditsMetaDataProvider.TABLE_NAME_AUDIT_TABLE))
{ {
QTableMetaData table = QContext.getQInstance().getTable(nameValue); QTableMetaData table = QContext.getQInstance().getTable(nameValue);
if(table != null) if(table != null)

View File

@ -23,10 +23,8 @@ package com.kingsrook.qqq.backend.core.actions.audits;
import java.io.Serializable; import java.io.Serializable;
import java.math.BigDecimal;
import java.time.Instant; import java.time.Instant;
import java.time.ZoneId; import java.time.ZoneId;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Comparator; import java.util.Comparator;
import java.util.HashMap; import java.util.HashMap;
@ -35,6 +33,7 @@ import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import com.kingsrook.qqq.backend.core.actions.AbstractQActionFunction; import com.kingsrook.qqq.backend.core.actions.AbstractQActionFunction;
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
import com.kingsrook.qqq.backend.core.actions.values.QPossibleValueTranslator; import com.kingsrook.qqq.backend.core.actions.values.QPossibleValueTranslator;
import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter; import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter;
import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.context.QContext;
@ -48,18 +47,20 @@ import com.kingsrook.qqq.backend.core.model.actions.audits.DMLAuditOutput;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput; import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput; 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.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput; import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
import com.kingsrook.qqq.backend.core.model.audits.AuditsMetaDataProvider;
import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.audits.AuditLevel; import com.kingsrook.qqq.backend.core.model.metadata.audits.AuditLevel;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.session.QSession; import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils; import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils; import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import static com.kingsrook.qqq.backend.core.actions.audits.AuditAction.getRecordSecurityKeyValues;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
@ -71,8 +72,6 @@ public class DMLAuditAction extends AbstractQActionFunction<DMLAuditInput, DMLAu
{ {
private static final QLogger LOG = QLogger.getLogger(DMLAuditAction.class); private static final QLogger LOG = QLogger.getLogger(DMLAuditAction.class);
public static final String AUDIT_CONTEXT_FIELD_NAME = "auditContext";
/******************************************************************************* /*******************************************************************************
@ -93,7 +92,7 @@ public class DMLAuditAction extends AbstractQActionFunction<DMLAuditInput, DMLAu
List<QRecord> recordList = CollectionUtils.nonNullList(input.getRecordList()).stream() List<QRecord> recordList = CollectionUtils.nonNullList(input.getRecordList()).stream()
.filter(r -> CollectionUtils.nullSafeIsEmpty(r.getErrors())).toList(); .filter(r -> CollectionUtils.nullSafeIsEmpty(r.getErrors())).toList();
AuditLevel auditLevel = getAuditLevel(tableActionInput); AuditLevel auditLevel = getAuditLevel(table);
if(auditLevel == null || auditLevel.equals(AuditLevel.NONE) || CollectionUtils.nullSafeIsEmpty(recordList)) if(auditLevel == null || auditLevel.equals(AuditLevel.NONE) || CollectionUtils.nullSafeIsEmpty(recordList))
{ {
///////////////////////////////////////////// /////////////////////////////////////////////
@ -102,7 +101,34 @@ public class DMLAuditAction extends AbstractQActionFunction<DMLAuditInput, DMLAu
return (output); return (output);
} }
String contextSuffix = getContentSuffix(input); String contextSuffix = "";
if(StringUtils.hasContent(input.getAuditContext()))
{
contextSuffix = " " + input.getAuditContext();
}
Optional<AbstractActionInput> actionInput = QContext.getFirstActionInStack();
if(actionInput.isPresent() && actionInput.get() instanceof RunProcessInput runProcessInput)
{
String processName = runProcessInput.getProcessName();
QProcessMetaData process = QContext.getQInstance().getProcess(processName);
if(process != null)
{
contextSuffix = " during process: " + process.getLabel();
}
}
QSession qSession = QContext.getQSession();
String apiVersion = qSession.getValue("apiVersion");
if(apiVersion != null)
{
String apiLabel = qSession.getValue("apiLabel");
if(!StringUtils.hasContent(apiLabel))
{
apiLabel = "API";
}
contextSuffix += (" via " + apiLabel + " Version: " + apiVersion);
}
AuditInput auditInput = new AuditInput(); AuditInput auditInput = new AuditInput();
if(auditLevel.equals(AuditLevel.RECORD) || (auditLevel.equals(AuditLevel.FIELD) && !dmlType.supportsFields)) if(auditLevel.equals(AuditLevel.RECORD) || (auditLevel.equals(AuditLevel.FIELD) && !dmlType.supportsFields))
@ -113,7 +139,7 @@ public class DMLAuditAction extends AbstractQActionFunction<DMLAuditInput, DMLAu
//////////////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////////////
for(QRecord record : recordList) for(QRecord record : recordList)
{ {
AuditAction.appendToInput(auditInput, table.getName(), record.getValueInteger(table.getPrimaryKeyField()), getRecordSecurityKeyValues(table, record, Optional.empty()), "Record was " + dmlType.pastTenseVerb + contextSuffix); AuditAction.appendToInput(auditInput, table.getName(), record.getValueInteger(table.getPrimaryKeyField()), getRecordSecurityKeyValues(table, record), "Record was " + dmlType.pastTenseVerb + contextSuffix);
} }
} }
else if(auditLevel.equals(AuditLevel.FIELD)) else if(auditLevel.equals(AuditLevel.FIELD))
@ -123,7 +149,7 @@ public class DMLAuditAction extends AbstractQActionFunction<DMLAuditInput, DMLAu
/////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////
// do many audits, all with field level details, for FIELD level // // do many audits, all with field level details, for FIELD level //
/////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////
QPossibleValueTranslator qPossibleValueTranslator = new QPossibleValueTranslator(QContext.getQInstance(), QContext.getQSession()); QPossibleValueTranslator qPossibleValueTranslator = new QPossibleValueTranslator(QContext.getQInstance(), qSession);
qPossibleValueTranslator.translatePossibleValuesInRecords(table, CollectionUtils.mergeLists(recordList, oldRecordList)); qPossibleValueTranslator.translatePossibleValuesInRecords(table, CollectionUtils.mergeLists(recordList, oldRecordList));
////////////////////////////////////////// //////////////////////////////////////////
@ -145,8 +171,92 @@ public class DMLAuditAction extends AbstractQActionFunction<DMLAuditInput, DMLAu
List<QRecord> details = new ArrayList<>(); List<QRecord> details = new ArrayList<>();
for(String fieldName : sortedFieldNames) for(String fieldName : sortedFieldNames)
{ {
makeAuditDetailRecordForField(fieldName, table, dmlType, record, oldRecord) if(!record.getValues().containsKey(fieldName))
.ifPresent(details::add); {
////////////////////////////////////////////////////////////////////////////////////////////////
// if the stored record doesn't have this field name, then don't audit anything about it //
// this is to deal with our Patch style updates not looking like every field was cleared out. //
////////////////////////////////////////////////////////////////////////////////////////////////
continue;
}
if(fieldName.equals("modifyDate") || fieldName.equals("createDate") || fieldName.equals("automationStatus"))
{
continue;
}
QFieldMetaData field = table.getField(fieldName);
Serializable value = ValueUtils.getValueAsFieldType(field.getType(), record.getValue(fieldName));
Serializable oldValue = oldRecord == null ? null : ValueUtils.getValueAsFieldType(field.getType(), oldRecord.getValue(fieldName));
QRecord detailRecord = null;
if(oldRecord == null)
{
if(DMLType.INSERT.equals(dmlType) && value == null)
{
continue;
}
if(field.getType().equals(QFieldType.BLOB))
{
detailRecord = new QRecord().withValue("message", "Set " + field.getLabel());
}
else
{
String formattedValue = getFormattedValueForAuditDetail(record, fieldName, field, value);
detailRecord = new QRecord().withValue("message", "Set " + field.getLabel() + " to " + formattedValue);
detailRecord.withValue("newValue", formattedValue);
}
}
else
{
if(!Objects.equals(oldValue, value))
{
if(field.getType().equals(QFieldType.BLOB))
{
if(oldValue == null)
{
detailRecord = new QRecord().withValue("message", "Set " + field.getLabel());
}
else if(value == null)
{
detailRecord = new QRecord().withValue("message", "Removed " + field.getLabel());
}
else
{
detailRecord = new QRecord().withValue("message", "Changed " + field.getLabel());
}
}
else
{
String formattedValue = getFormattedValueForAuditDetail(record, fieldName, field, value);
String formattedOldValue = getFormattedValueForAuditDetail(oldRecord, fieldName, field, oldValue);
if(oldValue == null)
{
detailRecord = new QRecord().withValue("message", "Set " + field.getLabel() + " to " + formatFormattedValueForDetailMessage(field, formattedValue));
detailRecord.withValue("newValue", formattedValue);
}
else if(value == null)
{
detailRecord = new QRecord().withValue("message", "Removed " + formatFormattedValueForDetailMessage(field, formattedOldValue) + " from " + field.getLabel());
detailRecord.withValue("oldValue", formattedOldValue);
}
else
{
detailRecord = new QRecord().withValue("message", "Changed " + field.getLabel() + " from " + formatFormattedValueForDetailMessage(field, formattedOldValue) + " to " + formatFormattedValueForDetailMessage(field, formattedValue));
detailRecord.withValue("oldValue", formattedOldValue);
detailRecord.withValue("newValue", formattedValue);
}
}
}
}
if(detailRecord != null)
{
detailRecord.withValue("fieldName", fieldName);
details.add(detailRecord);
}
} }
if(details.isEmpty() && DMLType.UPDATE.equals(dmlType)) if(details.isEmpty() && DMLType.UPDATE.equals(dmlType))
@ -156,13 +266,60 @@ public class DMLAuditAction extends AbstractQActionFunction<DMLAuditInput, DMLAu
} }
else else
{ {
AuditAction.appendToInput(auditInput, table.getName(), record.getValueInteger(table.getPrimaryKeyField()), getRecordSecurityKeyValues(table, record, Optional.ofNullable(oldRecord)), "Record was " + dmlType.pastTenseVerb + contextSuffix, details); AuditAction.appendToInput(auditInput, table.getName(), record.getValueInteger(table.getPrimaryKeyField()), getRecordSecurityKeyValues(table, record), "Record was " + dmlType.pastTenseVerb + contextSuffix, details);
} }
} }
} }
// new AuditAction().executeAsync(auditInput); // todo async??? maybe get that from rules??? // new AuditAction().executeAsync(auditInput); // todo async??? maybe get that from rules???
new AuditAction().execute(auditInput); AuditAction auditAction = new AuditAction();
auditAction.execute(auditInput);
if(DMLType.INSERT.equals(dmlType))
{
if(getIsAuditTreeRoot(table))
{
/* not needed? */
/*
Integer auditTableId = auditAction.getIdForName("auditTable", table.getName());
List<QRecord> auditTrees = recordList.stream().map(r ->
new QRecord()
.withValue("rootAuditTableId", auditTableId)
.withValue("rootRecordId", r.getValueInteger(table.getPrimaryKeyField()))
).toList();
InsertInput insertInput = new InsertInput();
insertInput.setTableName("audit tree"); // todo - from entity
insertInput.setRecords(auditTrees);
InsertOutput insertOutput = new InsertAction().execute(insertInput);
*/
}
for(String auditTreeParentTableName : CollectionUtils.nonNullList(getAuditTreeParentTableNames(table)))
{
Integer rootAuditTableId = auditAction.getIdForName(AuditsMetaDataProvider.TABLE_NAME_AUDIT_TABLE, auditTreeParentTableName);
Integer nodeAuditTableId = auditAction.getIdForName(AuditsMetaDataProvider.TABLE_NAME_AUDIT_TABLE, table.getName());
List<QRecord> auditTreeNodes = new ArrayList<>();
for(QRecord record : recordList)
{
Serializable rootRecordId = record.getValue("orderId"); // todo - figure this out, from joins...
auditTreeNodes.add(new QRecord()
.withValue("rootAuditTableId", rootAuditTableId)
.withValue("rootRecordId", rootRecordId)
.withValue("nodeAuditTableId", nodeAuditTableId)
.withValue("nodeRecordId", record.getValue(table.getPrimaryKeyField()))
);
}
InsertInput insertInput = new InsertInput();
insertInput.setTableName(AuditsMetaDataProvider.TABLE_NAME_AUDIT_TREE);
insertInput.setRecords(auditTreeNodes);
InsertOutput insertOutput = new InsertAction().execute(insertInput);
}
}
long end = System.currentTimeMillis(); long end = System.currentTimeMillis();
LOG.trace("Audit performance", logPair("auditLevel", String.valueOf(auditLevel)), logPair("recordCount", recordList.size()), logPair("millis", (end - start))); LOG.trace("Audit performance", logPair("auditLevel", String.valueOf(auditLevel)), logPair("recordCount", recordList.size()), logPair("millis", (end - start)));
} }
@ -176,256 +333,6 @@ public class DMLAuditAction extends AbstractQActionFunction<DMLAuditInput, DMLAu
/*******************************************************************************
**
*******************************************************************************/
static String getContentSuffix(DMLAuditInput input)
{
StringBuilder contextSuffix = new StringBuilder();
/////////////////////////////////////////////////////////////////////////////
// start with context from the input wrapper //
// note, these contexts get propagated down from Input/Update/Delete Input //
/////////////////////////////////////////////////////////////////////////////
if(StringUtils.hasContent(input.getAuditContext()))
{
contextSuffix.append(" ").append(input.getAuditContext());
}
/////////////////////////////////////////////////////////////////////////////////////
// note process label (and a possible context from the process's state) if present //
/////////////////////////////////////////////////////////////////////////////////////
Optional<AbstractActionInput> actionInput = QContext.getFirstActionInStack();
if(actionInput.isPresent() && actionInput.get() instanceof RunProcessInput runProcessInput)
{
String processAuditContext = ValueUtils.getValueAsString(runProcessInput.getValue(AUDIT_CONTEXT_FIELD_NAME));
if(StringUtils.hasContent(processAuditContext))
{
contextSuffix.append(" ").append(processAuditContext);
}
String processName = runProcessInput.getProcessName();
QProcessMetaData process = QContext.getQInstance().getProcess(processName);
if(process != null)
{
contextSuffix.append(" during process: ").append(process.getLabel());
}
}
///////////////////////////////////////////////////
// use api label & version if present in session //
///////////////////////////////////////////////////
QSession qSession = QContext.getQSession();
String apiVersion = qSession.getValue("apiVersion");
if(apiVersion != null)
{
String apiLabel = qSession.getValue("apiLabel");
if(!StringUtils.hasContent(apiLabel))
{
apiLabel = "API";
}
contextSuffix.append(" via ").append(apiLabel).append(" Version: ").append(apiVersion);
}
return (contextSuffix.toString());
}
/*******************************************************************************
**
*******************************************************************************/
static Optional<QRecord> makeAuditDetailRecordForField(String fieldName, QTableMetaData table, DMLType dmlType, QRecord record, QRecord oldRecord)
{
if(!record.getValues().containsKey(fieldName))
{
////////////////////////////////////////////////////////////////////////////////////////////////
// if the stored record doesn't have this field name, then don't audit anything about it //
// this is to deal with our Patch style updates not looking like every field was cleared out. //
////////////////////////////////////////////////////////////////////////////////////////////////
return (Optional.empty());
}
if(fieldName.equals("modifyDate") || fieldName.equals("createDate") || fieldName.equals("automationStatus"))
{
return (Optional.empty());
}
QFieldMetaData field = table.getField(fieldName);
Serializable value = ValueUtils.getValueAsFieldType(field.getType(), record.getValue(fieldName));
Serializable oldValue = oldRecord == null ? null : ValueUtils.getValueAsFieldType(field.getType(), oldRecord.getValue(fieldName));
QRecord detailRecord = null;
if(oldRecord == null)
{
if(DMLType.INSERT.equals(dmlType) && value == null)
{
return (Optional.empty());
}
if(field.getType().equals(QFieldType.BLOB) || field.getType().needsMasked())
{
detailRecord = new QRecord().withValue("message", "Set " + field.getLabel());
}
else
{
String formattedValue = getFormattedValueForAuditDetail(record, fieldName, field, value);
detailRecord = new QRecord().withValue("message", "Set " + field.getLabel() + " to " + formattedValue);
detailRecord.withValue("newValue", formattedValue);
}
}
else
{
if(areValuesDifferentForAudit(field, value, oldValue))
{
if(field.getType().equals(QFieldType.BLOB) || field.getType().needsMasked())
{
if(oldValue == null)
{
detailRecord = new QRecord().withValue("message", "Set " + field.getLabel());
}
else if(value == null)
{
detailRecord = new QRecord().withValue("message", "Removed " + field.getLabel());
}
else
{
detailRecord = new QRecord().withValue("message", "Changed " + field.getLabel());
}
}
else
{
String formattedValue = getFormattedValueForAuditDetail(record, fieldName, field, value);
String formattedOldValue = getFormattedValueForAuditDetail(oldRecord, fieldName, field, oldValue);
if(oldValue == null)
{
detailRecord = new QRecord().withValue("message", "Set " + field.getLabel() + " to " + formatFormattedValueForDetailMessage(field, formattedValue));
detailRecord.withValue("newValue", formattedValue);
}
else if(value == null)
{
detailRecord = new QRecord().withValue("message", "Removed " + formatFormattedValueForDetailMessage(field, formattedOldValue) + " from " + field.getLabel());
detailRecord.withValue("oldValue", formattedOldValue);
}
else
{
detailRecord = new QRecord().withValue("message", "Changed " + field.getLabel() + " from " + formatFormattedValueForDetailMessage(field, formattedOldValue) + " to " + formatFormattedValueForDetailMessage(field, formattedValue));
detailRecord.withValue("oldValue", formattedOldValue);
detailRecord.withValue("newValue", formattedValue);
}
}
}
}
if(detailRecord != null)
{
////////////////////////////////////////////////////////////////////
// useful if doing dev in here - but overkill for any other time. //
////////////////////////////////////////////////////////////////////
// LOG.debug("Returning with message: " + detailRecord.getValueString("message"));
detailRecord.withValue("fieldName", fieldName);
return (Optional.of(detailRecord));
}
return (Optional.empty());
}
/*******************************************************************************
**
*******************************************************************************/
static boolean areValuesDifferentForAudit(QFieldMetaData field, Serializable value, Serializable oldValue)
{
try
{
///////////////////
// decimal rules //
///////////////////
if(field.getType().equals(QFieldType.DECIMAL))
{
BigDecimal newBD = ValueUtils.getValueAsBigDecimal(value);
BigDecimal oldBD = ValueUtils.getValueAsBigDecimal(oldValue);
if(newBD == null && oldBD == null)
{
return (false);
}
if(newBD == null || oldBD == null)
{
return (true);
}
return (newBD.compareTo(oldBD) != 0);
}
////////////////////
// dateTime rules //
////////////////////
if(field.getType().equals(QFieldType.DATE_TIME))
{
Instant newI = ValueUtils.getValueAsInstant(value);
Instant oldI = ValueUtils.getValueAsInstant(oldValue);
if(newI == null && oldI == null)
{
return (false);
}
if(newI == null || oldI == null)
{
return (true);
}
////////////////////////////////
// just compare to the second //
////////////////////////////////
return (newI.truncatedTo(ChronoUnit.SECONDS).compareTo(oldI.truncatedTo(ChronoUnit.SECONDS)) != 0);
}
//////////////////
// string rules //
//////////////////
if(field.getType().isStringLike())
{
String newString = ValueUtils.getValueAsString(value);
String oldString = ValueUtils.getValueAsString(oldValue);
boolean newIsNullOrEmpty = !StringUtils.hasContent(newString);
boolean oldIsNullOrEmpty = !StringUtils.hasContent(oldString);
if(newIsNullOrEmpty && oldIsNullOrEmpty)
{
return (false);
}
if(newIsNullOrEmpty || oldIsNullOrEmpty)
{
return (true);
}
return (newString.compareTo(oldString) != 0);
}
/////////////////////////////////////
// default just use Objects.equals //
/////////////////////////////////////
return !Objects.equals(oldValue, value);
}
catch(Exception e)
{
LOG.debug("Error checking areValuesDifferentForAudit", e, logPair("fieldName", field.getName()), logPair("value", value), logPair("oldValue", oldValue));
}
////////////////////////////////////
// default to something simple... //
////////////////////////////////////
return !Objects.equals(oldValue, value);
}
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
@ -518,9 +425,23 @@ public class DMLAuditAction extends AbstractQActionFunction<DMLAuditInput, DMLAu
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
public static AuditLevel getAuditLevel(AbstractTableActionInput tableActionInput) private static Map<String, Serializable> getRecordSecurityKeyValues(QTableMetaData table, QRecord record)
{
Map<String, Serializable> securityKeyValues = new HashMap<>();
for(RecordSecurityLock recordSecurityLock : CollectionUtils.nonNullList(table.getRecordSecurityLocks()))
{
securityKeyValues.put(recordSecurityLock.getSecurityKeyType(), record == null ? null : record.getValue(recordSecurityLock.getFieldName()));
}
return securityKeyValues;
}
/*******************************************************************************
**
*******************************************************************************/
public static AuditLevel getAuditLevel(QTableMetaData table)
{ {
QTableMetaData table = tableActionInput.getTable();
if(table.getAuditRules() == null) if(table.getAuditRules() == null)
{ {
return (AuditLevel.NONE); return (AuditLevel.NONE);
@ -534,7 +455,37 @@ public class DMLAuditAction extends AbstractQActionFunction<DMLAuditInput, DMLAu
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
enum DMLType public static boolean getIsAuditTreeRoot(QTableMetaData table)
{
if(table.getAuditRules() == null)
{
return (false);
}
return (table.getAuditRules().getIsAuditTreeRoot());
}
/*******************************************************************************
**
*******************************************************************************/
public static List<String> getAuditTreeParentTableNames(QTableMetaData table)
{
if(table.getAuditRules() == null)
{
return (null);
}
return (table.getAuditRules().getAuditTreeParentTableNames());
}
/*******************************************************************************
**
*******************************************************************************/
private enum DMLType
{ {
INSERT("Inserted", true), INSERT("Inserted", true),
UPDATE("Edited", true), UPDATE("Edited", true),

View File

@ -22,8 +22,9 @@
package com.kingsrook.qqq.backend.core.actions.automation; package com.kingsrook.qqq.backend.core.actions.automation;
import java.util.Collections; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Objects;
import com.kingsrook.qqq.backend.core.actions.tables.CountAction; import com.kingsrook.qqq.backend.core.actions.tables.CountAction;
import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction; import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction;
import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.context.QContext;
@ -89,10 +90,6 @@ public class RecordAutomationStatusUpdater
} }
} }
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Avoid setting records to PENDING_INSERT or PENDING_UPDATE even if they don't have any insert or update automations or triggers //
// such records should go straight to OK status. //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
if(canWeSkipPendingAndGoToOkay(table, automationStatus)) if(canWeSkipPendingAndGoToOkay(table, automationStatus))
{ {
automationStatus = AutomationStatus.OK; automationStatus = AutomationStatus.OK;
@ -124,13 +121,9 @@ public class RecordAutomationStatusUpdater
** being asked to set status to PENDING_INSERT (or PENDING_UPDATE), then just ** being asked to set status to PENDING_INSERT (or PENDING_UPDATE), then just
** move the status straight to OK. ** move the status straight to OK.
*******************************************************************************/ *******************************************************************************/
static boolean canWeSkipPendingAndGoToOkay(QTableMetaData table, AutomationStatus automationStatus) private static boolean canWeSkipPendingAndGoToOkay(QTableMetaData table, AutomationStatus automationStatus)
{ {
List<TableAutomationAction> tableActions = Collections.emptyList(); List<TableAutomationAction> tableActions = Objects.requireNonNullElse(table.getAutomationDetails().getActions(), new ArrayList<>());
if(table.getAutomationDetails() != null && table.getAutomationDetails().getActions() != null)
{
tableActions = table.getAutomationDetails().getActions();
}
if(automationStatus.equals(AutomationStatus.PENDING_INSERT_AUTOMATIONS)) if(automationStatus.equals(AutomationStatus.PENDING_INSERT_AUTOMATIONS))
{ {
@ -142,12 +135,6 @@ public class RecordAutomationStatusUpdater
{ {
return (false); return (false);
} }
////////////////////////////////////////////////////////////////////////////////////////
// if we're going to pending-insert, and there are no insert automations or triggers, //
// then we may skip pending and go to okay. //
////////////////////////////////////////////////////////////////////////////////////////
return (true);
} }
else if(automationStatus.equals(AutomationStatus.PENDING_UPDATE_AUTOMATIONS)) else if(automationStatus.equals(AutomationStatus.PENDING_UPDATE_AUTOMATIONS))
{ {
@ -159,21 +146,9 @@ public class RecordAutomationStatusUpdater
{ {
return (false); return (false);
} }
}
//////////////////////////////////////////////////////////////////////////////////////// return (true);
// if we're going to pending-update, and there are no insert automations or triggers, //
// then we may skip pending and go to okay. //
////////////////////////////////////////////////////////////////////////////////////////
return (true);
}
else
{
///////////////////////////////////////////////////////////////////////////////////////////////////////
// if we're going to any other automation status - then we may never "skip pending" and go to okay - //
// because we weren't asked to go to pending! //
///////////////////////////////////////////////////////////////////////////////////////////////////////
return (false);
}
} }

View File

@ -42,7 +42,6 @@ import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.code.AdHocScriptCodeReference; import com.kingsrook.qqq.backend.core.model.metadata.code.AdHocScriptCodeReference;
import com.kingsrook.qqq.backend.core.model.scripts.Script; import com.kingsrook.qqq.backend.core.model.scripts.Script;
import com.kingsrook.qqq.backend.core.model.scripts.ScriptRevision; import com.kingsrook.qqq.backend.core.model.scripts.ScriptRevision;
import com.kingsrook.qqq.backend.core.model.scripts.ScriptsMetaDataProvider;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils; import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
@ -75,7 +74,7 @@ public class RunRecordScriptAutomationHandler extends RecordAutomationHandler
QueryInput queryInput = new QueryInput(); QueryInput queryInput = new QueryInput();
queryInput.setTableName(ScriptRevision.TABLE_NAME); queryInput.setTableName(ScriptRevision.TABLE_NAME);
queryInput.setFilter(new QQueryFilter(new QFilterCriteria("scriptId", QCriteriaOperator.EQUALS, scriptId))); queryInput.setFilter(new QQueryFilter(new QFilterCriteria("scriptId", QCriteriaOperator.EQUALS, scriptId)));
queryInput.withQueryJoin(new QueryJoin(Script.TABLE_NAME).withBaseTableOrAlias(ScriptRevision.TABLE_NAME).withJoinMetaData(QContext.getQInstance().getJoin(ScriptsMetaDataProvider.CURRENT_SCRIPT_REVISION_JOIN_NAME))); queryInput.withQueryJoin(new QueryJoin(Script.TABLE_NAME).withBaseTableOrAlias(ScriptRevision.TABLE_NAME).withJoinMetaData(QContext.getQInstance().getJoin("currentScriptRevision")));
QueryOutput queryOutput = new QueryAction().execute(queryInput); QueryOutput queryOutput = new QueryAction().execute(queryInput);
if(CollectionUtils.nullSafeIsEmpty(queryOutput.getRecords())) if(CollectionUtils.nullSafeIsEmpty(queryOutput.getRecords()))
{ {

View File

@ -342,9 +342,22 @@ public class PollingAutomationPerTableRunner implements Runnable
boolean anyActionsFailed = false; boolean anyActionsFailed = false;
for(TableAutomationAction action : actions) for(TableAutomationAction action : actions)
{ {
boolean hadError = applyActionToRecords(table, records, action); try
if(hadError)
{ {
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
// 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);
if(CollectionUtils.nullSafeHasContents(matchingQRecords))
{
LOG.debug(" Processing " + matchingQRecords.size() + " records in " + table + " for action " + action);
applyActionToMatchingRecords(table, matchingQRecords, action);
}
}
catch(Exception e)
{
LOG.warn("Caught exception processing records on " + table + " for action " + action, e);
anyActionsFailed = true; anyActionsFailed = true;
} }
} }
@ -364,37 +377,6 @@ public class PollingAutomationPerTableRunner implements Runnable
/*******************************************************************************
** Run one action over a list of records (if they match the action's filter).
**
** @return hadError - true if an exception was caught; false if all OK.
*******************************************************************************/
protected boolean applyActionToRecords(QTableMetaData table, List<QRecord> records, TableAutomationAction action)
{
try
{
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
// 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);
if(CollectionUtils.nullSafeHasContents(matchingQRecords))
{
LOG.debug(" Processing " + matchingQRecords.size() + " records in " + table + " for action " + action);
applyActionToMatchingRecords(table, matchingQRecords, action);
}
return (false);
}
catch(Exception e)
{
LOG.warn("Caught exception processing records on " + table + " for action " + action, e);
return (true);
}
}
/******************************************************************************* /*******************************************************************************
** For a given action, and a list of records - return a new list, of the ones ** For a given action, and a list of records - return a new list, of the ones
** which match the action's filter (if there is one - if not, then all match). ** which match the action's filter (if there is one - if not, then all match).

View File

@ -55,21 +55,6 @@ public abstract class AbstractPreInsertCustomizer
/////////////////////////////////////////////////////////////////////////////////
// allow the customizer to specify when it should be executed as part of the //
// insert action. default (per method in this class) is AFTER_ALL_VALIDATIONS //
/////////////////////////////////////////////////////////////////////////////////
public enum WhenToRun
{
BEFORE_ALL_VALIDATIONS,
BEFORE_UNIQUE_KEY_CHECKS,
BEFORE_REQUIRED_FIELD_CHECKS,
BEFORE_SECURITY_CHECKS,
AFTER_ALL_VALIDATIONS
}
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
@ -77,16 +62,6 @@ public abstract class AbstractPreInsertCustomizer
/*******************************************************************************
**
*******************************************************************************/
public WhenToRun getWhenToRun()
{
return (WhenToRun.AFTER_ALL_VALIDATIONS);
}
/******************************************************************************* /*******************************************************************************
** Getter for insertInput ** Getter for insertInput
** **

View File

@ -29,7 +29,6 @@ import java.util.List;
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction; import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction;
import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QRuntimeException;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput; import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput; import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
@ -43,16 +42,18 @@ import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
/******************************************************************************* /*******************************************************************************
** Standard/re-usable post-insert customizer, for the use case where, when we ** Standard/re-usable post-insert customizer, for the use case where, when we
** do an insert into table "parent", we want a record automatically inserted into ** do an insert into table "parent", we want a record automatically inserted into
** table "child". Optionally (based on RelationshipType), there can be a foreign ** table "child", and there's a foreign key in "parent", pointed at "child"
** key in "parent", pointed at "child". e.g., named: "parent.childId". ** e.g., named: "parent.childId".
** **
** A similar use-case would have the foreign key in the child table - in which case,
** we could add a "Type" enum, plus abstract method to get our "Type", then logic
** to switch behavior based on type. See existing type enum, but w/ only 1 case :)
*******************************************************************************/ *******************************************************************************/
public abstract class ChildInserterPostInsertCustomizer extends AbstractPostInsertCustomizer public abstract class ChildInserterPostInsertCustomizer extends AbstractPostInsertCustomizer
{ {
public enum RelationshipType public enum RelationshipType
{ {
PARENT_POINTS_AT_CHILD, PARENT_POINTS_AT_CHILD
CHILD_POINTS_AT_PARENT
} }
@ -67,17 +68,10 @@ public abstract class ChildInserterPostInsertCustomizer extends AbstractPostInse
*******************************************************************************/ *******************************************************************************/
public abstract String getChildTableName(); public abstract String getChildTableName();
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
public String getForeignKeyFieldName() public abstract String getForeignKeyFieldName();
{
return (null);
}
/******************************************************************************* /*******************************************************************************
** **
@ -94,7 +88,7 @@ public abstract class ChildInserterPostInsertCustomizer extends AbstractPostInse
{ {
try try
{ {
List<QRecord> rs = records; List<QRecord> rs = new ArrayList<>();
List<QRecord> childrenToInsert = new ArrayList<>(); List<QRecord> childrenToInsert = new ArrayList<>();
QTableMetaData table = getInsertInput().getTable(); QTableMetaData table = getInsertInput().getTable();
QTableMetaData childTable = getInsertInput().getInstance().getTable(getChildTableName()); QTableMetaData childTable = getInsertInput().getInstance().getTable(getChildTableName());
@ -103,37 +97,12 @@ public abstract class ChildInserterPostInsertCustomizer extends AbstractPostInse
// iterate over the inserted records, building a list child records to insert // // iterate over the inserted records, building a list child records to insert //
// for ones missing a value in the foreign key field. // // for ones missing a value in the foreign key field. //
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
switch(getRelationshipType()) for(QRecord record : records)
{ {
case PARENT_POINTS_AT_CHILD -> if(record.getValue(getForeignKeyFieldName()) == null)
{ {
String foreignKeyFieldName = getForeignKeyFieldName(); childrenToInsert.add(buildChildForRecord(record));
try
{
table.getField(foreignKeyFieldName);
}
catch(Exception e)
{
throw new QRuntimeException("For RelationshipType.PARENT_POINTS_AT_CHILD, a valid foreignKeyFieldName in the parent table must be given. "
+ "[" + foreignKeyFieldName + "] is not a valid field name in table [" + table.getName() + "]");
}
for(QRecord record : records)
{
if(record.getValue(foreignKeyFieldName) == null)
{
childrenToInsert.add(buildChildForRecord(record));
}
}
} }
case CHILD_POINTS_AT_PARENT ->
{
for(QRecord record : records)
{
childrenToInsert.add(buildChildForRecord(record));
}
}
default -> throw new IllegalStateException("Unexpected value: " + getRelationshipType());
} }
/////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////
@ -160,70 +129,51 @@ public abstract class ChildInserterPostInsertCustomizer extends AbstractPostInse
///////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////
// for the PARENT_POINTS_AT_CHILD relationship type:
// iterate over the original list of records again - for any that need a child (e.g., are missing // // iterate over the original list of records again - for any that need a child (e.g., are missing //
// foreign key), set their foreign key to a newly inserted child's key, and add them to be updated. // // foreign key), set their foreign key to a newly inserted child's key, and add them to be updated. //
////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////
switch(getRelationshipType()) List<QRecord> recordsToUpdate = new ArrayList<>();
for(QRecord record : records)
{ {
case PARENT_POINTS_AT_CHILD -> Serializable primaryKey = record.getValue(table.getPrimaryKeyField());
if(record.getValue(getForeignKeyFieldName()) == null)
{ {
rs = new ArrayList<>(); ///////////////////////////////////////////////////////////////////////////////////////////////////
List<QRecord> recordsToUpdate = new ArrayList<>(); // get the corresponding child record, if it has any errors, set that as a warning in the parent //
for(QRecord record : records) ///////////////////////////////////////////////////////////////////////////////////////////////////
QRecord childRecord = insertedRecordIterator.next();
if(CollectionUtils.nullSafeHasContents(childRecord.getErrors()))
{ {
Serializable primaryKey = record.getValue(table.getPrimaryKeyField()); for(QStatusMessage error : childRecord.getErrors())
if(record.getValue(getForeignKeyFieldName()) == null)
{ {
/////////////////////////////////////////////////////////////////////////////////////////////////// record.addWarning(new QWarningMessage("Error creating child " + childTable.getLabel() + " (" + error.toString() + ")"));
// get the corresponding child record, if it has any errors, set that as a warning in the parent //
///////////////////////////////////////////////////////////////////////////////////////////////////
QRecord childRecord = insertedRecordIterator.next();
if(CollectionUtils.nullSafeHasContents(childRecord.getErrors()))
{
for(QStatusMessage error : childRecord.getErrors())
{
record.addWarning(new QWarningMessage("Error creating child " + childTable.getLabel() + " (" + error.toString() + ")"));
}
rs.add(record);
continue;
}
Serializable foreignKey = childRecord.getValue(childTable.getPrimaryKeyField());
recordsToUpdate.add(new QRecord().withValue(table.getPrimaryKeyField(), primaryKey).withValue(getForeignKeyFieldName(), foreignKey));
record.setValue(getForeignKeyFieldName(), foreignKey);
rs.add(record);
}
else
{
rs.add(record);
} }
rs.add(record);
continue;
} }
//////////////////////////////////////////////////////////////////////////// Serializable foreignKey = childRecord.getValue(childTable.getPrimaryKeyField());
// update the originally inserted records to reference their new children // recordsToUpdate.add(new QRecord().withValue(table.getPrimaryKeyField(), primaryKey).withValue(getForeignKeyFieldName(), foreignKey));
//////////////////////////////////////////////////////////////////////////// record.setValue(getForeignKeyFieldName(), foreignKey);
UpdateInput updateInput = new UpdateInput(); rs.add(record);
updateInput.setTableName(getInsertInput().getTableName());
updateInput.setRecords(recordsToUpdate);
updateInput.setTransaction(this.insertInput.getTransaction());
new UpdateAction().execute(updateInput);
} }
case CHILD_POINTS_AT_PARENT -> else
{ {
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// rs.add(record);
// todo - some version of looking at the inserted children to confirm that they were inserted, and updating the parents with warnings if they weren't //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
} }
default -> throw new IllegalStateException("Unexpected value: " + getRelationshipType());
} }
////////////////////////////////////////////////////////////////////////////
// update the originally inserted records to reference their new children //
////////////////////////////////////////////////////////////////////////////
UpdateInput updateInput = new UpdateInput();
updateInput.setTableName(getInsertInput().getTableName());
updateInput.setRecords(recordsToUpdate);
updateInput.setTransaction(this.insertInput.getTransaction());
new UpdateAction().execute(updateInput);
return (rs); return (rs);
} }
catch(RuntimeException re)
{
throw (re);
}
catch(Exception e) catch(Exception e)
{ {
throw new RuntimeException("Error inserting new child records for new parent records", e); throw new RuntimeException("Error inserting new child records for new parent records", e);

View File

@ -46,7 +46,6 @@ import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleVal
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource; import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils; import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder;
/******************************************************************************* /*******************************************************************************
@ -124,7 +123,7 @@ public abstract class AbstractWidgetRenderer
output.getResults().removeIf(pvs -> !exists.add(pvs.getLabel())); output.getResults().removeIf(pvs -> !exists.add(pvs.getLabel()));
for(QPossibleValue<?> possibleValue : output.getResults()) for(QPossibleValue<?> possibleValue : output.getResults())
{ {
dropdownOptionList.add(MapBuilder.of( dropdownOptionList.add(Map.of(
"id", String.valueOf(possibleValue.getId()), "id", String.valueOf(possibleValue.getId()),
"label", possibleValue.getLabel() "label", possibleValue.getLabel()
)); ));

View File

@ -99,17 +99,7 @@ public enum DateTimeGroupBy
public String getSqlExpression() public String getSqlExpression()
{ {
ZoneId sessionOrInstanceZoneId = ValueUtils.getSessionOrInstanceZoneId(); ZoneId sessionOrInstanceZoneId = ValueUtils.getSessionOrInstanceZoneId();
return (getSqlExpression(sessionOrInstanceZoneId)); String targetTimezone = sessionOrInstanceZoneId.toString();
}
/*******************************************************************************
**
*******************************************************************************/
public String getSqlExpression(ZoneId targetZoneId)
{
String targetTimezone = targetZoneId.toString();
if("Z".equals(targetTimezone) || !StringUtils.hasContent(targetTimezone)) if("Z".equals(targetTimezone) || !StringUtils.hasContent(targetTimezone))
{ {
@ -168,18 +158,7 @@ public enum DateTimeGroupBy
*******************************************************************************/ *******************************************************************************/
public String makeSelectedString(Instant time) public String makeSelectedString(Instant time)
{ {
return (makeSelectedString(time, ValueUtils.getSessionOrInstanceZoneId())); ZonedDateTime zoned = time.atZone(ValueUtils.getSessionOrInstanceZoneId());
}
/*******************************************************************************
** Make an Instant into a string that will match what came out of the database's
** DATE_FORMAT() function
*******************************************************************************/
public String makeSelectedString(Instant time, ZoneId zoneId)
{
ZonedDateTime zoned = time.atZone(zoneId);
if(this == WEEK) if(this == WEEK)
{ {
@ -203,17 +182,7 @@ public enum DateTimeGroupBy
*******************************************************************************/ *******************************************************************************/
public String makeHumanString(Instant instant) public String makeHumanString(Instant instant)
{ {
return (makeHumanString(instant, ValueUtils.getSessionOrInstanceZoneId())); ZonedDateTime zoned = instant.atZone(ValueUtils.getSessionOrInstanceZoneId());
}
/*******************************************************************************
** Make a string to show to a user
*******************************************************************************/
public String makeHumanString(Instant instant, ZoneId zoneId)
{
ZonedDateTime zoned = instant.atZone(zoneId);
if(this.equals(WEEK)) if(this.equals(WEEK))
{ {
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("M'/'d"); DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("M'/'d");
@ -246,35 +215,25 @@ public enum DateTimeGroupBy
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
@SuppressWarnings("checkstyle:indentation")
public Instant roundDown(Instant instant) public Instant roundDown(Instant instant)
{ {
return roundDown(instant, ValueUtils.getSessionOrInstanceZoneId()); ZonedDateTime zoned = instant.atZone(ValueUtils.getSessionOrInstanceZoneId());
}
/*******************************************************************************
**
*******************************************************************************/
@SuppressWarnings("checkstyle:indentation")
public Instant roundDown(Instant instant, ZoneId zoneId)
{
ZonedDateTime zoned = instant.atZone(zoneId);
return switch(this) return switch(this)
{
case YEAR -> zoned.with(TemporalAdjusters.firstDayOfYear()).truncatedTo(ChronoUnit.DAYS).toInstant();
case MONTH -> zoned.with(TemporalAdjusters.firstDayOfMonth()).truncatedTo(ChronoUnit.DAYS).toInstant();
case WEEK ->
{ {
while(zoned.get(ChronoField.DAY_OF_WEEK) != DayOfWeek.SUNDAY.getValue()) case YEAR -> zoned.with(TemporalAdjusters.firstDayOfYear()).truncatedTo(ChronoUnit.DAYS).toInstant();
case MONTH -> zoned.with(TemporalAdjusters.firstDayOfMonth()).truncatedTo(ChronoUnit.DAYS).toInstant();
case WEEK ->
{ {
zoned = zoned.minusDays(1); while(zoned.get(ChronoField.DAY_OF_WEEK) != DayOfWeek.SUNDAY.getValue())
{
zoned = zoned.minusDays(1);
}
yield (zoned.truncatedTo(ChronoUnit.DAYS).toInstant());
} }
yield (zoned.truncatedTo(ChronoUnit.DAYS).toInstant()); case DAY -> zoned.truncatedTo(ChronoUnit.DAYS).toInstant();
} case HOUR -> zoned.truncatedTo(ChronoUnit.HOURS).toInstant();
case DAY -> zoned.truncatedTo(ChronoUnit.DAYS).toInstant(); };
case HOUR -> zoned.truncatedTo(ChronoUnit.HOURS).toInstant();
};
} }
@ -284,17 +243,7 @@ public enum DateTimeGroupBy
*******************************************************************************/ *******************************************************************************/
public Instant increment(Instant instant) public Instant increment(Instant instant)
{ {
return (increment(instant, ValueUtils.getSessionOrInstanceZoneId())); ZonedDateTime zoned = instant.atZone(ValueUtils.getSessionOrInstanceZoneId());
}
/*******************************************************************************
**
*******************************************************************************/
public Instant increment(Instant instant, ZoneId zoneId)
{
ZonedDateTime zoned = instant.atZone(zoneId);
return (zoned.plus(noOfChronoUnitsToAdd, chronoUnitToAdd).toInstant()); return (zoned.plus(noOfChronoUnitsToAdd, chronoUnitToAdd).toInstant());
} }
} }

View File

@ -60,8 +60,6 @@ public class ParentWidgetRenderer extends AbstractWidgetRenderer
widgetData.setChildWidgetNameList(metaData.getChildWidgetNameList()); widgetData.setChildWidgetNameList(metaData.getChildWidgetNameList());
} }
widgetData.setLayoutType(metaData.getLayoutType());
return (new RenderWidgetOutput(widgetData)); return (new RenderWidgetOutput(widgetData));
} }
catch(Exception e) catch(Exception e)

View File

@ -67,15 +67,4 @@ public interface BaseQueryInterface
} }
} }
/*******************************************************************************
**
*******************************************************************************/
default void cancelAction()
{
//////////////////////////////////////////////
// initially at least, a noop in base class //
//////////////////////////////////////////////
}
} }

View File

@ -28,7 +28,6 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput; import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; 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.model.metadata.tables.UniqueKey;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
/******************************************************************************* /*******************************************************************************
@ -56,7 +55,7 @@ public interface GetInterface
{ {
QTableMetaData table = getInput.getTable(); QTableMetaData table = getInput.getTable();
boolean foundMatch = false; boolean foundMatch = false;
for(UniqueKey uniqueKey : CollectionUtils.nonNullList(table.getUniqueKeys())) for(UniqueKey uniqueKey : table.getUniqueKeys())
{ {
if(new HashSet<>(uniqueKey.getFieldNames()).equals(getInput.getUniqueKey().keySet())) if(new HashSet<>(uniqueKey.getFieldNames()).equals(getInput.getUniqueKey().keySet()))
{ {

View File

@ -35,7 +35,6 @@ import com.kingsrook.qqq.backend.core.actions.dashboard.widgets.NoCodeWidgetRend
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction; import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger; import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessState; import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessState;
@ -51,7 +50,6 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput; 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.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.NoCodeWidgetFrontendComponentMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.NoCodeWidgetFrontendComponentMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData;
@ -59,7 +57,6 @@ import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendComponen
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData; 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.QProcessMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.processes.implementations.basepull.BasepullConfiguration; import com.kingsrook.qqq.backend.core.processes.implementations.basepull.BasepullConfiguration;
import com.kingsrook.qqq.backend.core.state.InMemoryStateProvider; import com.kingsrook.qqq.backend.core.state.InMemoryStateProvider;
import com.kingsrook.qqq.backend.core.state.StateProviderInterface; import com.kingsrook.qqq.backend.core.state.StateProviderInterface;
@ -486,35 +483,6 @@ public class RunProcessAction
/*******************************************************************************
**
*******************************************************************************/
protected String determineBasepullKeyValue(QProcessMetaData process, BasepullConfiguration basepullConfiguration) throws QException
{
String basepullKeyValue = (basepullConfiguration.getKeyValue() != null) ? basepullConfiguration.getKeyValue() : process.getName();
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// 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)
{
QSession session = QContext.getQSession();
QBackendMetaData backendMetaData = QContext.getQInstance().getBackend(process.getSchedule().getVariantBackend());
if(session.getBackendVariants() == null || !session.getBackendVariants().containsKey(backendMetaData.getVariantOptionsTableTypeValue()))
{
LOG.info("Could not find Backend Variant information for Backend '" + backendMetaData.getName() + "'");
}
else
{
basepullKeyValue += "-" + session.getBackendVariants().get(backendMetaData.getVariantOptionsTableTypeValue());
}
}
return (basepullKeyValue);
}
/******************************************************************************* /*******************************************************************************
** Insert or update the last runtime value for this basepull into the backend. ** Insert or update the last runtime value for this basepull into the backend.
*******************************************************************************/ *******************************************************************************/
@ -523,7 +491,7 @@ public class RunProcessAction
String basepullTableName = basepullConfiguration.getTableName(); String basepullTableName = basepullConfiguration.getTableName();
String basepullKeyFieldName = basepullConfiguration.getKeyField(); String basepullKeyFieldName = basepullConfiguration.getKeyField();
String basepullLastRunTimeFieldName = basepullConfiguration.getLastRunTimeFieldName(); String basepullLastRunTimeFieldName = basepullConfiguration.getLastRunTimeFieldName();
String basepullKeyValue = determineBasepullKeyValue(process, basepullConfiguration); String basepullKeyValue = (basepullConfiguration.getKeyValue() != null) ? basepullConfiguration.getKeyValue() : process.getName();
/////////////////////////////////////// ///////////////////////////////////////
// get the stored basepull timestamp // // get the stored basepull timestamp //
@ -603,7 +571,7 @@ public class RunProcessAction
String basepullKeyFieldName = basepullConfiguration.getKeyField(); String basepullKeyFieldName = basepullConfiguration.getKeyField();
String basepullLastRunTimeFieldName = basepullConfiguration.getLastRunTimeFieldName(); String basepullLastRunTimeFieldName = basepullConfiguration.getLastRunTimeFieldName();
Integer basepullHoursBackForInitialTimestamp = basepullConfiguration.getHoursBackForInitialTimestamp(); Integer basepullHoursBackForInitialTimestamp = basepullConfiguration.getHoursBackForInitialTimestamp();
String basepullKeyValue = determineBasepullKeyValue(process, basepullConfiguration); String basepullKeyValue = (basepullConfiguration.getKeyValue() != null) ? basepullConfiguration.getKeyValue() : process.getName();
/////////////////////////////////////// ///////////////////////////////////////
// get the stored basepull timestamp // // get the stored basepull timestamp //

View File

@ -197,27 +197,7 @@ public class ExportAction
String joinTableName = parts[0]; String joinTableName = parts[0];
if(!addedJoinNames.contains(joinTableName)) if(!addedJoinNames.contains(joinTableName))
{ {
QueryJoin queryJoin = new QueryJoin(joinTableName).withType(QueryJoin.Type.LEFT).withSelect(true); queryJoins.add(new QueryJoin(joinTableName).withType(QueryJoin.Type.LEFT).withSelect(true));
queryJoins.add(queryJoin);
/////////////////////////////////////////////////////////////////////////////////////////////
// in at least some cases, we need to let the queryJoin know what join-meta-data to use... //
// This code basically mirrors what QFMD is doing right now, so it's better - //
// but shouldn't all of this just be in JoinsContext? it does some of this... //
/////////////////////////////////////////////////////////////////////////////////////////////
QTableMetaData table = exportInput.getTable();
Optional<ExposedJoin> exposedJoinOptional = CollectionUtils.nonNullList(table.getExposedJoins()).stream().filter(ej -> ej.getJoinTable().equals(joinTableName)).findFirst();
if(exposedJoinOptional.isEmpty())
{
throw (new QException("Could not find exposed join between base table " + table.getName() + " and requested join table " + joinTableName));
}
ExposedJoin exposedJoin = exposedJoinOptional.get();
if(exposedJoin.getJoinPath().size() == 1)
{
queryJoin.setJoinMetaData(QContext.getQInstance().getJoin(exposedJoin.getJoinPath().get(exposedJoin.getJoinPath().size() - 1)));
}
addedJoinNames.add(joinTableName); addedJoinNames.add(joinTableName);
} }
} }

View File

@ -23,35 +23,22 @@ package com.kingsrook.qqq.backend.core.actions.scripts;
import java.io.Serializable; import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import com.kingsrook.qqq.backend.core.actions.scripts.logging.Log4jCodeExecutionLogger; import com.kingsrook.qqq.backend.core.actions.scripts.logging.Log4jCodeExecutionLogger;
import com.kingsrook.qqq.backend.core.actions.scripts.logging.QCodeExecutionLoggerInterface; import com.kingsrook.qqq.backend.core.actions.scripts.logging.QCodeExecutionLoggerInterface;
import com.kingsrook.qqq.backend.core.actions.scripts.logging.ScriptExecutionLoggerInterface; import com.kingsrook.qqq.backend.core.actions.scripts.logging.ScriptExecutionLoggerInterface;
import com.kingsrook.qqq.backend.core.actions.scripts.logging.StoreScriptLogAndScriptLogLineExecutionLogger; import com.kingsrook.qqq.backend.core.actions.scripts.logging.StoreScriptLogAndScriptLogLineExecutionLogger;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QCodeException; import com.kingsrook.qqq.backend.core.exceptions.QCodeException;
import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger; import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.scripts.AbstractRunScriptInput; import com.kingsrook.qqq.backend.core.model.actions.scripts.AbstractRunScriptInput;
import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeInput; import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeInput;
import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeOutput; import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType;
import com.kingsrook.qqq.backend.core.model.scripts.ScriptRevision; import com.kingsrook.qqq.backend.core.model.scripts.ScriptRevision;
import com.kingsrook.qqq.backend.core.model.scripts.ScriptRevisionFile;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.ObjectUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils; import com.kingsrook.qqq.backend.core.utils.StringUtils;
@ -114,11 +101,6 @@ public class ExecuteCodeAction
context.putAll(input.getInput()); context.putAll(input.getInput());
} }
//////////////////////////////////////////
// safely always set the deploymentMode //
//////////////////////////////////////////
context.put("deploymentMode", ObjectUtils.tryAndRequireNonNullElse(() -> QContext.getQInstance().getDeploymentMode(), null));
///////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////
// set the qCodeExecutor into any context objects which are QCodeExecutorAware // // set the qCodeExecutor into any context objects which are QCodeExecutorAware //
///////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////
@ -151,17 +133,7 @@ public class ExecuteCodeAction
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
public static ExecuteCodeInput setupExecuteCodeInput(AbstractRunScriptInput<?> input, ScriptRevision scriptRevision) throws QException public static ExecuteCodeInput setupExecuteCodeInput(AbstractRunScriptInput<?> input, ScriptRevision scriptRevision)
{
return setupExecuteCodeInput(input, scriptRevision, null);
}
/*******************************************************************************
**
*******************************************************************************/
public static ExecuteCodeInput setupExecuteCodeInput(AbstractRunScriptInput<?> input, ScriptRevision scriptRevision, String fileName) throws QException
{ {
ExecuteCodeInput executeCodeInput = new ExecuteCodeInput(); ExecuteCodeInput executeCodeInput = new ExecuteCodeInput();
executeCodeInput.setInput(new HashMap<>(Objects.requireNonNullElseGet(input.getInputValues(), HashMap::new))); executeCodeInput.setInput(new HashMap<>(Objects.requireNonNullElseGet(input.getInputValues(), HashMap::new)));
@ -178,49 +150,7 @@ public class ExecuteCodeAction
context.put("scriptUtils", input.getScriptUtils()); context.put("scriptUtils", input.getScriptUtils());
} }
if(CollectionUtils.nullSafeIsEmpty(scriptRevision.getFiles())) executeCodeInput.setCodeReference(new QCodeReference().withInlineCode(scriptRevision.getContents()).withCodeType(QCodeType.JAVA_SCRIPT)); // todo - code type as attribute of script!!
{
QueryInput queryInput = new QueryInput();
queryInput.setTableName(ScriptRevisionFile.TABLE_NAME);
queryInput.setFilter(new QQueryFilter(new QFilterCriteria("scriptRevisionId", QCriteriaOperator.EQUALS, scriptRevision.getId())));
QueryOutput queryOutput = new QueryAction().execute(queryInput);
scriptRevision.setFiles(new ArrayList<>());
for(QRecord record : queryOutput.getRecords())
{
scriptRevision.getFiles().add(new ScriptRevisionFile(record));
}
}
List<ScriptRevisionFile> files = scriptRevision.getFiles();
if(files == null || files.isEmpty())
{
throw (new QException("Script Revision " + scriptRevision.getId() + " had more than 1 associated ScriptRevisionFile (and the name to use was not specified)."));
}
else
{
String contents = null;
if(fileName == null || files.size() == 1)
{
contents = files.get(0).getContents();
}
else
{
for(ScriptRevisionFile file : files)
{
if(file.getFileName().equals(fileName))
{
contents = file.getContents();
}
}
if(contents == null)
{
throw (new QException("Could not find file named " + fileName + " for Script Revision " + scriptRevision.getId()));
}
}
executeCodeInput.setCodeReference(new QCodeReference().withInlineCode(contents).withCodeType(QCodeType.JAVA_SCRIPT)); // todo - code type as attribute of script!!
}
ExecuteCodeAction.addApiUtilityToContext(context, scriptRevision); ExecuteCodeAction.addApiUtilityToContext(context, scriptRevision);
context.put("qqq", new QqqScriptUtils()); context.put("qqq", new QqqScriptUtils());
@ -238,19 +168,7 @@ public class ExecuteCodeAction
*******************************************************************************/ *******************************************************************************/
public static void addApiUtilityToContext(Map<String, Serializable> context, ScriptRevision scriptRevision) public static void addApiUtilityToContext(Map<String, Serializable> context, ScriptRevision scriptRevision)
{ {
addApiUtilityToContext(context, scriptRevision.getApiName(), scriptRevision.getApiVersion()); if(!StringUtils.hasContent(scriptRevision.getApiName()) || !StringUtils.hasContent(scriptRevision.getApiVersion()))
}
/*******************************************************************************
** Try to (dynamically) load the ApiScriptUtils object from the api middleware
** module -- in case the runtime doesn't have that module deployed (e.g, not in
** the project pom).
*******************************************************************************/
public static void addApiUtilityToContext(Map<String, Serializable> context, String apiName, String apiVersion)
{
if(!StringUtils.hasContent(apiName) || !StringUtils.hasContent(apiVersion))
{ {
return; return;
} }
@ -258,7 +176,7 @@ public class ExecuteCodeAction
try try
{ {
Class<?> apiScriptUtilsClass = Class.forName("com.kingsrook.qqq.api.utils.ApiScriptUtils"); Class<?> apiScriptUtilsClass = Class.forName("com.kingsrook.qqq.api.utils.ApiScriptUtils");
Object apiScriptUtilsObject = apiScriptUtilsClass.getConstructor(String.class, String.class).newInstance(apiName, apiVersion); Object apiScriptUtilsObject = apiScriptUtilsClass.getConstructor(String.class, String.class).newInstance(scriptRevision.getApiName(), scriptRevision.getApiVersion());
context.put("api", (Serializable) apiScriptUtilsObject); context.put("api", (Serializable) apiScriptUtilsObject);
} }
catch(ClassNotFoundException e) catch(ClassNotFoundException e)

View File

@ -46,17 +46,7 @@ public interface QCodeExecutor
** e.g., a Nashorn ScriptObjectMirror will end up as a "primitive", or a List or Map of such ** e.g., a Nashorn ScriptObjectMirror will end up as a "primitive", or a List or Map of such
** **
*******************************************************************************/ *******************************************************************************/
default Object convertObjectToJava(Object object) throws QCodeException default Object convertObjectToJava(Object object)
{
return (object);
}
/*******************************************************************************
** Convert a native java object into one for the script's language/runtime.
** e.g., a java Instant to a Nashorn Date
**
*******************************************************************************/
default Object convertJavaObject(Object object, Object requestedTypeHint) throws QCodeException
{ {
return (object); return (object);
} }

View File

@ -1,159 +0,0 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.scripts;
import java.io.Serializable;
import java.util.Collections;
import java.util.List;
import com.kingsrook.qqq.backend.core.actions.scripts.logging.BuildScriptLogAndScriptLogLineExecutionLogger;
import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeInput;
import com.kingsrook.qqq.backend.core.model.actions.scripts.RunAdHocRecordScriptInput;
import com.kingsrook.qqq.backend.core.model.actions.scripts.RunAdHocRecordScriptOutput;
import com.kingsrook.qqq.backend.core.model.actions.scripts.TestScriptInput;
import com.kingsrook.qqq.backend.core.model.actions.scripts.TestScriptOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.code.AdHocScriptCodeReference;
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.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.scripts.Script;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
/*******************************************************************************
**
*******************************************************************************/
public class RecordScriptTestInterface implements TestScriptActionInterface
{
/*******************************************************************************
**
*******************************************************************************/
@Override
public void setupTestScriptInput(TestScriptInput testScriptInput, ExecuteCodeInput executeCodeInput) throws QException
{
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void execute(TestScriptInput input, TestScriptOutput output) throws QException
{
try
{
Serializable scriptId = input.getInputValues().get("scriptId");
QRecord script = new GetAction().executeForRecord(new GetInput(Script.TABLE_NAME).withPrimaryKey(scriptId));
//////////////////////////////////////////////
// look up the records being tested against //
//////////////////////////////////////////////
String tableName = script.getValueString("tableName");
QTableMetaData table = QContext.getQInstance().getTable(tableName);
if(table == null)
{
throw (new QException("Could not find table [" + tableName + "] for script"));
}
String recordPrimaryKeyList = ValueUtils.getValueAsString(input.getInputValues().get("recordPrimaryKeyList"));
if(!StringUtils.hasContent(recordPrimaryKeyList))
{
throw (new QException("Record primary key list was not given."));
}
QueryOutput queryOutput = new QueryAction().execute(new QueryInput(tableName)
.withFilter(new QQueryFilter(new QFilterCriteria(table.getPrimaryKeyField(), QCriteriaOperator.IN, recordPrimaryKeyList.split(","))))
.withIncludeAssociations(true));
if(CollectionUtils.nullSafeIsEmpty(queryOutput.getRecords()))
{
throw (new QException("No records were found by the given primary keys."));
}
/////////////////////////////
// set up & run the action //
/////////////////////////////
RunAdHocRecordScriptInput runAdHocRecordScriptInput = new RunAdHocRecordScriptInput();
runAdHocRecordScriptInput.setRecordList(queryOutput.getRecords());
BuildScriptLogAndScriptLogLineExecutionLogger executionLogger = new BuildScriptLogAndScriptLogLineExecutionLogger(null, null);
runAdHocRecordScriptInput.setLogger(executionLogger);
runAdHocRecordScriptInput.setTableName(tableName);
runAdHocRecordScriptInput.setCodeReference((AdHocScriptCodeReference) input.getCodeReference());
RunAdHocRecordScriptOutput runAdHocRecordScriptOutput = new RunAdHocRecordScriptOutput();
new RunAdHocRecordScriptAction().run(runAdHocRecordScriptInput, runAdHocRecordScriptOutput);
/////////////////////////////////
// send outputs back to caller //
/////////////////////////////////
output.setScriptLog(executionLogger.getScriptLog());
output.setScriptLogLines(executionLogger.getScriptLogLines());
if(runAdHocRecordScriptOutput.getException().isPresent())
{
output.setException(runAdHocRecordScriptOutput.getException().get());
}
}
catch(QException e)
{
output.setException(e);
}
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public List<QFieldMetaData> getTestInputFields()
{
return (List.of(new QFieldMetaData("recordPrimaryKeyList", QFieldType.STRING).withLabel("Record Primary Key List")));
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public List<QFieldMetaData> getTestOutputFields()
{
return (Collections.emptyList());
}
}

View File

@ -51,7 +51,6 @@ import com.kingsrook.qqq.backend.core.model.metadata.code.AdHocScriptCodeReferen
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.scripts.Script; import com.kingsrook.qqq.backend.core.model.scripts.Script;
import com.kingsrook.qqq.backend.core.model.scripts.ScriptRevision; import com.kingsrook.qqq.backend.core.model.scripts.ScriptRevision;
import com.kingsrook.qqq.backend.core.model.scripts.ScriptsMetaDataProvider;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
@ -198,7 +197,7 @@ public class RunAdHocRecordScriptAction
QueryInput queryInput = new QueryInput(); QueryInput queryInput = new QueryInput();
queryInput.setTableName(ScriptRevision.TABLE_NAME); queryInput.setTableName(ScriptRevision.TABLE_NAME);
queryInput.setFilter(new QQueryFilter(new QFilterCriteria("script.id", QCriteriaOperator.EQUALS, codeReference.getScriptId()))); queryInput.setFilter(new QQueryFilter(new QFilterCriteria("script.id", QCriteriaOperator.EQUALS, codeReference.getScriptId())));
queryInput.withQueryJoin(new QueryJoin(Script.TABLE_NAME).withBaseTableOrAlias(ScriptRevision.TABLE_NAME).withJoinMetaData(QContext.getQInstance().getJoin(ScriptsMetaDataProvider.CURRENT_SCRIPT_REVISION_JOIN_NAME))); queryInput.withQueryJoin(new QueryJoin(Script.TABLE_NAME).withBaseTableOrAlias(ScriptRevision.TABLE_NAME).withJoinMetaData(QContext.getQInstance().getJoin("currentScriptRevision")));
QueryOutput queryOutput = new QueryAction().execute(queryInput); QueryOutput queryOutput = new QueryAction().execute(queryInput);
if(CollectionUtils.nullSafeHasContents(queryOutput.getRecords())) if(CollectionUtils.nullSafeHasContents(queryOutput.getRecords()))

View File

@ -68,7 +68,6 @@ public class RunAssociatedScriptAction
new ExecuteCodeAction().run(executeCodeInput, executeCodeOutput); new ExecuteCodeAction().run(executeCodeInput, executeCodeOutput);
output.setOutput(executeCodeOutput.getOutput()); output.setOutput(executeCodeOutput.getOutput());
output.setScriptRevisionId(scriptRevision.getId());
} }
@ -111,7 +110,6 @@ public class RunAssociatedScriptAction
GetInput getInput = new GetInput(); GetInput getInput = new GetInput();
getInput.setTableName("scriptRevision"); getInput.setTableName("scriptRevision");
getInput.setPrimaryKey(scriptRevisionId); getInput.setPrimaryKey(scriptRevisionId);
getInput.setIncludeAssociations(true);
GetOutput getOutput = new GetAction().execute(getInput); GetOutput getOutput = new GetAction().execute(getInput);
if(getOutput.getRecord() == null) if(getOutput.getRecord() == null)
{ {

View File

@ -31,8 +31,6 @@ import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction; import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction;
import com.kingsrook.qqq.backend.core.exceptions.QException; 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.processes.RunBackendStepOutput;
import com.kingsrook.qqq.backend.core.model.actions.scripts.StoreAssociatedScriptInput; import com.kingsrook.qqq.backend.core.model.actions.scripts.StoreAssociatedScriptInput;
import com.kingsrook.qqq.backend.core.model.actions.scripts.StoreAssociatedScriptOutput; import com.kingsrook.qqq.backend.core.model.actions.scripts.StoreAssociatedScriptOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput; import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
@ -49,7 +47,6 @@ 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.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.tables.AssociatedScript; import com.kingsrook.qqq.backend.core.model.metadata.tables.AssociatedScript;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.processes.implementations.scripts.StoreScriptRevisionProcessStep;
import com.kingsrook.qqq.backend.core.utils.StringUtils; import com.kingsrook.qqq.backend.core.utils.StringUtils;
@ -183,19 +180,41 @@ public class StoreAssociatedScriptAction
} }
} }
RunBackendStepInput storeScriptRevisionInput = new RunBackendStepInput(); QRecord scriptRevision = new QRecord()
storeScriptRevisionInput.addValue("scriptId", script.getValue("id")); .withValue("scriptId", script.getValue("id"))
storeScriptRevisionInput.addValue("commitMessage", commitMessage); .withValue("contents", input.getCode())
storeScriptRevisionInput.addValue("apiName", input.getApiName()); .withValue("apiName", input.getApiName())
storeScriptRevisionInput.addValue("apiVersion", input.getApiVersion()); .withValue("apiVersion", input.getApiVersion())
storeScriptRevisionInput.addValue("fileNames", "script"); .withValue("commitMessage", commitMessage)
storeScriptRevisionInput.addValue("fileContents:script", input.getCode()); .withValue("sequenceNo", nextSequenceNo);
RunBackendStepOutput storeScriptRevisionOutput = new RunBackendStepOutput();
new StoreScriptRevisionProcessStep().run(storeScriptRevisionInput, storeScriptRevisionOutput); try
{
scriptRevision.setValue("author", input.getSession().getUser().getFullName());
}
catch(Exception e)
{
scriptRevision.setValue("author", "Unknown");
}
InsertInput insertInput = new InsertInput();
insertInput.setTableName("scriptRevision");
insertInput.setRecords(List.of(scriptRevision));
InsertOutput insertOutput = new InsertAction().execute(insertInput);
scriptRevision = insertOutput.getRecords().get(0);
////////////////////////////////////////////////////
// update the script to point at the new revision //
////////////////////////////////////////////////////
script.setValue("currentScriptRevisionId", scriptRevision.getValue("id"));
UpdateInput updateInput = new UpdateInput();
updateInput.setTableName("script");
updateInput.setRecords(List.of(script));
new UpdateAction().execute(updateInput);
output.setScriptId(script.getValueInteger("id")); output.setScriptId(script.getValueInteger("id"));
output.setScriptName(script.getValueString("name")); output.setScriptName(script.getValueString("name"));
output.setScriptRevisionId(storeScriptRevisionOutput.getValueInteger("scriptRevisionId")); output.setScriptRevisionId(scriptRevision.getValueInteger("id"));
output.setScriptRevisionSequenceNo(storeScriptRevisionOutput.getValueInteger("scriptRevisionSequenceNo")); output.setScriptRevisionSequenceNo(scriptRevision.getValueInteger("sequenceNo"));
} }
} }

View File

@ -41,7 +41,6 @@ public class Log4jCodeExecutionLogger implements QCodeExecutionLoggerInterface
private QCodeReference qCodeReference; private QCodeReference qCodeReference;
private String uuid = UUID.randomUUID().toString(); private String uuid = UUID.randomUUID().toString();
private boolean includeUUID = true;
/******************************************************************************* /*******************************************************************************
@ -53,7 +52,7 @@ public class Log4jCodeExecutionLogger implements QCodeExecutionLoggerInterface
this.qCodeReference = executeCodeInput.getCodeReference(); this.qCodeReference = executeCodeInput.getCodeReference();
String inputString = StringUtils.safeTruncate(ValueUtils.getValueAsString(executeCodeInput.getInput()), 250, "..."); String inputString = StringUtils.safeTruncate(ValueUtils.getValueAsString(executeCodeInput.getInput()), 250, "...");
LOG.info("Starting script execution: " + qCodeReference.getName() + (includeUUID ? ", uuid: " + uuid : "") + ", with input: " + inputString); LOG.info("Starting script execution: " + qCodeReference.getName() + ", uuid: " + uuid + ", with input: " + inputString);
} }
@ -64,7 +63,7 @@ public class Log4jCodeExecutionLogger implements QCodeExecutionLoggerInterface
@Override @Override
public void acceptLogLine(String logLine) public void acceptLogLine(String logLine)
{ {
LOG.info("Script log: " + (includeUUID ? uuid + ": " : "") + logLine); LOG.info("Script log: " + uuid + ": " + logLine);
} }
@ -75,7 +74,7 @@ public class Log4jCodeExecutionLogger implements QCodeExecutionLoggerInterface
@Override @Override
public void acceptException(Exception exception) public void acceptException(Exception exception)
{ {
LOG.info("Script Exception: " + (includeUUID ? uuid : ""), exception); LOG.info("Script Exception: " + uuid, exception);
} }
@ -87,38 +86,7 @@ public class Log4jCodeExecutionLogger implements QCodeExecutionLoggerInterface
public void acceptExecutionEnd(Serializable output) public void acceptExecutionEnd(Serializable output)
{ {
String outputString = StringUtils.safeTruncate(ValueUtils.getValueAsString(output), 250, "..."); String outputString = StringUtils.safeTruncate(ValueUtils.getValueAsString(output), 250, "...");
LOG.info("Finished script execution: " + qCodeReference.getName() + (includeUUID ? ", uuid: " + uuid : "") + ", with output: " + outputString); LOG.info("Finished script execution: " + qCodeReference.getName() + ", uuid: " + uuid + ", with output: " + outputString);
}
/*******************************************************************************
** Getter for includeUUID
*******************************************************************************/
public boolean getIncludeUUID()
{
return (this.includeUUID);
}
/*******************************************************************************
** Setter for includeUUID
*******************************************************************************/
public void setIncludeUUID(boolean includeUUID)
{
this.includeUUID = includeUUID;
}
/*******************************************************************************
** Fluent setter for includeUUID
*******************************************************************************/
public Log4jCodeExecutionLogger withIncludeUUID(boolean includeUUID)
{
this.includeUUID = includeUUID;
return (this);
} }
} }

View File

@ -29,7 +29,7 @@ import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeInput;
/******************************************************************************* /*******************************************************************************
** Interface to provide logging functionality to QCodeExecution (e.g., scripts) ** Interface to provide logging functionality to QCodeExecution (e.g., scripts)
*******************************************************************************/ *******************************************************************************/
public interface QCodeExecutionLoggerInterface extends Serializable public interface QCodeExecutionLoggerInterface
{ {
/******************************************************************************* /*******************************************************************************

View File

@ -1,88 +0,0 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.scripts.logging;
import java.io.Serializable;
import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeInput;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
/*******************************************************************************
** Implementation of a code execution logger that logs to System.out and
** System.err (for exceptions)
*******************************************************************************/
public class SystemOutExecutionLogger implements QCodeExecutionLoggerInterface
{
private QCodeReference qCodeReference;
/*******************************************************************************
**
*******************************************************************************/
@Override
public void acceptExecutionStart(ExecuteCodeInput executeCodeInput)
{
this.qCodeReference = executeCodeInput.getCodeReference();
String inputString = ValueUtils.getValueAsString(executeCodeInput.getInput());
System.out.println("Starting script execution: " + qCodeReference.getName() + ", with input: " + inputString);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void acceptLogLine(String logLine)
{
System.out.println("Script log: " + logLine);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void acceptException(Exception exception)
{
System.out.println("Script Exception: " + exception.getMessage());
exception.printStackTrace();
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void acceptExecutionEnd(Serializable output)
{
String outputString = ValueUtils.getValueAsString(output);
System.out.println("Finished script execution: " + qCodeReference.getName() + ", with output: " + outputString);
}
}

View File

@ -26,7 +26,6 @@ import com.kingsrook.qqq.backend.core.actions.ActionHelper;
import com.kingsrook.qqq.backend.core.actions.interfaces.AggregateInterface; import com.kingsrook.qqq.backend.core.actions.interfaces.AggregateInterface;
import com.kingsrook.qqq.backend.core.actions.tables.helpers.QueryStatManager; import com.kingsrook.qqq.backend.core.actions.tables.helpers.QueryStatManager;
import com.kingsrook.qqq.backend.core.exceptions.QException; 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.AggregateInput; import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.AggregateInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.AggregateOutput; import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.AggregateOutput;
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData; import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
@ -42,12 +41,6 @@ import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
*******************************************************************************/ *******************************************************************************/
public class AggregateAction public class AggregateAction
{ {
private static final QLogger LOG = QLogger.getLogger(AggregateAction.class);
private AggregateInterface aggregateInterface;
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
@ -63,7 +56,7 @@ public class AggregateAction
QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher(); QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher();
QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(aggregateInput.getBackend()); QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(aggregateInput.getBackend());
aggregateInterface = qModule.getAggregateInterface(); AggregateInterface aggregateInterface = qModule.getAggregateInterface();
aggregateInterface.setQueryStat(queryStat); aggregateInterface.setQueryStat(queryStat);
AggregateOutput aggregateOutput = aggregateInterface.execute(aggregateInput); AggregateOutput aggregateOutput = aggregateInterface.execute(aggregateInput);
@ -71,20 +64,4 @@ public class AggregateAction
return aggregateOutput; return aggregateOutput;
} }
/*******************************************************************************
**
*******************************************************************************/
public void cancel()
{
if(aggregateInterface == null)
{
LOG.warn("aggregateInterface object was null when requested to cancel");
return;
}
aggregateInterface.cancelAction();
}
} }

View File

@ -26,7 +26,6 @@ import com.kingsrook.qqq.backend.core.actions.ActionHelper;
import com.kingsrook.qqq.backend.core.actions.interfaces.CountInterface; import com.kingsrook.qqq.backend.core.actions.interfaces.CountInterface;
import com.kingsrook.qqq.backend.core.actions.tables.helpers.QueryStatManager; import com.kingsrook.qqq.backend.core.actions.tables.helpers.QueryStatManager;
import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput; import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput; import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput;
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData; import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
@ -42,12 +41,6 @@ import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
*******************************************************************************/ *******************************************************************************/
public class CountAction public class CountAction
{ {
private static final QLogger LOG = QLogger.getLogger(CountAction.class);
private CountInterface countInterface;
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
@ -63,7 +56,7 @@ public class CountAction
QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher(); QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher();
QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(countInput.getBackend()); QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(countInput.getBackend());
countInterface = qModule.getCountInterface(); CountInterface countInterface = qModule.getCountInterface();
countInterface.setQueryStat(queryStat); countInterface.setQueryStat(queryStat);
CountOutput countOutput = countInterface.execute(countInput); CountOutput countOutput = countInterface.execute(countInput);
@ -71,20 +64,4 @@ public class CountAction
return countOutput; return countOutput;
} }
/*******************************************************************************
**
*******************************************************************************/
public void cancel()
{
if(countInterface == null)
{
LOG.warn("countInterface object was null when requested to cancel");
return;
}
countInterface.cancelAction();
}
} }

View File

@ -40,10 +40,8 @@ import com.kingsrook.qqq.backend.core.actions.customizers.AbstractPreDeleteCusto
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader; import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers; 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.interfaces.DeleteInterface;
import com.kingsrook.qqq.backend.core.actions.tables.helpers.ValidateRecordSecurityLockHelper;
import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.LogPair;
import com.kingsrook.qqq.backend.core.logging.QLogger; import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.audits.DMLAuditInput; import com.kingsrook.qqq.backend.core.model.actions.audits.DMLAuditInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput; import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput;
@ -119,14 +117,12 @@ public class DeleteAction
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if there's a query filter, but the interface doesn't support using a query filter, then do a query for the filter, to get a list of primary keys instead // // if there's a query filter, but the interface doesn't support using a query filter, then do a query for the filter, to get a list of primary keys instead //
// or - anytime there are associations on the table we want primary keys, as that's what the manage associations method uses //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
if(deleteInput.getQueryFilter() != null && (!deleteInterface.supportsQueryFilterInput() || CollectionUtils.nullSafeHasContents(table.getAssociations()))) if(deleteInput.getQueryFilter() != null && !deleteInterface.supportsQueryFilterInput())
{ {
LOG.info("Querying for primary keys, for table " + table.getName() + " in backend module " + qModule.getBackendType() + " which does not support queryFilter input for deletes (or the table has associations)"); LOG.info("Querying for primary keys, for backend module " + qModule.getBackendType() + " which does not support queryFilter input for deletes");
List<Serializable> primaryKeyList = getPrimaryKeysFromQueryFilter(deleteInput); List<Serializable> primaryKeyList = getPrimaryKeysFromQueryFilter(deleteInput);
deleteInput.setPrimaryKeys(primaryKeyList); deleteInput.setPrimaryKeys(primaryKeyList);
primaryKeys = primaryKeyList;
if(primaryKeyList.isEmpty()) if(primaryKeyList.isEmpty())
{ {
@ -169,22 +165,10 @@ public class DeleteAction
if(!primaryKeysToRemoveFromInput.isEmpty()) if(!primaryKeysToRemoveFromInput.isEmpty())
{ {
if(primaryKeys == null) primaryKeys.removeAll(primaryKeysToRemoveFromInput);
{
LOG.warn("There were primary keys to remove from the input, but no primary key list (filter supplied as input?)", new LogPair("primaryKeysToRemoveFromInput", primaryKeysToRemoveFromInput));
}
else
{
primaryKeys.removeAll(primaryKeysToRemoveFromInput);
}
} }
} }
////////////////////////////////////////////////////////////////////////////////////////////////
// stash a copy of primary keys that didn't have errors (for use in manageAssociations below) //
////////////////////////////////////////////////////////////////////////////////////////////////
Set<Serializable> primaryKeysWithoutErrors = new HashSet<>(CollectionUtils.nonNullList(primaryKeys));
//////////////////////////////////// ////////////////////////////////////
// have the backend do the delete // // have the backend do the delete //
//////////////////////////////////// ////////////////////////////////////
@ -203,13 +187,11 @@ public class DeleteAction
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if a record had a validation warning, but then an execution error, remove it from the warning list - so it's only in one of them. // // if a record had a validation warning, but then an execution error, remove it from the warning list - so it's only in one of them. //
// also, always remove from
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
for(QRecord outputRecordWithError : outputRecordsWithErrors) for(QRecord outputRecordWithError : outputRecordsWithErrors)
{ {
Serializable pkey = outputRecordWithError.getValue(primaryKeyFieldName); Serializable pkey = outputRecordWithError.getValue(primaryKeyFieldName);
recordsWithValidationWarnings.remove(pkey); recordsWithValidationWarnings.remove(pkey);
primaryKeysWithoutErrors.remove(pkey);
} }
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
@ -229,23 +211,15 @@ public class DeleteAction
//////////////////////////////////////// ////////////////////////////////////////
// delete associations, if applicable // // delete associations, if applicable //
//////////////////////////////////////// ////////////////////////////////////////
manageAssociations(primaryKeysWithoutErrors, deleteInput); manageAssociations(deleteInput);
////////////////// ///////////////////////////////////
// do the audit // // do the audit //
////////////////// // todo - add input.omitDmlAudit //
if(deleteInput.getOmitDmlAudit()) ///////////////////////////////////
{ DMLAuditInput dmlAuditInput = new DMLAuditInput().withTableActionInput(deleteInput);
LOG.debug("Requested to omit DML audit"); oldRecordList.ifPresent(l -> dmlAuditInput.setRecordList(l));
} new DMLAuditAction().execute(dmlAuditInput);
else
{
DMLAuditInput dmlAuditInput = new DMLAuditInput()
.withTableActionInput(deleteInput)
.withAuditContext(deleteInput.getAuditContext());
oldRecordList.ifPresent(l -> dmlAuditInput.setRecordList(l));
new DMLAuditAction().execute(dmlAuditInput);
}
////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////
// finally, run the post-delete customizer, if there is one // // finally, run the post-delete customizer, if there is one //
@ -322,8 +296,6 @@ public class DeleteAction
QTableMetaData table = deleteInput.getTable(); QTableMetaData table = deleteInput.getTable();
List<QRecord> primaryKeysNotFound = validateRecordsExistAndCanBeAccessed(deleteInput, oldRecordList.get()); List<QRecord> primaryKeysNotFound = validateRecordsExistAndCanBeAccessed(deleteInput, oldRecordList.get());
ValidateRecordSecurityLockHelper.validateSecurityFields(table, oldRecordList.get(), ValidateRecordSecurityLockHelper.Action.DELETE);
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////
// after all validations, run the pre-delete customizer, if there is one // // after all validations, run the pre-delete customizer, if there is one //
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////
@ -368,7 +340,7 @@ public class DeleteAction
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
private void manageAssociations(Set<Serializable> primaryKeysWithoutErrors, DeleteInput deleteInput) throws QException private void manageAssociations(DeleteInput deleteInput) throws QException
{ {
QTableMetaData table = deleteInput.getTable(); QTableMetaData table = deleteInput.getTable();
for(Association association : CollectionUtils.nonNullList(table.getAssociations())) for(Association association : CollectionUtils.nonNullList(table.getAssociations()))
@ -381,7 +353,7 @@ public class DeleteAction
if(join.getJoinOns().size() == 1 && join.getJoinOns().get(0).getLeftField().equals(table.getPrimaryKeyField())) if(join.getJoinOns().size() == 1 && join.getJoinOns().get(0).getLeftField().equals(table.getPrimaryKeyField()))
{ {
filter.addCriteria(new QFilterCriteria(join.getJoinOns().get(0).getRightField(), QCriteriaOperator.IN, new ArrayList<>(primaryKeysWithoutErrors))); filter.addCriteria(new QFilterCriteria(join.getJoinOns().get(0).getRightField(), QCriteriaOperator.IN, deleteInput.getPrimaryKeys()));
} }
else else
{ {

View File

@ -23,6 +23,8 @@ package com.kingsrook.qqq.backend.core.actions.tables;
import java.io.Serializable; import java.io.Serializable;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
@ -31,24 +33,35 @@ import com.kingsrook.qqq.backend.core.actions.customizers.AbstractPostQueryCusto
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader; import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers; 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.interfaces.GetInterface;
import com.kingsrook.qqq.backend.core.actions.tables.helpers.GetActionCacheHelper;
import com.kingsrook.qqq.backend.core.actions.values.QPossibleValueTranslator; import com.kingsrook.qqq.backend.core.actions.values.QPossibleValueTranslator;
import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter; import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter;
import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.LogPair;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput; import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput; import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator; 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.QFilterCriteria;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.fields.AdornmentType; import com.kingsrook.qqq.backend.core.model.metadata.fields.AdornmentType;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; 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.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.cache.CacheUseCase;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher; import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface; import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
import com.kingsrook.qqq.backend.core.modules.backend.implementations.utils.BackendQueryFilterUtils;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import org.apache.commons.lang.NotImplementedException;
/******************************************************************************* /*******************************************************************************
@ -57,6 +70,8 @@ import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
*******************************************************************************/ *******************************************************************************/
public class GetAction public class GetAction
{ {
private static final QLogger LOG = QLogger.getLogger(InsertAction.class);
private Optional<AbstractPostQueryCustomizer> postGetRecordCustomizer; private Optional<AbstractPostQueryCustomizer> postGetRecordCustomizer;
private GetInput getInput; private GetInput getInput;
@ -64,16 +79,6 @@ public class GetAction
/*******************************************************************************
**
*******************************************************************************/
public QRecord executeForRecord(GetInput getInput) throws QException
{
return (execute(getInput).getRecord());
}
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
@ -120,7 +125,36 @@ public class GetAction
//////////////////////////// ////////////////////////////
if(table.getCacheOf() != null) if(table.getCacheOf() != null)
{ {
new GetActionCacheHelper().handleCaching(getInput, getOutput); if(getOutput.getRecord() == null)
{
///////////////////////////////////////////////////////////////////////
// if the record wasn't found, see if we should look in cache-source //
///////////////////////////////////////////////////////////////////////
QRecord recordFromSource = tryToGetFromCacheSource(getInput);
if(recordFromSource != null)
{
/////////////////////////////////////////////////////////////////////////////////////////////////
// good, we found a record from the source, make sure we should cache it, and if so, do it now //
/////////////////////////////////////////////////////////////////////////////////////////////////
QRecord recordToCache = mapSourceRecordToCacheRecord(table, recordFromSource);
boolean shouldCacheRecord = shouldCacheRecord(table, recordToCache);
if(shouldCacheRecord)
{
InsertInput insertInput = new InsertInput();
insertInput.setTableName(getInput.getTableName());
insertInput.setRecords(List.of(recordToCache));
InsertOutput insertOutput = new InsertAction().execute(insertInput);
getOutput.setRecord(insertOutput.getRecords().get(0));
}
}
}
else
{
/////////////////////////////////////////////////////////////////////////////////
// if the record was found, but it's too old, maybe re-fetch from cache source //
/////////////////////////////////////////////////////////////////////////////////
refreshCacheIfExpired(getInput, getOutput);
}
} }
//////////////////////////////////////////////////////// ////////////////////////////////////////////////////////
@ -137,12 +171,168 @@ public class GetAction
/******************************************************************************* /*******************************************************************************
** Run a GetAction by using the QueryAction instead (e.g., with a filter made **
** from the pkey/ukey, and returning the single record if found).
*******************************************************************************/ *******************************************************************************/
public GetOutput executeViaQuery(GetInput getInput) throws QException private boolean shouldCacheRecord(QTableMetaData table, QRecord recordToCache)
{ {
return (new DefaultGetInterface().execute(getInput)); boolean shouldCacheRecord = true;
recordMatchExclusionLoop:
for(CacheUseCase useCase : CollectionUtils.nonNullList(table.getCacheOf().getUseCases()))
{
for(QQueryFilter filter : CollectionUtils.nonNullList(useCase.getExcludeRecordsMatching()))
{
if(BackendQueryFilterUtils.doesRecordMatch(filter, recordToCache))
{
LOG.info("Not caching record because it matches a use case's filter exclusion", new LogPair("record", recordToCache), new LogPair("filter", filter));
shouldCacheRecord = false;
break recordMatchExclusionLoop;
}
}
}
return (shouldCacheRecord);
}
/*******************************************************************************
**
*******************************************************************************/
private static QRecord mapSourceRecordToCacheRecord(QTableMetaData table, QRecord recordFromSource)
{
QRecord cacheRecord = new QRecord(recordFromSource);
//////////////////////////////////////////////////////////////////////////////////////////////
// make sure every value in the qRecord is set, because we will possibly be doing an update //
// on this record and want to null out any fields not set, not leave them populated //
//////////////////////////////////////////////////////////////////////////////////////////////
for(String fieldName : table.getFields().keySet())
{
if(!cacheRecord.getValues().containsKey(fieldName))
{
cacheRecord.setValue(fieldName, null);
}
}
if(StringUtils.hasContent(table.getCacheOf().getCachedDateFieldName()))
{
cacheRecord.setValue(table.getCacheOf().getCachedDateFieldName(), Instant.now());
}
return (cacheRecord);
}
/*******************************************************************************
**
*******************************************************************************/
private void refreshCacheIfExpired(GetInput getInput, GetOutput getOutput) throws QException
{
QTableMetaData table = getInput.getTable();
Integer expirationSeconds = table.getCacheOf().getExpirationSeconds();
if(expirationSeconds != null)
{
QRecord cachedRecord = getOutput.getRecord();
Instant cachedDate = cachedRecord.getValueInstant(table.getCacheOf().getCachedDateFieldName());
if(cachedDate == null || cachedDate.isBefore(Instant.now().minus(expirationSeconds, ChronoUnit.SECONDS)))
{
//////////////////////////////////////////////////////////////////////////
// keep the serial key from the old record in case we need to delete it //
//////////////////////////////////////////////////////////////////////////
Serializable oldRecordPrimaryKey = getOutput.getRecord().getValue(table.getPrimaryKeyField());
boolean shouldDeleteCachedRecord = true;
///////////////////////////////////////////
// fetch record from original source now //
///////////////////////////////////////////
QRecord recordFromSource = tryToGetFromCacheSource(getInput);
if(recordFromSource != null)
{
//////////////////////////////////////////////////////////////////////
// if the record was found in the source, put it into the output //
// object so returned back to caller, check that it should actually //
// be cached before doing so //
//////////////////////////////////////////////////////////////////////
QRecord recordToCache = mapSourceRecordToCacheRecord(table, recordFromSource);
recordToCache.setValue(table.getPrimaryKeyField(), cachedRecord.getValue(table.getPrimaryKeyField()));
getOutput.setRecord(recordToCache);
if(shouldCacheRecord(table, recordToCache))
{
UpdateInput updateInput = new UpdateInput();
updateInput.setTableName(getInput.getTableName());
updateInput.setRecords(List.of(recordToCache));
UpdateOutput updateOutput = new UpdateAction().execute(updateInput);
getOutput.setRecord(updateOutput.getRecords().get(0));
shouldDeleteCachedRecord = false;
}
}
else
{
///////////////////////////////////////////////////////////////////////////////////////
// if we did not get a record back from the source, empty out the getOutput's record //
///////////////////////////////////////////////////////////////////////////////////////
getOutput.setRecord(null);
}
if(shouldDeleteCachedRecord)
{
/////////////////////////////////////////////////////////////////////////////
// if the record is no longer in the source, then remove it from the cache //
/////////////////////////////////////////////////////////////////////////////
DeleteInput deleteInput = new DeleteInput();
deleteInput.setTableName(getInput.getTableName());
deleteInput.setPrimaryKeys(List.of(oldRecordPrimaryKey));
new DeleteAction().execute(deleteInput);
}
}
}
}
/*******************************************************************************
**
*******************************************************************************/
private QRecord tryToGetFromCacheSource(GetInput getInput) throws QException
{
QRecord recordFromSource = null;
QTableMetaData table = getInput.getTable();
for(CacheUseCase cacheUseCase : CollectionUtils.nonNullList(table.getCacheOf().getUseCases()))
{
if(CacheUseCase.Type.UNIQUE_KEY_TO_UNIQUE_KEY.equals(cacheUseCase.getType()) && getInput.getUniqueKey() != null)
{
recordFromSource = getFromCachedSourceForUniqueKeyToUniqueKey(getInput, table.getCacheOf().getSourceTable());
break;
}
else
{
// todo!!
throw new NotImplementedException("Not-yet-implemented cache use case type: " + cacheUseCase.getType());
}
}
return (recordFromSource);
}
/*******************************************************************************
**
*******************************************************************************/
private QRecord getFromCachedSourceForUniqueKeyToUniqueKey(GetInput getInput, String sourceTableName) throws QException
{
/////////////////////////////////////////////////////
// do a Get on the source table, by the unique key //
/////////////////////////////////////////////////////
GetInput sourceGetInput = new GetInput();
sourceGetInput.setTableName(sourceTableName);
sourceGetInput.setUniqueKey(getInput.getUniqueKey());
GetOutput sourceGetOutput = new GetAction().execute(sourceGetInput);
QRecord outputRecord = sourceGetOutput.getRecord();
return (outputRecord);
} }
@ -155,7 +345,42 @@ public class GetAction
@Override @Override
public GetOutput execute(GetInput getInput) throws QException public GetOutput execute(GetInput getInput) throws QException
{ {
QueryInput queryInput = convertGetInputToQueryInput(getInput); QueryInput queryInput = new QueryInput();
queryInput.setTableName(getInput.getTableName());
//////////////////////////////////////////////////
// build filter using either pkey or unique key //
//////////////////////////////////////////////////
QQueryFilter filter = new QQueryFilter();
if(getInput.getPrimaryKey() != null)
{
filter.addCriteria(new QFilterCriteria(getInput.getTable().getPrimaryKeyField(), QCriteriaOperator.EQUALS, getInput.getPrimaryKey()));
}
else if(getInput.getUniqueKey() != null)
{
for(Map.Entry<String, Serializable> entry : getInput.getUniqueKey().entrySet())
{
if(entry.getValue() == null)
{
filter.addCriteria(new QFilterCriteria(entry.getKey(), QCriteriaOperator.IS_BLANK));
}
else
{
filter.addCriteria(new QFilterCriteria(entry.getKey(), QCriteriaOperator.EQUALS, entry.getValue()));
}
}
}
else
{
throw (new QException("No primaryKey or uniqueKey was passed to Get"));
}
queryInput.setFilter(filter);
queryInput.setIncludeAssociations(getInput.getIncludeAssociations());
queryInput.setAssociationNamesToInclude(getInput.getAssociationNamesToInclude());
queryInput.setShouldFetchHeavyFields(getInput.getShouldFetchHeavyFields());
queryInput.setShouldMaskPasswords(getInput.getShouldMaskPasswords());
queryInput.setShouldOmitHiddenFields(getInput.getShouldOmitHiddenFields());
QueryOutput queryOutput = new QueryAction().execute(queryInput); QueryOutput queryOutput = new QueryAction().execute(queryInput);
@ -170,48 +395,6 @@ public class GetAction
/*******************************************************************************
**
*******************************************************************************/
public static QueryInput convertGetInputToQueryInput(GetInput getInput) throws QException
{
QueryInput queryInput = new QueryInput();
queryInput.setTableName(getInput.getTableName());
//////////////////////////////////////////////////
// build filter using either pkey or unique key //
//////////////////////////////////////////////////
QQueryFilter filter = new QQueryFilter();
if(getInput.getPrimaryKey() != null)
{
filter.addCriteria(new QFilterCriteria(getInput.getTable().getPrimaryKeyField(), QCriteriaOperator.EQUALS, getInput.getPrimaryKey()));
}
else if(getInput.getUniqueKey() != null)
{
for(Map.Entry<String, Serializable> entry : getInput.getUniqueKey().entrySet())
{
if(entry.getValue() == null)
{
filter.addCriteria(new QFilterCriteria(entry.getKey(), QCriteriaOperator.IS_BLANK));
}
else
{
filter.addCriteria(new QFilterCriteria(entry.getKey(), QCriteriaOperator.EQUALS, entry.getValue()));
}
}
}
else
{
throw (new QException("No primaryKey or uniqueKey was passed to Get"));
}
queryInput.setFilter(filter);
queryInput.setCommonParamsFrom(getInput);
return queryInput;
}
/******************************************************************************* /*******************************************************************************
** Run the necessary actions on a record. This may include setting display values, ** Run the necessary actions on a record. This may include setting display values,
** translating possible values, and running post-record customizations. ** translating possible values, and running post-record customizations.

View File

@ -78,28 +78,6 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
/*******************************************************************************
**
*******************************************************************************/
public QRecord executeForRecord(InsertInput insertInput) throws QException
{
InsertOutput insertOutput = new InsertAction().execute(insertInput);
return (insertOutput.getRecords().get(0));
}
/*******************************************************************************
**
*******************************************************************************/
public static List<QRecord> executeForRecords(InsertInput insertInput) throws QException
{
InsertOutput insertOutput = new InsertAction().execute(insertInput);
return (insertOutput.getRecords());
}
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
@ -155,10 +133,7 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
} }
else else
{ {
new DMLAuditAction().execute(new DMLAuditInput() new DMLAuditAction().execute(new DMLAuditInput().withTableActionInput(insertInput).withRecordList(insertOutput.getRecords()));
.withTableActionInput(insertInput)
.withAuditContext(insertInput.getAuditContext())
.withRecordList(insertOutput.getRecords()));
} }
////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////
@ -193,76 +168,25 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
{ {
QTableMetaData table = insertInput.getTable(); QTableMetaData table = insertInput.getTable();
///////////////////////////////////////////////////////////////////
// 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());
if(preInsertCustomizer.isPresent())
{
preInsertCustomizer.get().setInsertInput(insertInput);
preInsertCustomizer.get().setIsPreview(isPreview);
runPreInsertCustomizerIfItIsTime(insertInput, preInsertCustomizer, AbstractPreInsertCustomizer.WhenToRun.BEFORE_ALL_VALIDATIONS);
}
setDefaultValuesInRecords(table, insertInput.getRecords());
ValueBehaviorApplier.applyFieldBehaviors(insertInput.getInstance(), table, insertInput.getRecords()); ValueBehaviorApplier.applyFieldBehaviors(insertInput.getInstance(), table, insertInput.getRecords());
runPreInsertCustomizerIfItIsTime(insertInput, preInsertCustomizer, AbstractPreInsertCustomizer.WhenToRun.BEFORE_UNIQUE_KEY_CHECKS);
setErrorsIfUniqueKeyErrors(insertInput, table); setErrorsIfUniqueKeyErrors(insertInput, table);
runPreInsertCustomizerIfItIsTime(insertInput, preInsertCustomizer, AbstractPreInsertCustomizer.WhenToRun.BEFORE_REQUIRED_FIELD_CHECKS);
if(insertInput.getInputSource().shouldValidateRequiredFields()) if(insertInput.getInputSource().shouldValidateRequiredFields())
{ {
validateRequiredFields(insertInput); validateRequiredFields(insertInput);
} }
runPreInsertCustomizerIfItIsTime(insertInput, preInsertCustomizer, AbstractPreInsertCustomizer.WhenToRun.BEFORE_SECURITY_CHECKS);
ValidateRecordSecurityLockHelper.validateSecurityFields(insertInput.getTable(), insertInput.getRecords(), ValidateRecordSecurityLockHelper.Action.INSERT); ValidateRecordSecurityLockHelper.validateSecurityFields(insertInput.getTable(), insertInput.getRecords(), ValidateRecordSecurityLockHelper.Action.INSERT);
runPreInsertCustomizerIfItIsTime(insertInput, preInsertCustomizer, AbstractPreInsertCustomizer.WhenToRun.AFTER_ALL_VALIDATIONS); ///////////////////////////////////////////////////////////////////////////
} // after all validations, run the pre-insert customizer, if there is one //
///////////////////////////////////////////////////////////////////////////
Optional<AbstractPreInsertCustomizer> preInsertCustomizer = QCodeLoader.getTableCustomizer(AbstractPreInsertCustomizer.class, table, TableCustomizers.PRE_INSERT_RECORD.getRole());
/*******************************************************************************
**
*******************************************************************************/
private void setDefaultValuesInRecords(QTableMetaData table, List<QRecord> records)
{
////////////////////////////////////////////////////////////////////////////////////////////////
// for all fields in the table - if any have a default value, then look at all input records, //
// and if they have null value, then apply the default //
////////////////////////////////////////////////////////////////////////////////////////////////
for(QFieldMetaData field : table.getFields().values())
{
if(field.getDefaultValue() != null)
{
for(QRecord record : records)
{
if(record.getValue(field.getName()) == null)
{
record.setValue(field.getName(), field.getDefaultValue());
}
}
}
}
}
/*******************************************************************************
**
*******************************************************************************/
private void runPreInsertCustomizerIfItIsTime(InsertInput insertInput, Optional<AbstractPreInsertCustomizer> preInsertCustomizer, AbstractPreInsertCustomizer.WhenToRun whenToRun) throws QException
{
if(preInsertCustomizer.isPresent()) if(preInsertCustomizer.isPresent())
{ {
if(whenToRun.equals(preInsertCustomizer.get().getWhenToRun())) preInsertCustomizer.get().setInsertInput(insertInput);
{ preInsertCustomizer.get().setIsPreview(isPreview);
insertInput.setRecords(preInsertCustomizer.get().apply(insertInput.getRecords())); insertInput.setRecords(preInsertCustomizer.get().apply(insertInput.getRecords()));
}
} }
} }

View File

@ -37,7 +37,6 @@ 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.interfaces.QueryInterface;
import com.kingsrook.qqq.backend.core.actions.reporting.BufferedRecordPipe; import com.kingsrook.qqq.backend.core.actions.reporting.BufferedRecordPipe;
import com.kingsrook.qqq.backend.core.actions.reporting.RecordPipeBufferedWrapper; import com.kingsrook.qqq.backend.core.actions.reporting.RecordPipeBufferedWrapper;
import com.kingsrook.qqq.backend.core.actions.tables.helpers.QueryActionCacheHelper;
import com.kingsrook.qqq.backend.core.actions.tables.helpers.QueryStatManager; import com.kingsrook.qqq.backend.core.actions.tables.helpers.QueryStatManager;
import com.kingsrook.qqq.backend.core.actions.values.QPossibleValueTranslator; import com.kingsrook.qqq.backend.core.actions.values.QPossibleValueTranslator;
import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter; import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter;
@ -76,7 +75,6 @@ public class QueryAction
private Optional<AbstractPostQueryCustomizer> postQueryRecordCustomizer; private Optional<AbstractPostQueryCustomizer> postQueryRecordCustomizer;
private QueryInput queryInput; private QueryInput queryInput;
private QueryInterface queryInterface;
private QPossibleValueTranslator qPossibleValueTranslator; private QPossibleValueTranslator qPossibleValueTranslator;
@ -122,20 +120,12 @@ public class QueryAction
QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher(); QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher();
QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(backend); QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(backend);
queryInterface = qModule.getQueryInterface(); QueryInterface queryInterface = qModule.getQueryInterface();
queryInterface.setQueryStat(queryStat); queryInterface.setQueryStat(queryStat);
QueryOutput queryOutput = queryInterface.execute(queryInput); QueryOutput queryOutput = queryInterface.execute(queryInput);
QueryStatManager.getInstance().add(queryStat); QueryStatManager.getInstance().add(queryStat);
////////////////////////////
// handle cache use-cases //
////////////////////////////
if(table.getCacheOf() != null)
{
new QueryActionCacheHelper().handleCaching(queryInput, queryOutput);
}
if(queryInput.getRecordPipe() instanceof BufferedRecordPipe bufferedRecordPipe) if(queryInput.getRecordPipe() instanceof BufferedRecordPipe bufferedRecordPipe)
{ {
bufferedRecordPipe.finalFlush(); bufferedRecordPipe.finalFlush();
@ -340,20 +330,4 @@ public class QueryAction
} }
} }
} }
/*******************************************************************************
**
*******************************************************************************/
public void cancel()
{
if(queryInterface == null)
{
LOG.warn("queryInterface object was null when requested to cancel");
return;
}
queryInterface.cancelAction();
}
} }

View File

@ -1,179 +0,0 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.tables;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import com.kingsrook.qqq.backend.core.actions.AbstractQActionFunction;
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
import com.kingsrook.qqq.backend.core.actions.tables.helpers.UniqueKeyHelper;
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.delete.DeleteInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.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.replace.ReplaceInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.replace.ReplaceOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
/*******************************************************************************
** Action to do a "replace" - e.g: Update rows with unique-key values that are
** already in the table; insert rows whose unique keys weren't already in the
** table, and delete rows that weren't in the input (all based on a
** UniqueKey that's part of the input)
**
** Note - the filter in the ReplaceInput - its role is to limit what rows are
** potentially deleted. e.g., if you have a table that's segmented, and you're
** only replacing a particular segment of it (say, for 1 client), then you pass
** in a filter that finds rows matching that segment. See Test for example.
*******************************************************************************/
public class ReplaceAction extends AbstractQActionFunction<ReplaceInput, ReplaceOutput>
{
private static final QLogger LOG = QLogger.getLogger(ReplaceAction.class);
/*******************************************************************************
**
*******************************************************************************/
@Override
public ReplaceOutput execute(ReplaceInput input) throws QException
{
ReplaceOutput output = new ReplaceOutput();
QBackendTransaction transaction = input.getTransaction();
boolean weOwnTheTransaction = false;
try
{
QTableMetaData table = input.getTable();
UniqueKey uniqueKey = input.getKey();
String primaryKeyField = table.getPrimaryKeyField();
if(transaction == null)
{
InsertInput insertInput = new InsertInput();
insertInput.setTableName(input.getTableName());
transaction = new InsertAction().openTransaction(insertInput);
weOwnTheTransaction = true;
}
List<QRecord> insertList = new ArrayList<>();
List<QRecord> updateList = new ArrayList<>();
List<Serializable> primaryKeysToKeep = new ArrayList<>();
for(List<QRecord> page : CollectionUtils.getPages(input.getRecords(), 1000))
{
///////////////////////////////////////////////////////////////////////////////////
// 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);
for(QRecord record : page)
{
Optional<List<Serializable>> keyValues = UniqueKeyHelper.getKeyValues(table, uniqueKey, record);
if(keyValues.isPresent())
{
if(existingKeys.containsKey(keyValues.get()))
{
Serializable primaryKey = existingKeys.get(keyValues.get());
record.setValue(primaryKeyField, primaryKey);
updateList.add(record);
primaryKeysToKeep.add(primaryKey);
}
else
{
insertList.add(record);
}
}
}
}
InsertInput insertInput = new InsertInput();
insertInput.setTableName(table.getName());
insertInput.setRecords(insertList);
insertInput.setTransaction(transaction);
insertInput.setOmitDmlAudit(input.getOmitDmlAudit());
InsertOutput insertOutput = new InsertAction().execute(insertInput);
primaryKeysToKeep.addAll(insertOutput.getRecords().stream().map(r -> r.getValue(primaryKeyField)).toList());
output.setInsertOutput(insertOutput);
UpdateInput updateInput = new UpdateInput();
updateInput.setTableName(table.getName());
updateInput.setRecords(updateList);
updateInput.setTransaction(transaction);
updateInput.setOmitDmlAudit(input.getOmitDmlAudit());
UpdateOutput updateOutput = new UpdateAction().execute(updateInput);
output.setUpdateOutput(updateOutput);
QQueryFilter deleteFilter = new QQueryFilter(new QFilterCriteria(primaryKeyField, QCriteriaOperator.NOT_IN, primaryKeysToKeep));
if(input.getFilter() != null)
{
deleteFilter.addSubFilter(input.getFilter());
}
DeleteInput deleteInput = new DeleteInput();
deleteInput.setTableName(table.getName());
deleteInput.setQueryFilter(deleteFilter);
deleteInput.setTransaction(transaction);
deleteInput.setOmitDmlAudit(input.getOmitDmlAudit());
DeleteOutput deleteOutput = new DeleteAction().execute(deleteInput);
output.setDeleteOutput(deleteOutput);
if(weOwnTheTransaction)
{
transaction.commit();
}
return (output);
}
catch(Exception e)
{
if(weOwnTheTransaction)
{
LOG.warn("Caught top-level ReplaceAction exception - rolling back exception", e);
transaction.rollback();
}
throw (new QException("Error executing replace action", e));
}
finally
{
if(weOwnTheTransaction)
{
transaction.close();
}
}
}
}

View File

@ -61,8 +61,6 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn; import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn;
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData; import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLockFilters;
import com.kingsrook.qqq.backend.core.model.metadata.tables.Association; import com.kingsrook.qqq.backend.core.model.metadata.tables.Association;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.statusmessages.BadInputStatusMessage; import com.kingsrook.qqq.backend.core.model.statusmessages.BadInputStatusMessage;
@ -85,28 +83,6 @@ public class UpdateAction
/*******************************************************************************
**
*******************************************************************************/
public QRecord executeForRecord(UpdateInput updateInput) throws QException
{
UpdateOutput updateOutput = new UpdateAction().execute(updateInput);
return (updateOutput.getRecords().get(0));
}
/*******************************************************************************
**
*******************************************************************************/
public static List<QRecord> executeForRecords(UpdateInput updateInput) throws QException
{
UpdateOutput updateOutput = new UpdateAction().execute(updateInput);
return (updateOutput.getRecords());
}
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
@ -211,16 +187,14 @@ public class UpdateAction
{ {
validateRecordsExistAndCanBeAccessed(updateInput, oldRecordList.get()); validateRecordsExistAndCanBeAccessed(updateInput, oldRecordList.get());
} }
else
{
ValidateRecordSecurityLockHelper.validateSecurityFields(table, updateInput.getRecords(), ValidateRecordSecurityLockHelper.Action.UPDATE);
}
if(updateInput.getInputSource().shouldValidateRequiredFields()) if(updateInput.getInputSource().shouldValidateRequiredFields())
{ {
validateRequiredFields(updateInput); validateRequiredFields(updateInput);
} }
ValidateRecordSecurityLockHelper.validateSecurityFields(table, updateInput.getRecords(), ValidateRecordSecurityLockHelper.Action.UPDATE);
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////
// after all validations, run the pre-update customizer, if there is one // // after all validations, run the pre-update customizer, if there is one //
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////
@ -291,8 +265,6 @@ public class UpdateAction
QTableMetaData table = updateInput.getTable(); QTableMetaData table = updateInput.getTable();
QFieldMetaData primaryKeyField = table.getField(table.getPrimaryKeyField()); QFieldMetaData primaryKeyField = table.getField(table.getPrimaryKeyField());
List<RecordSecurityLock> onlyWriteLocks = RecordSecurityLockFilters.filterForOnlyWriteLocks(CollectionUtils.nonNullList(table.getRecordSecurityLocks()));
for(List<QRecord> page : CollectionUtils.getPages(updateInput.getRecords(), 1000)) for(List<QRecord> page : CollectionUtils.getPages(updateInput.getRecords(), 1000))
{ {
List<Serializable> primaryKeysToLookup = new ArrayList<>(); List<Serializable> primaryKeysToLookup = new ArrayList<>();
@ -326,8 +298,6 @@ public class UpdateAction
} }
} }
ValidateRecordSecurityLockHelper.validateSecurityFields(table, updateInput.getRecords(), ValidateRecordSecurityLockHelper.Action.UPDATE);
for(QRecord record : page) for(QRecord record : page)
{ {
Serializable value = ValueUtils.getValueAsFieldType(primaryKeyField.getType(), record.getValue(table.getPrimaryKeyField())); Serializable value = ValueUtils.getValueAsFieldType(primaryKeyField.getType(), record.getValue(table.getPrimaryKeyField()));
@ -340,19 +310,6 @@ public class UpdateAction
{ {
record.addError(new NotFoundStatusMessage("No record was found to update for " + primaryKeyField.getLabel() + " = " + value)); record.addError(new NotFoundStatusMessage("No record was found to update for " + primaryKeyField.getLabel() + " = " + value));
} }
else
{
///////////////////////////////////////////////////////////////////////////////////////////
// if the table has any write-only locks, validate their values here, on the old-records //
///////////////////////////////////////////////////////////////////////////////////////////
for(RecordSecurityLock lock : onlyWriteLocks)
{
QRecord oldRecord = lookedUpRecords.get(value);
QFieldType fieldType = table.getField(lock.getFieldName()).getType();
Serializable lockValue = ValueUtils.getValueAsFieldType(fieldType, oldRecord.getValue(lock.getFieldName()));
ValidateRecordSecurityLockHelper.validateRecordSecurityValue(table, record, lock, lockValue, fieldType, ValidateRecordSecurityLockHelper.Action.UPDATE);
}
}
} }
} }
} }
@ -417,11 +374,6 @@ public class UpdateAction
////////////////////////////////////////////////////// //////////////////////////////////////////////////////
for(QRecord record : page) for(QRecord record : page)
{ {
if(CollectionUtils.nullSafeHasContents(record.getErrors()))
{
continue;
}
if(record.getAssociatedRecords() != null && record.getAssociatedRecords().containsKey(association.getName())) if(record.getAssociatedRecords() != null && record.getAssociatedRecords().containsKey(association.getName()))
{ {
/////////////////////////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////////////////////////

View File

@ -1,109 +0,0 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.tables.helpers;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
/*******************************************************************************
** For actions that may want to set a timeout, and cancel themselves if they run
** too long - this class helps.
**
** Construct with the timeout (delay & timeUnit), and a runnable that takes care
** of doing the cancel (e.g., cancelling a JDBC statement).
**
** Call start() to make a future get scheduled (note, if delay was null or <= 0,
** then it doesn't get scheduled at all).
**
** Call cancel() if the action got far enough/completed, to cancel the future.
**
** You can check didTimeout (getDidTimeout()) to know if the timeout did occur.
*******************************************************************************/
public class ActionTimeoutHelper
{
private final Integer delay;
private final TimeUnit timeUnit;
private final Runnable runnable;
private ScheduledFuture<?> future;
private boolean didTimeout = false;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public ActionTimeoutHelper(Integer delay, TimeUnit timeUnit, Runnable runnable)
{
this.delay = delay;
this.timeUnit = timeUnit;
this.runnable = runnable;
}
/*******************************************************************************
**
*******************************************************************************/
public void start()
{
if(delay == null || delay <= 0)
{
return;
}
future = Executors.newSingleThreadScheduledExecutor().schedule(() ->
{
didTimeout = true;
runnable.run();
}, delay, timeUnit);
}
/*******************************************************************************
**
*******************************************************************************/
public void cancel()
{
if(future != null)
{
future.cancel(true);
}
}
/*******************************************************************************
** Getter for didTimeout
**
*******************************************************************************/
public boolean getDidTimeout()
{
return didTimeout;
}
}

View File

@ -1,104 +0,0 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.tables.helpers;
import java.time.Instant;
import com.kingsrook.qqq.backend.core.logging.LogPair;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
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.cache.CacheUseCase;
import com.kingsrook.qqq.backend.core.modules.backend.implementations.utils.BackendQueryFilterUtils;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
/*******************************************************************************
**
*******************************************************************************/
public class CacheUtils
{
private static final QLogger LOG = QLogger.getLogger(CacheUtils.class);
/*******************************************************************************
**
*******************************************************************************/
static QRecord mapSourceRecordToCacheRecord(QTableMetaData table, QRecord recordFromSource, CacheUseCase cacheUseCase)
{
QRecord cacheRecord = new QRecord(recordFromSource);
//////////////////////////////////////////////////////////////////////////////////////////////
// make sure every value in the qRecord is set, because we will possibly be doing an update //
// on this record and want to null out any fields not set, not leave them populated //
//////////////////////////////////////////////////////////////////////////////////////////////
for(String fieldName : table.getFields().keySet())
{
if(fieldName.equals(table.getPrimaryKeyField()))
{
if(!cacheUseCase.getDoCopySourcePrimaryKeyToCache())
{
cacheRecord.removeValue(fieldName);
}
}
else if(!cacheRecord.getValues().containsKey(fieldName))
{
cacheRecord.setValue(fieldName, null);
}
}
if(StringUtils.hasContent(table.getCacheOf().getCachedDateFieldName()))
{
cacheRecord.setValue(table.getCacheOf().getCachedDateFieldName(), Instant.now());
}
return (cacheRecord);
}
/*******************************************************************************
**
*******************************************************************************/
static boolean shouldCacheRecord(QTableMetaData table, QRecord recordToCache)
{
boolean shouldCacheRecord = true;
recordMatchExclusionLoop:
for(CacheUseCase useCase : CollectionUtils.nonNullList(table.getCacheOf().getUseCases()))
{
for(QQueryFilter filter : CollectionUtils.nonNullList(useCase.getExcludeRecordsMatching()))
{
if(BackendQueryFilterUtils.doesRecordMatch(filter, recordToCache))
{
LOG.info("Not caching record because it matches a use case's filter exclusion", new LogPair("record", recordToCache), new LogPair("filter", filter));
shouldCacheRecord = false;
break recordMatchExclusionLoop;
}
}
}
return (shouldCacheRecord);
}
}

View File

@ -1,241 +0,0 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.tables.helpers;
import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
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.get.GetInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
/*******************************************************************************
**
*******************************************************************************/
public class GetActionCacheHelper
{
private static final QLogger LOG = QLogger.getLogger(GetActionCacheHelper.class);
/*******************************************************************************
**
*******************************************************************************/
public void handleCaching(GetInput getInput, GetOutput getOutput) throws QException
{
///////////////////////////////////////////////////////
// copy Get input & output into Query input & output //
///////////////////////////////////////////////////////
QueryInput queryInput = GetAction.convertGetInputToQueryInput(getInput);
QueryOutput queryOutput = new QueryOutput(queryInput);
if(getOutput.getRecord() != null)
{
queryOutput.addRecord(getOutput.getRecord());
}
////////////////////////////////////
// run the QueryActionCacheHelper //
////////////////////////////////////
new QueryActionCacheHelper().handleCaching(queryInput, queryOutput);
///////////////////////////////////
// set result back in get output //
///////////////////////////////////
if(CollectionUtils.nullSafeHasContents(queryOutput.getRecords()))
{
getOutput.setRecord(queryOutput.getRecords().get(0));
}
else
{
getOutput.setRecord(null);
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// In July 2023, initial caching was added in QueryAction. //
// at this time, it felt wrong to essentially duplicate this code between Get & Query - as Get is a simplified use-case of Query. //
// so - we'll keep this code here, as a potential quick/easy fallback - but - see above - where we use QueryActionCacheHelper instead. //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/*
public void handleCaching(GetInput getInput, GetOutput getOutput) throws QException
{
if(getOutput.getRecord() == null)
{
///////////////////////////////////////////////////////////////////////
// if the record wasn't found, see if we should look in cache-source //
///////////////////////////////////////////////////////////////////////
QRecord recordFromSource = tryToGetFromCacheSource(getInput);
if(recordFromSource != null)
{
/////////////////////////////////////////////////////////////////////////////////////////////////
// good, we found a record from the source, make sure we should cache it, and if so, do it now //
// note, we always return the record from the source, even if we don't cache it. //
/////////////////////////////////////////////////////////////////////////////////////////////////
QTableMetaData table = getInput.getTable();
QRecord recordToCache = CacheUtils.mapSourceRecordToCacheRecord(table, recordFromSource);
getOutput.setRecord(recordToCache);
boolean shouldCacheRecord = CacheUtils.shouldCacheRecord(table, recordToCache);
if(shouldCacheRecord)
{
InsertInput insertInput = new InsertInput();
insertInput.setTableName(getInput.getTableName());
insertInput.setRecords(List.of(recordToCache));
InsertOutput insertOutput = new InsertAction().execute(insertInput);
/////////////////////////////////////////////////////////////////////////////////////////////
// update the result record from the insert (e.g., so we get its id, just in case we care) //
/////////////////////////////////////////////////////////////////////////////////////////////
getOutput.setRecord(insertOutput.getRecords().get(0));
}
}
}
else
{
/////////////////////////////////////////////////////////////////////////////////
// if the record was found, but it's too old, maybe re-fetch from cache source //
/////////////////////////////////////////////////////////////////////////////////
refreshCacheIfExpired(getInput, getOutput);
}
}
private QRecord tryToGetFromCacheSource(GetInput getInput) throws QException
{
QRecord recordFromSource = null;
QTableMetaData table = getInput.getTable();
for(CacheUseCase cacheUseCase : CollectionUtils.nonNullList(table.getCacheOf().getUseCases()))
{
if(CacheUseCase.Type.UNIQUE_KEY_TO_UNIQUE_KEY.equals(cacheUseCase.getType()) && getInput.getUniqueKey() != null)
{
recordFromSource = getFromCachedSourceForUniqueKeyToUniqueKey(getInput, table.getCacheOf().getSourceTable());
break;
}
else
{
// todo!!
throw new NotImplementedException("Not-yet-implemented cache use case type: " + cacheUseCase.getType());
}
}
return (recordFromSource);
}
private void refreshCacheIfExpired(GetInput getInput, GetOutput getOutput) throws QException
{
QTableMetaData table = getInput.getTable();
Integer expirationSeconds = table.getCacheOf().getExpirationSeconds();
if(expirationSeconds != null)
{
QRecord cachedRecord = getOutput.getRecord();
Instant cachedDate = cachedRecord.getValueInstant(table.getCacheOf().getCachedDateFieldName());
if(cachedDate == null || cachedDate.isBefore(Instant.now().minus(expirationSeconds, ChronoUnit.SECONDS)))
{
//////////////////////////////////////////////////////////////////////////
// keep the serial key from the old record in case we need to delete it //
//////////////////////////////////////////////////////////////////////////
Serializable oldRecordPrimaryKey = cachedRecord.getValue(table.getPrimaryKeyField());
boolean shouldDeleteCachedRecord;
///////////////////////////////////////////
// fetch record from original source now //
///////////////////////////////////////////
QRecord recordFromSource = tryToGetFromCacheSource(getInput);
if(recordFromSource != null)
{
///////////////////////////////////////////////////////////////////
// if the record was found in the source, put it into the output //
// object so returned back to caller //
///////////////////////////////////////////////////////////////////
QRecord recordToCache = CacheUtils.mapSourceRecordToCacheRecord(table, recordFromSource);
recordToCache.setValue(table.getPrimaryKeyField(), cachedRecord.getValue(table.getPrimaryKeyField()));
getOutput.setRecord(recordToCache);
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if the record should be cached, update the cache record - else set the flag to delete the cached record. //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
if(CacheUtils.shouldCacheRecord(table, recordToCache))
{
UpdateInput updateInput = new UpdateInput();
updateInput.setTableName(getInput.getTableName());
updateInput.setRecords(List.of(recordToCache));
UpdateOutput updateOutput = new UpdateAction().execute(updateInput);
getOutput.setRecord(updateOutput.getRecords().get(0));
shouldDeleteCachedRecord = false;
}
else
{
shouldDeleteCachedRecord = true;
}
}
else
{
///////////////////////////////////////////////////////////////////////////////////////
// if we did not get a record back from the source, empty out the getOutput's record //
// and set the flag to delete the cached record //
///////////////////////////////////////////////////////////////////////////////////////
getOutput.setRecord(null);
shouldDeleteCachedRecord = true;
}
if(shouldDeleteCachedRecord)
{
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if the record is no longer in the source (or it was in the source, but failed the should-cache check), then remove it from the cache //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
DeleteInput deleteInput = new DeleteInput();
deleteInput.setTableName(getInput.getTableName());
deleteInput.setPrimaryKeys(List.of(oldRecordPrimaryKey));
new DeleteAction().execute(deleteInput);
}
}
}
}
private QRecord getFromCachedSourceForUniqueKeyToUniqueKey(GetInput getInput, String sourceTableName) throws QException
{
/////////////////////////////////////////////////////
// do a Get on the source table, by the unique key //
/////////////////////////////////////////////////////
GetInput sourceGetInput = new GetInput();
sourceGetInput.setTableName(sourceTableName);
sourceGetInput.setUniqueKey(getInput.getUniqueKey());
GetOutput sourceGetOutput = new GetAction().execute(sourceGetInput);
QRecord outputRecord = sourceGetOutput.getRecord();
return (outputRecord);
}
*/
}

View File

@ -1,622 +0,0 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.tables.helpers;
import java.io.Serializable;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import com.kingsrook.qqq.backend.core.actions.tables.DeleteAction;
import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
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.metadata.tables.UniqueKey;
import com.kingsrook.qqq.backend.core.model.metadata.tables.cache.CacheUseCase;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.ListingHash;
import org.apache.commons.lang.NotImplementedException;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
** After running a query, if it's for a table that's a CacheOf another table,
** see if there are any cache use-cases to apply to the query result.
**
** Such as:
** - if it's a query for one or more values in a UniqueKey:
** - if any particular UniqueKeys weren't found, look in the source table
** - if any cached records are expired, refresh them from the source
** - possibly updating the cached record; possibly deleting it.
*******************************************************************************/
public class QueryActionCacheHelper
{
private static final QLogger LOG = QLogger.getLogger(QueryActionCacheHelper.class);
private boolean isQueryInputCacheable = false;
private Map<CacheUseCase.Type, CacheUseCase> cacheUseCaseMap = new HashMap<>();
private CacheUseCase activeCacheUseCase = null;
private UniqueKey cacheUniqueKey = null;
private ListingHash<String, Serializable> uniqueKeyValues = new ListingHash<>();
/*******************************************************************************
**
*******************************************************************************/
public void handleCaching(QueryInput queryInput, QueryOutput queryOutput) throws QException
{
analyzeInput(queryInput);
if(!isQueryInputCacheable)
{
return;
}
//////////////////////////////////////////////////////////////////////////
// figure out which keys in the query were found, and which were missed //
//////////////////////////////////////////////////////////////////////////
List<QRecord> recordsFoundInCache = new ArrayList<>(queryOutput.getRecords());
Set<List<Serializable>> uniqueKeyValuesInFoundRecords = getUniqueKeyValuesFromFoundRecords(queryOutput.getRecords());
Set<List<Serializable>> missedUniqueKeyValues = getUniqueKeyValuesFromQuery();
missedUniqueKeyValues.removeAll(uniqueKeyValuesInFoundRecords);
///////////////////////////////////////////////////////////////////////////////////
// if any requested records weren't found, see if we should look in cache-source //
///////////////////////////////////////////////////////////////////////////////////
if(CollectionUtils.nullSafeHasContents(missedUniqueKeyValues))
{
List<QRecord> recordsFromSource = tryToGetFromCacheSource(queryInput, missedUniqueKeyValues);
if(CollectionUtils.nullSafeHasContents(recordsFromSource))
{
//////////////////////////////////////////////////////////////////////////////////////////////////
// good, we found records from the source, make sure we should cache them, and if so, do it now //
// note, we always return the record from the source, even if we don't cache it. //
//////////////////////////////////////////////////////////////////////////////////////////////////
QTableMetaData table = queryInput.getTable();
List<QRecord> recordsToReturn = recordsFromSource.stream()
.map(r -> CacheUtils.mapSourceRecordToCacheRecord(table, r, activeCacheUseCase))
.toList();
queryOutput.addRecords(recordsToReturn);
List<QRecord> recordsToCache = recordsToReturn.stream()
.filter(r -> CacheUtils.shouldCacheRecord(table, r))
.toList();
if(CollectionUtils.nullSafeHasContents(recordsToCache))
{
try
{
InsertInput insertInput = new InsertInput();
insertInput.setTableName(queryInput.getTableName());
insertInput.setRecords(recordsToCache);
insertInput.setSkipUniqueKeyCheck(true);
InsertOutput insertOutput = new InsertAction().execute(insertInput);
//////////////////////////////////////////////////////////
// set the (generated) ids in the records being returne //
//////////////////////////////////////////////////////////
Map<List<Serializable>, QRecord> insertedRecordsByUniqueKey = new HashMap<>();
for(QRecord record : insertOutput.getRecords())
{
insertedRecordsByUniqueKey.put(getUniqueKeyValues(record), record);
}
for(QRecord record : recordsToReturn)
{
QRecord insertedRecord = insertedRecordsByUniqueKey.get(getUniqueKeyValues(record));
if(insertedRecord != null)
{
record.setValue(table.getPrimaryKeyField(), insertedRecord.getValue(table.getPrimaryKeyField()));
}
}
}
catch(Exception e)
{
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// don't let an exception break this query - it (probably) just indicates some data that didn't get cached - so - that's generally "ok" //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
LOG.warn("Error inserting cached records", e, logPair("cacheTable", queryInput.getTableName()));
}
}
}
}
//////////////////////////////////////////////////////////////////////////
// for records that were found, if they're too old, maybe re-fetch them //
//////////////////////////////////////////////////////////////////////////
if(CollectionUtils.nullSafeHasContents(recordsFoundInCache))
{
refreshCacheIfExpired(recordsFoundInCache, queryInput, queryOutput);
}
}
/*******************************************************************************
**
*******************************************************************************/
private void refreshCacheIfExpired(List<QRecord> recordsFoundInCache, QueryInput queryInput, QueryOutput queryOutput) throws QException
{
QTableMetaData table = queryInput.getTable();
Integer expirationSeconds = table.getCacheOf().getExpirationSeconds();
if(expirationSeconds != null)
{
List<QRecord> expiredRecords = new ArrayList<>();
for(QRecord cachedRecord : recordsFoundInCache)
{
Instant cachedDate = cachedRecord.getValueInstant(table.getCacheOf().getCachedDateFieldName());
if(cachedDate == null || cachedDate.isBefore(Instant.now().minus(expirationSeconds, ChronoUnit.SECONDS)))
{
expiredRecords.add(cachedRecord);
}
}
if(CollectionUtils.nullSafeHasContents(expiredRecords))
{
Map<List<Serializable>, Serializable> uniqueKeyToPrimaryKeyMap = getUniqueKeyToPrimaryKeyMap(table.getPrimaryKeyField(), expiredRecords);
Set<List<Serializable>> uniqueKeyValuesToRefresh = uniqueKeyToPrimaryKeyMap.keySet();
////////////////////////////////////////////
// fetch records from original source now //
////////////////////////////////////////////
List<QRecord> recordsFromSource = tryToGetFromCacheSource(queryInput, uniqueKeyValuesToRefresh);
Set<List<Serializable>> uniqueKeyValuesInFoundRecords = getUniqueKeyValuesFromFoundRecords(recordsFromSource);
Set<List<Serializable>> missedUniqueKeyValues = getUniqueKeyValuesFromQuery();
missedUniqueKeyValues.retainAll(getUniqueKeyValuesFromFoundRecords(expiredRecords));
missedUniqueKeyValues.removeAll(uniqueKeyValuesInFoundRecords);
//////////////////////////////////////////////////////////////////////////////////////////////////////
// build records to cache - setting their original (from cache) ids back in them, so they'll update //
//////////////////////////////////////////////////////////////////////////////////////////////////////
List<QRecord> refreshedRecordsToReturn = recordsFromSource.stream()
.map(r ->
{
QRecord recordToCache = CacheUtils.mapSourceRecordToCacheRecord(table, r, activeCacheUseCase);
recordToCache.setValue(table.getPrimaryKeyField(), uniqueKeyToPrimaryKeyMap.get(getUniqueKeyValues(recordToCache)));
return (recordToCache);
})
.toList();
///////////////////////////////////////////////////////////////////////////////////////////////////////
// if the records were found in the source, put it into the output object so returned back to caller //
///////////////////////////////////////////////////////////////////////////////////////////////////////
Map<List<Serializable>, QRecord> refreshedRecordsByUniqueKeyValues = refreshedRecordsToReturn.stream().collect(Collectors.toMap(this::getUniqueKeyValues, r -> r, (a, b) -> a));
ListIterator<QRecord> queryOutputListIterator = queryOutput.getRecords().listIterator();
while(queryOutputListIterator.hasNext())
{
QRecord originalRecord = queryOutputListIterator.next();
List<Serializable> recordUniqueKeyValues = getUniqueKeyValues(originalRecord);
QRecord refreshedRecord = refreshedRecordsByUniqueKeyValues.get(recordUniqueKeyValues);
if(refreshedRecord != null)
{
queryOutputListIterator.set(refreshedRecord);
}
else if(missedUniqueKeyValues.contains(recordUniqueKeyValues))
{
queryOutputListIterator.remove();
}
}
////////////////////////////////////////////////////////////////////////////
// for refreshed records which should be cached, update them in the cache //
////////////////////////////////////////////////////////////////////////////
List<QRecord> recordsToUpdate = refreshedRecordsToReturn.stream().filter(r -> CacheUtils.shouldCacheRecord(table, r)).toList();
if(CollectionUtils.nullSafeHasContents(recordsToUpdate))
{
try
{
UpdateInput updateInput = new UpdateInput();
updateInput.setTableName(queryInput.getTableName());
updateInput.setRecords(recordsToUpdate);
UpdateOutput updateOutput = new UpdateAction().execute(updateInput);
}
catch(Exception e)
{
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// don't let an exception break this query - it (probably) just indicates some data that didn't get cached - so - that's generally "ok" //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
LOG.warn("Error updating cached records", e, logPair("cacheTable", queryInput.getTableName()));
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if the records were missed in the source - OR if they shouldn't be cached now, then mark them for deleting //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
Set<Serializable> cachedRecordIdsToDelete = missedUniqueKeyValues.stream()
.map(uniqueKeyToPrimaryKeyMap::get)
.collect(Collectors.toSet());
cachedRecordIdsToDelete.addAll(refreshedRecordsToReturn.stream()
.filter(r -> !CacheUtils.shouldCacheRecord(table, r))
.map(r -> r.getValue(table.getPrimaryKeyField()))
.collect(Collectors.toSet()));
if(CollectionUtils.nullSafeHasContents(cachedRecordIdsToDelete))
{
/////////////////////////////////////////////////////////////////////////////////
// if the records are no longer in the source, then remove them from the cache //
/////////////////////////////////////////////////////////////////////////////////
try
{
DeleteInput deleteInput = new DeleteInput();
deleteInput.setTableName(queryInput.getTableName());
deleteInput.setPrimaryKeys(new ArrayList<>(cachedRecordIdsToDelete));
new DeleteAction().execute(deleteInput);
}
catch(Exception e)
{
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// don't let an exception break this query - it (probably) just indicates some data that didn't get uncached - so - that's generally "ok" //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
LOG.warn("Error deleting cached records", e, logPair("cacheTable", queryInput.getTableName()));
}
}
}
}
}
/*******************************************************************************
**
*******************************************************************************/
private Set<List<Serializable>> getUniqueKeyValuesFromQuery()
{
Set<List<Serializable>> rs = new HashSet<>();
int noOfUniqueKeys = uniqueKeyValues.get(cacheUniqueKey.getFieldNames().get(0)).size();
for(int i = 0; i < noOfUniqueKeys; i++)
{
List<Serializable> values = new ArrayList<>();
for(String fieldName : cacheUniqueKey.getFieldNames())
{
values.add(uniqueKeyValues.get(fieldName).get(i));
}
////////////////////////////////////////////////////////////////////////////////
// critical - leave this here so hashCode from the list is correctly computed //
////////////////////////////////////////////////////////////////////////////////
rs.add(values);
}
return (rs);
}
/*******************************************************************************
**
*******************************************************************************/
private Set<List<Serializable>> getUniqueKeyValuesFromFoundRecords(List<QRecord> records)
{
return (getUniqueKeyToPrimaryKeyMap("ignore", records).keySet());
}
/*******************************************************************************
**
*******************************************************************************/
private Map<List<Serializable>, Serializable> getUniqueKeyToPrimaryKeyMap(String primaryKeyField, List<QRecord> records)
{
Map<List<Serializable>, Serializable> rs = new HashMap<>();
for(QRecord record : records)
{
List<Serializable> uniqueKeyValues = getUniqueKeyValues(record);
rs.put(uniqueKeyValues, record.getValue(primaryKeyField));
}
return (rs);
}
/*******************************************************************************
**
*******************************************************************************/
private List<Serializable> getUniqueKeyValues(QRecord record)
{
List<Serializable> uniqueKeyValues = new ArrayList<>();
for(String fieldName : cacheUniqueKey.getFieldNames())
{
uniqueKeyValues.add(record.getValue(fieldName));
}
return uniqueKeyValues;
}
/*******************************************************************************
** figure out if this was a request that we can cache records for -
** e.g., if it's a request for unique-key EQUALS or IN
** build up fields for the unique keys, the values, etc.
*******************************************************************************/
private void analyzeInput(QueryInput queryInput)
{
QTableMetaData table = queryInput.getTable();
for(CacheUseCase cacheUseCase : CollectionUtils.nonNullList(table.getCacheOf().getUseCases()))
{
cacheUseCaseMap.put(cacheUseCase.getType(), cacheUseCase);
}
if(cacheUseCaseMap.containsKey(CacheUseCase.Type.UNIQUE_KEY_TO_UNIQUE_KEY))
{
if(queryInput.getFilter() == null)
{
LOG.trace("Unable to cache: there is no filter");
return;
}
QQueryFilter filter = queryInput.getFilter();
Set<String> queryFields = new HashSet<>();
if(CollectionUtils.nullSafeHasContents(filter.getSubFilters()))
{
if(CollectionUtils.nullSafeHasContents(filter.getCriteria()))
{
LOG.trace("Unable to cache: we have sub-filters and criteria");
return;
}
if(!QQueryFilter.BooleanOperator.OR.equals(filter.getBooleanOperator()))
{
LOG.trace("Unable to cache: we have sub-filters but not an OR query");
return;
}
/////////////////////////
// look at sub-filters //
/////////////////////////
for(QQueryFilter subFilter : filter.getSubFilters())
{
Set<String> thisSubFilterFields = getQueryFieldsIfCacheableFilter(subFilter, false);
if(thisSubFilterFields == null)
{
return;
}
if(queryFields.isEmpty())
{
queryFields.addAll(thisSubFilterFields);
}
else
{
if(!queryFields.equals(thisSubFilterFields))
{
LOG.trace("Unable to cache: sub-filters have different sets of fields");
return;
}
}
}
if(doQueryFieldsMatchAUniqueKey(table, queryFields))
{
return;
}
LOG.trace("Unable to cache: we have sub-filters that do match a unique key");
return;
}
else
{
//////////////////////////////////////////
// look at the criteria in the query: //
// - build a set of field names //
// - fail upon unsupported operators //
// - collect the values in the criteria //
//////////////////////////////////////////
queryFields = getQueryFieldsIfCacheableFilter(filter, true);
if(queryFields == null)
{
return;
}
}
if(doQueryFieldsMatchAUniqueKey(table, queryFields))
{
return;
}
LOG.trace("Unable to cache: we have query fields that don't match a unique key: " + queryFields);
return;
}
LOG.trace("Unable to cache: No supported use case: " + cacheUseCaseMap.keySet());
}
/*******************************************************************************
**
*******************************************************************************/
private boolean doQueryFieldsMatchAUniqueKey(QTableMetaData table, Set<String> queryFields)
{
for(UniqueKey uniqueKey : CollectionUtils.nonNullList(table.getUniqueKeys()))
{
if(queryFields.equals(new HashSet<>(uniqueKey.getFieldNames())))
{
this.cacheUniqueKey = uniqueKey;
isQueryInputCacheable = true;
activeCacheUseCase = cacheUseCaseMap.get(CacheUseCase.Type.UNIQUE_KEY_TO_UNIQUE_KEY);
return true;
}
}
return false;
}
/*******************************************************************************
**
*******************************************************************************/
private Set<String> getQueryFieldsIfCacheableFilter(QQueryFilter filter, boolean allowOperatorIn)
{
Set<String> rs = new HashSet<>();
for(QFilterCriteria criterion : filter.getCriteria())
{
boolean isEquals = criterion.getOperator().equals(QCriteriaOperator.EQUALS);
boolean isIn = criterion.getOperator().equals(QCriteriaOperator.IN);
if(isEquals || (isIn && allowOperatorIn))
{
rs.add(criterion.getFieldName());
this.uniqueKeyValues.addAll(criterion.getFieldName(), criterion.getValues());
}
else
{
LOG.trace("Unable to cache: we have an unsupported criteria operator: " + criterion.getOperator());
isQueryInputCacheable = false;
return (null);
}
}
return (rs);
}
/*******************************************************************************
**
*******************************************************************************/
private List<QRecord> tryToGetFromCacheSource(QueryInput queryInput, Set<List<Serializable>> uniqueKeyValues) throws QException
{
List<QRecord> recordsFromSource = null;
QTableMetaData table = queryInput.getTable();
if(CacheUseCase.Type.UNIQUE_KEY_TO_UNIQUE_KEY.equals(activeCacheUseCase.getType()))
{
recordsFromSource = getFromCachedSourceForUniqueKeyToUniqueKey(queryInput, uniqueKeyValues, table.getCacheOf().getSourceTable());
}
else
{
// todo!!
throw (new NotImplementedException("Not-yet-implemented cache use case type: " + activeCacheUseCase.getType()));
}
return (recordsFromSource);
}
/*******************************************************************************
**
*******************************************************************************/
private List<QRecord> getFromCachedSourceForUniqueKeyToUniqueKey(QueryInput cacheQueryInput, Set<List<Serializable>> uniqueKeyValues, String sourceTableName) throws QException
{
QTableMetaData sourceTable = QContext.getQInstance().getTable(sourceTableName);
QBackendMetaData sourceBackend = QContext.getQInstance().getBackendForTable(sourceTableName);
if(sourceTable.isCapabilityEnabled(sourceBackend, Capability.TABLE_QUERY))
{
///////////////////////////////////////////////////////
// do a Query on the source table, by the unique key //
///////////////////////////////////////////////////////
QueryInput sourceQueryInput = new QueryInput();
sourceQueryInput.setTableName(sourceTableName);
QQueryFilter filter = new QQueryFilter().withBooleanOperator(QQueryFilter.BooleanOperator.OR);
sourceQueryInput.setFilter(filter);
sourceQueryInput.setCommonParamsFrom(cacheQueryInput);
for(List<Serializable> uniqueKeyValue : uniqueKeyValues)
{
QQueryFilter subFilter = new QQueryFilter();
filter.addSubFilter(subFilter);
for(int i = 0; i < cacheUniqueKey.getFieldNames().size(); i++)
{
subFilter.addCriteria(new QFilterCriteria(cacheUniqueKey.getFieldNames().get(i), QCriteriaOperator.EQUALS, uniqueKeyValue.get(i)));
}
}
QueryOutput sourceQueryOutput = new QueryAction().execute(sourceQueryInput);
return (sourceQueryOutput.getRecords());
}
else if(sourceTable.isCapabilityEnabled(sourceBackend, Capability.TABLE_GET))
{
///////////////////////////////////////////////////////////////////////
// if the table only supports GET, then do a GET for each unique key //
///////////////////////////////////////////////////////////////////////
List<QRecord> outputRecords = new ArrayList<>();
for(List<Serializable> uniqueKeyValue : uniqueKeyValues)
{
Map<String, Serializable> uniqueKey = new HashMap<>();
for(int i = 0; i < cacheUniqueKey.getFieldNames().size(); i++)
{
uniqueKey.put(cacheUniqueKey.getFieldNames().get(i), uniqueKeyValue.get(i));
}
GetInput getInput = new GetInput();
getInput.setTableName(sourceTableName);
getInput.setUniqueKey(uniqueKey);
getInput.setCommonParamsFrom(cacheQueryInput);
GetOutput getOutput = new GetAction().execute(getInput);
if(getOutput.getRecord() != null)
{
outputRecords.add(getOutput.getRecord());
}
}
return (outputRecords);
}
else
{
throw (new QException("Cache source table " + sourceTableName + " does not support Query or Get capability."));
}
}
}

View File

@ -44,7 +44,6 @@ import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn;
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData; import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.security.QSecurityKeyType; import com.kingsrook.qqq.backend.core.model.metadata.security.QSecurityKeyType;
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock; import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLockFilters;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.statusmessages.PermissionDeniedMessage; import com.kingsrook.qqq.backend.core.model.statusmessages.PermissionDeniedMessage;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
@ -69,7 +68,6 @@ public class ValidateRecordSecurityLockHelper
{ {
INSERT, INSERT,
UPDATE, UPDATE,
DELETE,
SELECT SELECT
} }
@ -80,7 +78,7 @@ public class ValidateRecordSecurityLockHelper
*******************************************************************************/ *******************************************************************************/
public static void validateSecurityFields(QTableMetaData table, List<QRecord> records, Action action) throws QException public static void validateSecurityFields(QTableMetaData table, List<QRecord> records, Action action) throws QException
{ {
List<RecordSecurityLock> locksToCheck = getRecordSecurityLocks(table, action); List<RecordSecurityLock> locksToCheck = getRecordSecurityLocks(table);
if(CollectionUtils.nullSafeIsEmpty(locksToCheck)) if(CollectionUtils.nullSafeIsEmpty(locksToCheck))
{ {
return; return;
@ -100,12 +98,11 @@ public class ValidateRecordSecurityLockHelper
for(QRecord record : records) for(QRecord record : records)
{ {
if(action.equals(Action.UPDATE) && !record.getValues().containsKey(field.getName()) && RecordSecurityLock.LockScope.READ_AND_WRITE.equals(recordSecurityLock.getLockScope())) if(action.equals(Action.UPDATE) && !record.getValues().containsKey(field.getName()))
{ {
///////////////////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////
// if this is a read-write lock, then if we have the record, it means we were able to read the record. // // if not updating the security field, then no error can come from it! //
// So if we're not updating the security field, then no error can come from it! // /////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////////////////////
continue; continue;
} }
@ -247,18 +244,11 @@ public class ValidateRecordSecurityLockHelper
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
private static List<RecordSecurityLock> getRecordSecurityLocks(QTableMetaData table, Action action) private static List<RecordSecurityLock> getRecordSecurityLocks(QTableMetaData table)
{ {
List<RecordSecurityLock> recordSecurityLocks = CollectionUtils.nonNullList(table.getRecordSecurityLocks()); List<RecordSecurityLock> recordSecurityLocks = table.getRecordSecurityLocks();
List<RecordSecurityLock> locksToCheck = new ArrayList<>(); List<RecordSecurityLock> locksToCheck = new ArrayList<>();
recordSecurityLocks = switch(action)
{
case INSERT, UPDATE, DELETE -> RecordSecurityLockFilters.filterForWriteLocks(recordSecurityLocks);
case SELECT -> RecordSecurityLockFilters.filterForReadLocks(recordSecurityLocks);
default -> throw (new IllegalArgumentException("Unsupported action: " + action));
};
//////////////////////////////////////// ////////////////////////////////////////
// if there are no locks, just return // // if there are no locks, just return //
//////////////////////////////////////// ////////////////////////////////////////
@ -291,7 +281,7 @@ public class ValidateRecordSecurityLockHelper
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
public static void validateRecordSecurityValue(QTableMetaData table, QRecord record, RecordSecurityLock recordSecurityLock, Serializable recordSecurityValue, QFieldType fieldType, Action action) static void validateRecordSecurityValue(QTableMetaData table, QRecord record, RecordSecurityLock recordSecurityLock, Serializable recordSecurityValue, QFieldType fieldType, Action action)
{ {
if(recordSecurityValue == null) if(recordSecurityValue == null)
{ {

View File

@ -26,8 +26,8 @@ import java.nio.file.Path;
import java.util.Map; import java.util.Map;
import com.kingsrook.qqq.backend.core.actions.AbstractQActionFunction; import com.kingsrook.qqq.backend.core.actions.AbstractQActionFunction;
import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.templates.ConvertHtmlToPdfInput; import com.kingsrook.qqq.backend.core.model.templates.ConvertHtmlToPdfInput;
import com.kingsrook.qqq.backend.core.model.actions.templates.ConvertHtmlToPdfOutput; import com.kingsrook.qqq.backend.core.model.templates.ConvertHtmlToPdfOutput;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import org.jsoup.Jsoup; import org.jsoup.Jsoup;
import org.jsoup.nodes.Document; import org.jsoup.nodes.Document;

View File

@ -28,8 +28,8 @@ import com.kingsrook.qqq.backend.core.actions.AbstractQActionFunction;
import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger; 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.AbstractActionInput;
import com.kingsrook.qqq.backend.core.model.actions.templates.RenderTemplateInput; import com.kingsrook.qqq.backend.core.model.templates.RenderTemplateInput;
import com.kingsrook.qqq.backend.core.model.actions.templates.RenderTemplateOutput; import com.kingsrook.qqq.backend.core.model.templates.RenderTemplateOutput;
import com.kingsrook.qqq.backend.core.model.templates.TemplateType; import com.kingsrook.qqq.backend.core.model.templates.TemplateType;
import com.kingsrook.qqq.backend.core.utils.StringUtils; import com.kingsrook.qqq.backend.core.utils.StringUtils;
import org.apache.velocity.VelocityContext; import org.apache.velocity.VelocityContext;
@ -62,15 +62,7 @@ public class RenderTemplateAction extends AbstractQActionFunction<RenderTemplate
if(TemplateType.VELOCITY.equals(input.getTemplateType())) if(TemplateType.VELOCITY.equals(input.getTemplateType()))
{ {
Velocity.init(); Velocity.init();
Context context = new VelocityContext(); Context context = new VelocityContext(input.getContext());
if(input.getContext() != null)
{
for(Map.Entry<String, ?> entry : input.getContext().entrySet())
{
context.put(entry.getKey(), entry.getValue());
}
}
setupEventHandlers(context); setupEventHandlers(context);

View File

@ -32,14 +32,11 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Set; import java.util.Set;
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader; import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QValueException; import com.kingsrook.qqq.backend.core.exceptions.QValueException;
import com.kingsrook.qqq.backend.core.logging.QLogger; import com.kingsrook.qqq.backend.core.logging.QLogger;
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.QCriteriaOperator;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria; 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.QQueryFilter;
@ -77,47 +74,9 @@ public class QPossibleValueTranslator
/////////////////////////////////////////////////////// ///////////////////////////////////////////////////////
private Map<String, Map<Serializable, String>> possibleValueCache = new HashMap<>(); private Map<String, Map<Serializable, String>> possibleValueCache = new HashMap<>();
private int maxSizePerPvsCache = 50_000;
private Map<String, QBackendTransaction> transactionsPerTable = new HashMap<>();
// todo not commit - remove instance & session - use Context // todo not commit - remove instance & session - use Context
boolean useTransactionsAsConnectionPool = false;
/*******************************************************************************
**
*******************************************************************************/
private QBackendTransaction getTransaction(String tableName)
{
/////////////////////////////////////////////////////////////
// mmm, this does cut down on connections used - //
// especially seems helpful in big exports. //
// but, let's just start using connection pools instead... //
/////////////////////////////////////////////////////////////
if(useTransactionsAsConnectionPool)
{
try
{
if(!transactionsPerTable.containsKey(tableName))
{
transactionsPerTable.put(tableName, new InsertAction().openTransaction(new InsertInput(tableName)));
}
return (transactionsPerTable.get(tableName));
}
catch(Exception e)
{
LOG.warn("Error opening transaction for table", logPair("tableName", tableName));
}
}
return null;
}
/******************************************************************************* /*******************************************************************************
** Constructor ** Constructor
@ -466,10 +425,9 @@ public class QPossibleValueTranslator
for(Map.Entry<String, Map<Serializable, String>> entry : possibleValueCache.entrySet()) for(Map.Entry<String, Map<Serializable, String>> entry : possibleValueCache.entrySet())
{ {
int size = entry.getValue().size(); int size = entry.getValue().size();
if(size > maxSizePerPvsCache) if(size > 50_000)
{ {
LOG.info("Found a big PVS cache - clearing it.", logPair("name", entry.getKey()), logPair("size", size)); LOG.info("Found a big PVS cache - clearing it.", logPair("name", entry.getKey()), logPair("size", size));
entry.getValue().clear();
} }
} }
@ -563,7 +521,6 @@ public class QPossibleValueTranslator
QueryInput queryInput = new QueryInput(); QueryInput queryInput = new QueryInput();
queryInput.setTableName(tableName); queryInput.setTableName(tableName);
queryInput.setFilter(new QQueryFilter().withCriteria(new QFilterCriteria(primaryKeyField, QCriteriaOperator.IN, page))); queryInput.setFilter(new QQueryFilter().withCriteria(new QFilterCriteria(primaryKeyField, QCriteriaOperator.IN, page)));
queryInput.setTransaction(getTransaction(tableName));
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// when querying for possible values, we do want to generate their display values, which makes record labels, which are usually used as PVS labels // // when querying for possible values, we do want to generate their display values, which makes record labels, which are usually used as PVS labels //
@ -656,24 +613,4 @@ public class QPossibleValueTranslator
return (count < 5); return (count < 5);
} }
/*******************************************************************************
** Getter for maxSizePerPvsCache
*******************************************************************************/
public int getMaxSizePerPvsCache()
{
return (this.maxSizePerPvsCache);
}
/*******************************************************************************
** Setter for maxSizePerPvsCache
*******************************************************************************/
public void setMaxSizePerPvsCache(int maxSizePerPvsCache)
{
this.maxSizePerPvsCache = maxSizePerPvsCache;
}
} }

View File

@ -112,12 +112,6 @@ public class QValueFormatter
{ {
return formatLocalTime(lt); return formatLocalTime(lt);
} }
//////////////////////////////////////////////////////////////////////////////////////////
// else, just return the value as a string, rather than going through String.formatted //
// this saves some overhead incurred by String.formatted when called millions of times. //
//////////////////////////////////////////////////////////////////////////////////////////
return (ValueUtils.getValueAsString(value));
} }
//////////////////////////////////////////////////////// ////////////////////////////////////////////////////////
@ -274,14 +268,6 @@ public class QValueFormatter
*******************************************************************************/ *******************************************************************************/
private static String formatRecordLabelExceptionalCases(QTableMetaData table, QRecord record) private static String formatRecordLabelExceptionalCases(QTableMetaData table, QRecord record)
{ {
//////////////////////////////////////////////////////////////////////////////////////
// if the record already has a label (say, from a query-customizer), then return it //
//////////////////////////////////////////////////////////////////////////////////////
if(record.getRecordLabel() != null)
{
return (record.getRecordLabel());
}
/////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////
// if there's no record label format, then just return the primary key display value // // if there's no record label format, then just return the primary key display value //
/////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////

View File

@ -23,8 +23,6 @@ package com.kingsrook.qqq.backend.core.actions.values;
import java.io.Serializable; import java.io.Serializable;
import java.time.Instant;
import java.time.LocalDate;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
@ -210,55 +208,44 @@ public class SearchPossibleValueSourceAction
} }
else else
{ {
String searchTerm = input.getSearchTerm(); if(StringUtils.hasContent(input.getSearchTerm()))
if(StringUtils.hasContent(searchTerm))
{ {
for(String valueField : possibleValueSource.getSearchFields()) for(String valueField : possibleValueSource.getSearchFields())
{ {
try QFieldMetaData field = table.getField(valueField);
if(field.getType().equals(QFieldType.STRING))
{ {
QFieldMetaData field = table.getField(valueField); queryFilter.addCriteria(new QFilterCriteria(valueField, QCriteriaOperator.STARTS_WITH, List.of(input.getSearchTerm())));
if(field.getType().equals(QFieldType.STRING)) }
else if(field.getType().equals(QFieldType.DATE) || field.getType().equals(QFieldType.DATE_TIME))
{
LOG.debug("Not querying PVS [" + possibleValueSource.getName() + "] on date field [" + field.getName() + "]");
// todo - what? queryFilter.addCriteria(new QFilterCriteria(valueField, QCriteriaOperator.STARTS_WITH, List.of(input.getSearchTerm())));
}
else
{
try
{ {
queryFilter.addCriteria(new QFilterCriteria(valueField, QCriteriaOperator.STARTS_WITH, List.of(searchTerm))); Integer valueAsInteger = ValueUtils.getValueAsInteger(input.getSearchTerm());
}
else if(field.getType().equals(QFieldType.DATE))
{
LocalDate searchDate = ValueUtils.getValueAsLocalDate(searchTerm);
if(searchDate != null)
{
queryFilter.addCriteria(new QFilterCriteria(valueField, QCriteriaOperator.EQUALS, searchDate));
}
}
else if(field.getType().equals(QFieldType.DATE_TIME))
{
Instant searchDate = ValueUtils.getValueAsInstant(searchTerm);
if(searchDate != null)
{
queryFilter.addCriteria(new QFilterCriteria(valueField, QCriteriaOperator.EQUALS, searchDate));
}
}
else
{
Integer valueAsInteger = ValueUtils.getValueAsInteger(searchTerm);
if(valueAsInteger != null) if(valueAsInteger != null)
{ {
queryFilter.addCriteria(new QFilterCriteria(valueField, QCriteriaOperator.EQUALS, List.of(valueAsInteger))); queryFilter.addCriteria(new QFilterCriteria(valueField, QCriteriaOperator.EQUALS, List.of(valueAsInteger)));
} }
} }
} catch(Exception e)
catch(Exception e) {
{ ////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////// // write a FALSE criteria if the value isn't a number //
// write a FALSE criteria upon exceptions (e.g., type conversion fails) // ////////////////////////////////////////////////////////
// Why are we doing this? so a single-field query finds nothing instead of everything. // queryFilter.addCriteria(new QFilterCriteria(valueField, QCriteriaOperator.IN, List.of()));
////////////////////////////////////////////////////////////////////////////////////////// }
queryFilter.addCriteria(new QFilterCriteria(valueField, QCriteriaOperator.IN, List.of()));
} }
} }
} }
} }
queryFilter.setOrderBys(possibleValueSource.getOrderByFields());
// todo - skip & limit as params // todo - skip & limit as params
queryFilter.setLimit(250); queryFilter.setLimit(250);
@ -270,9 +257,6 @@ public class SearchPossibleValueSourceAction
input.getDefaultQueryFilter().addSubFilter(queryFilter); input.getDefaultQueryFilter().addSubFilter(queryFilter);
queryFilter = input.getDefaultQueryFilter(); queryFilter = input.getDefaultQueryFilter();
} }
queryFilter.setOrderBys(possibleValueSource.getOrderByFields());
queryInput.setFilter(queryFilter); queryInput.setFilter(queryFilter);
QueryOutput queryOutput = new QueryAction().execute(queryInput); QueryOutput queryOutput = new QueryAction().execute(queryInput);

View File

@ -36,9 +36,7 @@ import com.kingsrook.qqq.backend.core.model.actions.shared.mapping.AbstractQFiel
import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.statusmessages.BadInputStatusMessage;
import com.kingsrook.qqq.backend.core.utils.StringUtils; import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import org.apache.commons.csv.CSVFormat; import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVParser; import org.apache.commons.csv.CSVParser;
import org.apache.commons.csv.CSVRecord; import org.apache.commons.csv.CSVRecord;
@ -158,21 +156,14 @@ public class CsvToQRecordAdapter
// now move values into the QRecord, using the mapping to get the 'header' corresponding to each QField // // now move values into the QRecord, using the mapping to get the 'header' corresponding to each QField //
////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////
QRecord qRecord = new QRecord(); QRecord qRecord = new QRecord();
try for(QFieldMetaData field : table.getFields().values())
{ {
for(QFieldMetaData field : table.getFields().values()) String fieldSource = mapping == null ? field.getName() : String.valueOf(mapping.getFieldSource(field.getName()));
{ fieldSource = adjustHeaderCase(fieldSource, inputWrapper);
String fieldSource = mapping == null ? field.getName() : String.valueOf(mapping.getFieldSource(field.getName())); qRecord.setValue(field.getName(), csvValues.get(fieldSource));
fieldSource = adjustHeaderCase(fieldSource, inputWrapper); }
setValue(inputWrapper, qRecord, field, csvValues.get(fieldSource));
}
runRecordCustomizer(recordCustomizer, qRecord); runRecordCustomizer(recordCustomizer, qRecord);
}
catch(Exception e)
{
qRecord.addError(new BadInputStatusMessage("Error parsing line #" + (recordCount + 1) + ": " + e.getMessage()));
}
addRecord(qRecord); addRecord(qRecord);
recordCount++; recordCount++;
@ -211,20 +202,13 @@ public class CsvToQRecordAdapter
// now move values into the QRecord, using the mapping to get the 'header' corresponding to each QField // // now move values into the QRecord, using the mapping to get the 'header' corresponding to each QField //
////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////
QRecord qRecord = new QRecord(); QRecord qRecord = new QRecord();
try for(QFieldMetaData field : table.getFields().values())
{ {
for(QFieldMetaData field : table.getFields().values()) Integer fieldIndex = (Integer) mapping.getFieldSource(field.getName());
{ qRecord.setValue(field.getName(), csvValues.get(fieldIndex));
Integer fieldIndex = (Integer) mapping.getFieldSource(field.getName()); }
setValue(inputWrapper, qRecord, field, csvValues.get(fieldIndex));
}
runRecordCustomizer(recordCustomizer, qRecord); runRecordCustomizer(recordCustomizer, qRecord);
}
catch(Exception e)
{
qRecord.addError(new BadInputStatusMessage("Error parsing line #" + (recordCount + 1) + ": " + e.getMessage()));
}
addRecord(qRecord); addRecord(qRecord);
recordCount++; recordCount++;
@ -247,23 +231,6 @@ public class CsvToQRecordAdapter
/*******************************************************************************
**
*******************************************************************************/
private void setValue(InputWrapper inputWrapper, QRecord qRecord, QFieldMetaData field, String valueString)
{
if(inputWrapper.doCorrectValueTypes)
{
qRecord.setValue(field.getName(), ValueUtils.getValueAsFieldType(field.getType(), valueString));
}
else
{
qRecord.setValue(field.getName(), valueString);
}
}
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
@ -374,7 +341,6 @@ public class CsvToQRecordAdapter
private AbstractQFieldMapping<?> mapping; private AbstractQFieldMapping<?> mapping;
private Consumer<QRecord> recordCustomizer; private Consumer<QRecord> recordCustomizer;
private Integer limit; private Integer limit;
private boolean doCorrectValueTypes = false;
private boolean caseSensitiveHeaders = false; private boolean caseSensitiveHeaders = false;
@ -616,40 +582,6 @@ public class CsvToQRecordAdapter
return (this); return (this);
} }
/*******************************************************************************
** Getter for doCorrectValueTypes
**
*******************************************************************************/
public boolean getDoCorrectValueTypes()
{
return doCorrectValueTypes;
}
/*******************************************************************************
** Setter for doCorrectValueTypes
**
*******************************************************************************/
public void setDoCorrectValueTypes(boolean doCorrectValueTypes)
{
this.doCorrectValueTypes = doCorrectValueTypes;
}
/*******************************************************************************
** Fluent setter for doCorrectValueTypes
**
*******************************************************************************/
public InputWrapper withDoCorrectValueTypes(boolean doCorrectValueTypes)
{
this.doCorrectValueTypes = doCorrectValueTypes;
return (this);
}
} }
} }

View File

@ -80,23 +80,8 @@ public class QRecordToCsvAdapter
/******************************************************************************* /*******************************************************************************
** todo - kinda weak... can we find this in a CSV lib?? ** todo - kinda weak... can we find this in a CSV lib??
*******************************************************************************/ *******************************************************************************/
static String sanitize(String value) private String sanitize(String value)
{ {
///////////////////////////////////////////////////////////////////////////////////// return (value.replaceAll("\"", "\"\"").replaceAll("\n", " "));
// especially in big exports, we see a TON of memory allocated and CPU spent here, //
// if we just blindly replaceAll. So, only do it if needed. //
/////////////////////////////////////////////////////////////////////////////////////
if(value.contains("\""))
{
value = value.replaceAll("\"", "\"\"");
}
if(value.contains("\n"))
{
value = value.replaceAll("\n", " ");
}
return (value);
} }
} }

View File

@ -84,7 +84,7 @@ public class QContext
actionStackThreadLocal.get().add(actionInput); actionStackThreadLocal.get().add(actionInput);
} }
if(qInstance != null && !qInstance.getHasBeenValidated()) if(!qInstance.getHasBeenValidated())
{ {
try try
{ {

View File

@ -77,7 +77,6 @@ import com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert.Bulk
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.ExtractViaQueryStep; 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.processes.implementations.etl.streamedwithfrontend.StreamedETLWithFrontendProcess;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.ListingHash;
import com.kingsrook.qqq.backend.core.utils.StringUtils; import com.kingsrook.qqq.backend.core.utils.StringUtils;
@ -101,7 +100,7 @@ public class QInstanceEnricher
////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////
// let an instance define mappings to be applied during name-to-label enrichments, // // let an instance define mappings to be applied during name-to-label enrichments, //
// e.g., to avoid ever incorrectly camel-casing an acronym (e.g., "Tla" should always be "TLA") // // e.g., to avoid ever incorrectly camel-casing an acronym (e.g., "Tla" shoudl always be "TLA") //
// or to expand abbreviations in code (e.g., "Addr" should always be "Address" // // or to expand abbreviations in code (e.g., "Addr" should always be "Address" //
////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////
private static final Map<String, String> labelMappings = new LinkedHashMap<>(); private static final Map<String, String> labelMappings = new LinkedHashMap<>();
@ -273,7 +272,7 @@ public class QInstanceEnricher
for(QSupplementalTableMetaData supplementalTableMetaData : CollectionUtils.nonNullMap(table.getSupplementalMetaData()).values()) for(QSupplementalTableMetaData supplementalTableMetaData : CollectionUtils.nonNullMap(table.getSupplementalMetaData()).values())
{ {
supplementalTableMetaData.enrich(qInstance, table); supplementalTableMetaData.enrich(table);
} }
} }
@ -532,70 +531,11 @@ public class QInstanceEnricher
enrichAppSection(section); enrichAppSection(section);
} }
ensureAppSectionMembersAreAppChildren(app);
enrichPermissionRules(app); enrichPermissionRules(app);
} }
/*******************************************************************************
**
*******************************************************************************/
private void ensureAppSectionMembersAreAppChildren(QAppMetaData app)
{
ListingHash<Class<? extends QAppChildMetaData>, String> childrenByType = new ListingHash<>();
childrenByType.put(QTableMetaData.class, new ArrayList<>());
childrenByType.put(QProcessMetaData.class, new ArrayList<>());
childrenByType.put(QReportMetaData.class, new ArrayList<>());
for(QAppChildMetaData qAppChildMetaData : CollectionUtils.nonNullList(app.getChildren()))
{
childrenByType.add(qAppChildMetaData.getClass(), qAppChildMetaData.getName());
}
for(QAppSection section : CollectionUtils.nonNullList(app.getSections()))
{
for(String tableName : CollectionUtils.nonNullList(section.getTables()))
{
if(!childrenByType.get(QTableMetaData.class).contains(tableName))
{
QTableMetaData table = qInstance.getTable(tableName);
if(table != null)
{
app.withChild(table);
}
}
}
for(String processName : CollectionUtils.nonNullList(section.getProcesses()))
{
if(!childrenByType.get(QProcessMetaData.class).contains(processName))
{
QProcessMetaData process = qInstance.getProcess(processName);
if(process != null)
{
app.withChild(process);
}
}
}
for(String reportName : CollectionUtils.nonNullList(section.getReports()))
{
if(!childrenByType.get(QReportMetaData.class).contains(reportName))
{
QReportMetaData report = qInstance.getReport(reportName);
if(report != null)
{
app.withChild(report);
}
}
}
}
}
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/

View File

@ -1,269 +0,0 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.instances;
import java.util.HashMap;
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 com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.helpcontent.HelpContent;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.help.HelpFormat;
import com.kingsrook.qqq.backend.core.model.metadata.help.HelpRole;
import com.kingsrook.qqq.backend.core.model.metadata.help.QHelpContent;
import com.kingsrook.qqq.backend.core.model.metadata.help.QHelpRole;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
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.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
** Utility methods for working with (dynamic, from a table) HelpContent - and
** putting it into meta-data in a QInstance.
*******************************************************************************/
public class QInstanceHelpContentManager
{
private static final QLogger LOG = QLogger.getLogger(QInstanceHelpContentManager.class);
/*******************************************************************************
**
*******************************************************************************/
public static void loadHelpContent(QInstance qInstance)
{
try
{
if(qInstance.getTable(HelpContent.TABLE_NAME) == null)
{
return;
}
QueryInput queryInput = new QueryInput();
queryInput.setTableName(HelpContent.TABLE_NAME);
QueryOutput queryOutput = new QueryAction().execute(queryInput);
for(QRecord record : queryOutput.getRecords())
{
processHelpContentRecord(qInstance, record);
}
}
catch(Exception e)
{
LOG.error("Error loading help content", e);
}
}
/*******************************************************************************
**
*******************************************************************************/
public static void processHelpContentRecord(QInstance qInstance, QRecord record)
{
try
{
/////////////////////////////////////////////////
// parse the key into its parts that we expect //
/////////////////////////////////////////////////
String key = record.getValueString("key");
Map<String, String> nameValuePairs = new HashMap<>();
for(String part : key.split(";"))
{
String[] parts = part.split(":");
nameValuePairs.put(parts[0], parts[1]);
}
String tableName = nameValuePairs.get("table");
String processName = nameValuePairs.get("process");
String fieldName = nameValuePairs.get("field");
String sectionName = nameValuePairs.get("section");
///////////////////////////////////////////////////////////
// build a help content meta-data object from the record //
///////////////////////////////////////////////////////////
QHelpContent helpContent = new QHelpContent()
.withContent(record.getValueString("content"))
.withRole(QHelpRole.valueOf(record.getValueString("role"))); // mmm, we could fall down a bit here w/ other app-defined roles...
if(StringUtils.hasContent(record.getValueString("format")))
{
helpContent.setFormat(HelpFormat.valueOf(record.getValueString("format")));
}
Set<HelpRole> roles = helpContent.getRoles();
///////////////////////////////////////////////////////////////////////////////////////////////////
// check - if there are no contents, then let's remove this help content from the container //
// (note pre-delete customizer will take advantage of this, passing in empty content on purpose) //
///////////////////////////////////////////////////////////////////////////////////////////////////
if(!StringUtils.hasContent(helpContent.getContent()))
{
helpContent = null;
}
///////////////////////////////////////////////////////////////////////////////////
// look at what parts of the key we got, and find the meta-data object to update //
///////////////////////////////////////////////////////////////////////////////////
if(StringUtils.hasContent(tableName))
{
QTableMetaData table = qInstance.getTable(tableName);
if(table == null)
{
LOG.info("Unrecognized table in help content", logPair("key", key));
return;
}
if(StringUtils.hasContent(fieldName))
{
//////////////////////////
// handle a table field //
//////////////////////////
QFieldMetaData field = table.getFields().get(fieldName);
if(field == null)
{
LOG.info("Unrecognized table field in help content", logPair("key", key));
return;
}
if(helpContent != null)
{
field.withHelpContent(helpContent);
}
else
{
field.removeHelpContent(roles);
}
}
else if(StringUtils.hasContent(sectionName))
{
////////////////////////////
// handle a table section //
////////////////////////////
Optional<QFieldSection> optionalSection = table.getSections().stream().filter(s -> sectionName.equals(s.getName())).findFirst();
if(optionalSection.isEmpty())
{
LOG.info("Unrecognized table section in help content", logPair("key", key));
return;
}
if(helpContent != null)
{
optionalSection.get().withHelpContent(helpContent);
}
else
{
optionalSection.get().removeHelpContent(roles);
}
}
}
else if(StringUtils.hasContent(processName))
{
QProcessMetaData process = qInstance.getProcess(processName);
if(process == null)
{
LOG.info("Unrecognized process in help content", logPair("key", key));
return;
}
if(StringUtils.hasContent(fieldName))
{
////////////////////////////
// handle a process field //
////////////////////////////
Optional<QFieldMetaData> optionalField = CollectionUtils.mergeLists(process.getInputFields(), process.getOutputFields())
.stream().filter(f -> fieldName.equals(f.getName()))
.findFirst();
if(optionalField.isEmpty())
{
LOG.info("Unrecognized process field in help content", logPair("key", key));
return;
}
if(helpContent != null)
{
optionalField.get().withHelpContent(helpContent);
}
else
{
optionalField.get().removeHelpContent(roles);
}
}
}
}
catch(Exception e)
{
LOG.warn("Error processing a helpContent record", e, logPair("id", record.getValue("id")));
}
}
/*******************************************************************************
** add a help content object to a list - replacing an entry in the list with the
** same roles if one is found.
*******************************************************************************/
public static void putHelpContentInList(QHelpContent helpContent, List<QHelpContent> helpContents)
{
ListIterator<QHelpContent> iterator = helpContents.listIterator();
while(iterator.hasNext())
{
QHelpContent existingContent = iterator.next();
if(Objects.equals(existingContent.getRoles(), helpContent.getRoles()))
{
iterator.set(helpContent);
return;
}
}
helpContents.add(helpContent);
}
/*******************************************************************************
** Remove any helpContent entries in a list if they have a set of roles that matches
** the input set.
*******************************************************************************/
public static void removeHelpContentByRoleSetFromList(Set<HelpRole> roles, List<QHelpContent> helpContents)
{
if(helpContents == null)
{
return;
}
helpContents.removeIf(existingContent -> Objects.equals(existingContent.getRoles(), roles));
}
}

View File

@ -438,13 +438,10 @@ public class QInstanceValidator
for(QFieldSection section : table.getSections()) for(QFieldSection section : table.getSections())
{ {
validateTableSection(qInstance, table, section, fieldNamesInSections); validateTableSection(qInstance, table, section, fieldNamesInSections);
if(assertCondition(section.getTier() != null, "Table " + tableName + " " + section.getName() + " is missing its tier")) if(section.getTier().equals(Tier.T1))
{ {
if(section.getTier().equals(Tier.T1)) assertCondition(tier1Section == null, "Table " + tableName + " has more than 1 section listed as Tier 1");
{ tier1Section = section;
assertCondition(tier1Section == null, "Table " + tableName + " has more than 1 section listed as Tier 1");
tier1Section = section;
}
} }
assertCondition(!usedSectionNames.contains(section.getName()), "Table " + tableName + " has more than 1 section named " + section.getName()); assertCondition(!usedSectionNames.contains(section.getName()), "Table " + tableName + " has more than 1 section named " + section.getName());
@ -589,8 +586,6 @@ public class QInstanceValidator
prefix = "Table " + table.getName() + " recordSecurityLock (of key type " + securityKeyTypeName + ") "; prefix = "Table " + table.getName() + " recordSecurityLock (of key type " + securityKeyTypeName + ") ";
assertCondition(recordSecurityLock.getLockScope() != null, prefix + " is missing its lockScope");
boolean hasAnyBadJoins = false; boolean hasAnyBadJoins = false;
for(String joinName : CollectionUtils.nonNullList(recordSecurityLock.getJoinNameChain())) for(String joinName : CollectionUtils.nonNullList(recordSecurityLock.getJoinNameChain()))
{ {
@ -1104,39 +1099,13 @@ public class QInstanceValidator
boolean hasFields = CollectionUtils.nullSafeHasContents(section.getFieldNames()); boolean hasFields = CollectionUtils.nullSafeHasContents(section.getFieldNames());
boolean hasWidget = StringUtils.hasContent(section.getWidgetName()); boolean hasWidget = StringUtils.hasContent(section.getWidgetName());
String sectionPrefix = "Table " + table.getName() + " section " + section.getName() + " "; if(assertCondition(hasFields || hasWidget, "Table " + table.getName() + " section " + section.getName() + " does not have any fields or a widget."))
if(assertCondition(hasFields || hasWidget, sectionPrefix + "does not have any fields or a widget."))
{ {
if(table.getFields() != null && hasFields) if(table.getFields() != null && hasFields)
{ {
for(String fieldName : section.getFieldNames()) for(String fieldName : section.getFieldNames())
{ {
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// assertCondition(table.getFields().containsKey(fieldName), "Table " + table.getName() + " section " + section.getName() + " specifies fieldName " + fieldName + ", which is not a field on this table.");
// note - this was originally written as an assertion: //
// if(assertCondition(qInstance.getTable(otherTableName) != null, sectionPrefix + "join-field " + fieldName + ", which is referencing an unrecognized table name [" + otherTableName + "]")) //
// but... then a field name with dots gives us a bad time here, so... //
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
if(fieldName.contains(".") && qInstance.getTable(fieldName.split("\\.")[0]) != null)
{
String[] parts = fieldName.split("\\.");
String otherTableName = parts[0];
String foreignFieldName = parts[1];
if(assertCondition(qInstance.getTable(otherTableName) != null, sectionPrefix + "join-field " + fieldName + ", which is referencing an unrecognized table name [" + otherTableName + "]"))
{
List<ExposedJoin> matchedExposedJoins = CollectionUtils.nonNullList(table.getExposedJoins()).stream().filter(ej -> otherTableName.equals(ej.getJoinTable())).toList();
if(assertCondition(CollectionUtils.nullSafeHasContents(matchedExposedJoins), sectionPrefix + "join-field " + fieldName + ", referencing table [" + otherTableName + "] which is not an exposed join on this table."))
{
assertCondition(!matchedExposedJoins.get(0).getIsMany(qInstance), sectionPrefix + "join-field " + fieldName + " references an is-many join, which is not supported.");
}
assertCondition(qInstance.getTable(otherTableName).getFields().containsKey(foreignFieldName), sectionPrefix + "join-field " + fieldName + " specifies a fieldName [" + foreignFieldName + "] which does not exist in that table [" + otherTableName + "].");
}
}
else
{
assertCondition(table.getFields().containsKey(fieldName), sectionPrefix + "specifies fieldName " + fieldName + ", which is not a field on this table.");
}
assertCondition(!fieldNamesInSections.contains(fieldName), "Table " + table.getName() + " has field " + fieldName + " listed more than once in its field sections."); assertCondition(!fieldNamesInSections.contains(fieldName), "Table " + table.getName() + " has field " + fieldName + " listed more than once in its field sections.");
fieldNamesInSections.add(fieldName); fieldNamesInSections.add(fieldName);
@ -1144,7 +1113,7 @@ public class QInstanceValidator
} }
else if(hasWidget) else if(hasWidget)
{ {
assertCondition(qInstance.getWidget(section.getWidgetName()) != null, sectionPrefix + "specifies widget " + section.getWidgetName() + ", which is not a widget in this instance."); assertCondition(qInstance.getWidget(section.getWidgetName()) != null, "Table " + table.getName() + " section " + section.getName() + " specifies widget " + section.getWidgetName() + ", which is not a widget in this instance.");
} }
} }
} }
@ -1255,10 +1224,11 @@ public class QInstanceValidator
QScheduleMetaData schedule = process.getSchedule(); 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); assertCondition(schedule.getRepeatMillis() != null || schedule.getRepeatSeconds() != null, "Either repeat millis or repeat seconds must be set on schedule in process " + processName);
if(schedule.getVariantBackend() != null) if(schedule.getBackendVariant() != null)
{ {
assertCondition(qInstance.getBackend(schedule.getVariantBackend()) != null, "A variant backend was not found for " + schedule.getVariantBackend()); assertCondition(schedule.getVariantRunStrategy() != null, "A variant strategy was not set for " + schedule.getBackendVariant() + " on schedule in process " + processName);
assertCondition(schedule.getVariantRunStrategy() != null, "A variant run strategy was not set for " + schedule.getVariantBackend() + " on schedule in process " + processName); assertCondition(schedule.getVariantTableName() != null, "A variant table name was not set for " + schedule.getBackendVariant() + " on schedule in process " + processName);
assertCondition(schedule.getVariantFieldName() != null, "A variant field name was not set for " + schedule.getBackendVariant() + " on schedule in process " + processName);
} }
} }

View File

@ -270,32 +270,6 @@ public class QMetaDataVariableInterpreter
/*******************************************************************************
** First look for a string in the specified system property -
** Next look for a string in the specified env var name -
** Finally return the default.
*******************************************************************************/
public String getStringFromPropertyOrEnvironment(String systemPropertyName, String environmentVariableName, String defaultIfNotSet)
{
String propertyValue = System.getProperty(systemPropertyName);
if(StringUtils.hasContent(propertyValue))
{
LOG.info("Read system property [" + systemPropertyName + "] as [" + propertyValue + "].");
return (propertyValue);
}
String envValue = interpret("${env." + environmentVariableName + "}");
if(StringUtils.hasContent(envValue))
{
LOG.info("Read env var [" + environmentVariableName + "] as [" + envValue + "].");
return (envValue);
}
return defaultIfNotSet;
}
/******************************************************************************* /*******************************************************************************
** First look for a boolean ("true" or "false") in the specified system property - ** First look for a boolean ("true" or "false") in the specified system property -
** Next look for a boolean in the specified env var name - ** Next look for a boolean in the specified env var name -
@ -373,7 +347,7 @@ public class QMetaDataVariableInterpreter
if(canParseAsInteger(envValue)) if(canParseAsInteger(envValue))
{ {
LOG.info("Read env var [" + environmentVariableName + "] as integer " + environmentVariableName); LOG.info("Read env var [" + environmentVariableName + "] as integer " + environmentVariableName);
return (Integer.parseInt(envValue)); return (Integer.parseInt(propertyValue));
} }
else else
{ {

View File

@ -26,7 +26,6 @@ import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import com.kingsrook.qqq.backend.core.instances.QMetaDataVariableInterpreter;
import com.kingsrook.qqq.backend.core.utils.lambdas.UnsafeSupplier; import com.kingsrook.qqq.backend.core.utils.lambdas.UnsafeSupplier;
@ -35,17 +34,6 @@ import com.kingsrook.qqq.backend.core.utils.lambdas.UnsafeSupplier;
*******************************************************************************/ *******************************************************************************/
public class LogUtils public class LogUtils
{ {
///////////////////////////////////////////////////////////////////////////////////////////////
// This string will be used in regex, inside ()'s, so you can supply pipe-delimited packages //
// as in, com.kingsrook|com.yourdomain|org.some.other.package //
///////////////////////////////////////////////////////////////////////////////////////////////
private static String packagesToKeep = ".";
static
{
packagesToKeep = new QMetaDataVariableInterpreter().getStringFromPropertyOrEnvironment("qqq.logger.packagesToKeep", "QQQ_LOGGER_PACKAGES_TO_KEEP", ".");
}
/******************************************************************************* /*******************************************************************************
** **
@ -130,8 +118,9 @@ public class LogUtils
{ {
try try
{ {
StringBuilder rs = new StringBuilder(); String packagesToKeep = "com.kingsrook|com.coldtrack"; // todo - parameterize!!
String[] lines = stackTrace.split("\n"); StringBuilder rs = new StringBuilder();
String[] lines = stackTrace.split("\n");
int indexWithinSubStack = 0; int indexWithinSubStack = 0;
int skipsInThisPackage = 0; int skipsInThisPackage = 0;
@ -145,13 +134,7 @@ public class LogUtils
{ {
keepLine = false; keepLine = false;
indexWithinSubStack++; indexWithinSubStack++;
if(line.matches("^\\s+at (" + packagesToKeep + ").*"))
/////////////////////////////////////////////////////////////////////////////
// avoid NPE on packages to keep (and keep all packages) //
// also, avoid the regex call if it's the default of "." (e.g., match all) //
// otherwise, check if the line matches "at (packagesToKeep).*" //
/////////////////////////////////////////////////////////////////////////////
if(packagesToKeep == null || ".".equals(packagesToKeep) || line.matches("^\\s+at (" + packagesToKeep + ").*"))
{ {
keepLine = true; keepLine = true;
} }

View File

@ -124,7 +124,7 @@ public class QLogger
*******************************************************************************/ *******************************************************************************/
public void log(Level level, String message) public void log(Level level, String message)
{ {
logger.log(level, () -> makeJsonString(message)); logger.log(level, makeJsonString(message));
} }
@ -134,7 +134,7 @@ public class QLogger
*******************************************************************************/ *******************************************************************************/
public void log(Level level, String message, Throwable t) public void log(Level level, String message, Throwable t)
{ {
logger.log(level, () -> makeJsonString(message, t)); logger.log(level, makeJsonString(message, t));
} }
@ -144,7 +144,7 @@ public class QLogger
*******************************************************************************/ *******************************************************************************/
public void log(Level level, String message, Throwable t, LogPair... logPairs) public void log(Level level, String message, Throwable t, LogPair... logPairs)
{ {
logger.log(level, () -> makeJsonString(message, t, logPairs)); logger.log(level, makeJsonString(message, t, logPairs));
} }
@ -154,7 +154,7 @@ public class QLogger
*******************************************************************************/ *******************************************************************************/
public void log(Level level, Throwable t) public void log(Level level, Throwable t)
{ {
logger.log(level, () -> makeJsonString(null, t)); logger.log(level, makeJsonString(null, t));
} }
@ -164,7 +164,7 @@ public class QLogger
*******************************************************************************/ *******************************************************************************/
public void trace(String message) public void trace(String message)
{ {
logger.trace(() -> makeJsonString(message)); logger.trace(makeJsonString(message));
} }
@ -174,7 +174,7 @@ public class QLogger
*******************************************************************************/ *******************************************************************************/
public void trace(String message, LogPair... logPairs) public void trace(String message, LogPair... logPairs)
{ {
logger.trace(() -> makeJsonString(message, null, logPairs)); logger.trace(makeJsonString(message, null, logPairs));
} }
@ -194,7 +194,7 @@ public class QLogger
*******************************************************************************/ *******************************************************************************/
public void trace(String message, Throwable t) public void trace(String message, Throwable t)
{ {
logger.trace(() -> makeJsonString(message, t)); logger.trace(makeJsonString(message, t));
} }
@ -204,7 +204,7 @@ public class QLogger
*******************************************************************************/ *******************************************************************************/
public void trace(String message, Throwable t, LogPair... logPairs) public void trace(String message, Throwable t, LogPair... logPairs)
{ {
logger.trace(() -> makeJsonString(message, t, logPairs)); logger.trace(makeJsonString(message, t, logPairs));
} }
@ -214,7 +214,7 @@ public class QLogger
*******************************************************************************/ *******************************************************************************/
public void trace(Throwable t) public void trace(Throwable t)
{ {
logger.trace(() -> makeJsonString(null, t)); logger.trace(makeJsonString(null, t));
} }
@ -224,7 +224,7 @@ public class QLogger
*******************************************************************************/ *******************************************************************************/
public void debug(String message) public void debug(String message)
{ {
logger.debug(() -> makeJsonString(message)); logger.debug(makeJsonString(message));
} }
@ -234,7 +234,7 @@ public class QLogger
*******************************************************************************/ *******************************************************************************/
public void debug(String message, LogPair... logPairs) public void debug(String message, LogPair... logPairs)
{ {
logger.debug(() -> makeJsonString(message, null, logPairs)); logger.debug(makeJsonString(message, null, logPairs));
} }
@ -254,7 +254,7 @@ public class QLogger
*******************************************************************************/ *******************************************************************************/
public void debug(String message, Throwable t) public void debug(String message, Throwable t)
{ {
logger.debug(() -> makeJsonString(message, t)); logger.debug(makeJsonString(message, t));
} }
@ -264,7 +264,7 @@ public class QLogger
*******************************************************************************/ *******************************************************************************/
public void debug(String message, Throwable t, LogPair... logPairs) public void debug(String message, Throwable t, LogPair... logPairs)
{ {
logger.debug(() -> makeJsonString(message, t, logPairs)); logger.debug(makeJsonString(message, t, logPairs));
} }
@ -274,7 +274,7 @@ public class QLogger
*******************************************************************************/ *******************************************************************************/
public void debug(Throwable t) public void debug(Throwable t)
{ {
logger.debug(() -> makeJsonString(null, t)); logger.debug(makeJsonString(null, t));
} }
@ -284,7 +284,7 @@ public class QLogger
*******************************************************************************/ *******************************************************************************/
public void info(String message) public void info(String message)
{ {
logger.info(() -> makeJsonString(message)); logger.info(makeJsonString(message));
} }
@ -294,7 +294,7 @@ public class QLogger
*******************************************************************************/ *******************************************************************************/
public void info(LogPair... logPairs) public void info(LogPair... logPairs)
{ {
logger.info(() -> makeJsonString(null, null, logPairs)); logger.info(makeJsonString(null, null, logPairs));
} }
@ -304,7 +304,7 @@ public class QLogger
*******************************************************************************/ *******************************************************************************/
public void info(List<LogPair> logPairList) public void info(List<LogPair> logPairList)
{ {
logger.info(() -> makeJsonString(null, null, logPairList)); logger.info(makeJsonString(null, null, logPairList));
} }
@ -314,7 +314,7 @@ public class QLogger
*******************************************************************************/ *******************************************************************************/
public void info(String message, LogPair... logPairs) public void info(String message, LogPair... logPairs)
{ {
logger.info(() -> makeJsonString(message, null, logPairs)); logger.info(makeJsonString(message, null, logPairs));
} }
@ -334,7 +334,7 @@ public class QLogger
*******************************************************************************/ *******************************************************************************/
public void info(String message, Throwable t) public void info(String message, Throwable t)
{ {
logger.info(() -> makeJsonString(message, t)); logger.info(makeJsonString(message, t));
} }
@ -344,7 +344,7 @@ public class QLogger
*******************************************************************************/ *******************************************************************************/
public void info(String message, Throwable t, LogPair... logPairs) public void info(String message, Throwable t, LogPair... logPairs)
{ {
logger.info(() -> makeJsonString(message, t, logPairs)); logger.info(makeJsonString(message, t, logPairs));
} }
@ -354,7 +354,7 @@ public class QLogger
*******************************************************************************/ *******************************************************************************/
public void info(Throwable t) public void info(Throwable t)
{ {
logger.info(() -> makeJsonString(null, t)); logger.info(makeJsonString(null, t));
} }
@ -364,7 +364,7 @@ public class QLogger
*******************************************************************************/ *******************************************************************************/
public void warn(String message) public void warn(String message)
{ {
logger.warn(() -> makeJsonString(message)); logger.warn(makeJsonString(message));
} }
@ -374,7 +374,7 @@ public class QLogger
*******************************************************************************/ *******************************************************************************/
public void warn(String message, LogPair... logPairs) public void warn(String message, LogPair... logPairs)
{ {
logger.warn(() -> makeJsonString(message, null, logPairs)); logger.warn(makeJsonString(message, null, logPairs));
} }
@ -394,7 +394,7 @@ public class QLogger
*******************************************************************************/ *******************************************************************************/
public void warn(String message, Throwable t) public void warn(String message, Throwable t)
{ {
logger.log(determineIfShouldDowngrade(t, Level.WARN), () -> makeJsonString(message, t)); logger.log(determineIfShouldDowngrade(t, Level.WARN), makeJsonString(message, t));
} }
@ -404,7 +404,7 @@ public class QLogger
*******************************************************************************/ *******************************************************************************/
public void warn(String message, Throwable t, LogPair... logPairs) public void warn(String message, Throwable t, LogPair... logPairs)
{ {
logger.log(determineIfShouldDowngrade(t, Level.WARN), () -> makeJsonString(message, t, logPairs)); logger.log(determineIfShouldDowngrade(t, Level.WARN), makeJsonString(message, t, logPairs));
} }
@ -414,7 +414,7 @@ public class QLogger
*******************************************************************************/ *******************************************************************************/
public void warn(Throwable t) public void warn(Throwable t)
{ {
logger.log(determineIfShouldDowngrade(t, Level.WARN), () -> makeJsonString(null, t)); logger.log(determineIfShouldDowngrade(t, Level.WARN), makeJsonString(null, t));
} }
@ -424,7 +424,7 @@ public class QLogger
*******************************************************************************/ *******************************************************************************/
public void error(String message) public void error(String message)
{ {
logger.error(() -> makeJsonString(message)); logger.error(makeJsonString(message));
} }
@ -434,7 +434,7 @@ public class QLogger
*******************************************************************************/ *******************************************************************************/
public void error(String message, LogPair... logPairs) public void error(String message, LogPair... logPairs)
{ {
logger.error(() -> makeJsonString(message, null, logPairs)); logger.error(makeJsonString(message, null, logPairs));
} }
@ -454,7 +454,7 @@ public class QLogger
*******************************************************************************/ *******************************************************************************/
public void error(String message, Throwable t) public void error(String message, Throwable t)
{ {
logger.log(determineIfShouldDowngrade(t, Level.ERROR), () -> makeJsonString(message, t)); logger.log(determineIfShouldDowngrade(t, Level.ERROR), makeJsonString(message, t));
} }
@ -464,7 +464,7 @@ public class QLogger
*******************************************************************************/ *******************************************************************************/
public void error(String message, Throwable t, LogPair... logPairs) public void error(String message, Throwable t, LogPair... logPairs)
{ {
logger.log(determineIfShouldDowngrade(t, Level.ERROR), () -> makeJsonString(message, t, logPairs)); logger.log(determineIfShouldDowngrade(t, Level.ERROR), makeJsonString(message, t, logPairs));
} }
@ -474,7 +474,7 @@ public class QLogger
*******************************************************************************/ *******************************************************************************/
public void error(Throwable t) public void error(Throwable t)
{ {
logger.log(determineIfShouldDowngrade(t, Level.ERROR), () -> makeJsonString(null, t)); logger.log(determineIfShouldDowngrade(t, Level.ERROR), makeJsonString(null, t));
} }

View File

@ -1,78 +0,0 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model;
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;
/*******************************************************************************
** Interface for classes that know how to produce meta data objects. Useful with
** MetaDataProducerHelper, to put point at a package full of these, and populate
** your whole QInstance.
**
** See also MetaDataProducer - an implementer of this interface, which actually
** came first, and is fine to extend if producing a meta-data class is all your
** clas means to do (nice and "Single-responsibility principle").
**
** But, in some applications you may want to, for example, have one class that
** defines a process step, and also produces the meta-data for that process, so
** your whole process can just be one class - so then just have your step class
** implement this interface. or, same idea for a QRecordEntity that provides
** its own TableMetaData.
*******************************************************************************/
public interface MetaDataProducerInterface<T extends TopLevelMetaDataInterface>
{
int DEFAULT_SORT_ORDER = 500;
/*******************************************************************************
** Produce the metaData object. Generally, you don't want to add it to the instance
** yourself - but the instance is there in case you need it to get other metaData.
*******************************************************************************/
T produce(QInstance qInstance) throws QException;
/*******************************************************************************
** In case this producer needs to run before (or after) others, this method
** can control influence that (e.g., if used by MetaDataProducerHelper).
**
** Smaller values run first.
*******************************************************************************/
default int getSortOrder()
{
return (DEFAULT_SORT_ORDER);
}
/*******************************************************************************
** turn this producer on or off - e.g., maybe based on an env value.
**
*******************************************************************************/
default boolean isEnabled()
{
return (true);
}
}

View File

@ -155,4 +155,14 @@ public class AbstractActionInput
this.asyncJobCallback = asyncJobCallback; this.asyncJobCallback = asyncJobCallback;
} }
/*******************************************************************************
** Fluent setter for instance
*******************************************************************************/
public AbstractActionInput withInstance(QInstance instance)
{
return (this);
}
} }

View File

@ -28,12 +28,8 @@ import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import com.kingsrook.qqq.backend.core.actions.audits.AuditAction;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock; import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLockFilters;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
@ -55,41 +51,6 @@ public class AuditSingleInput
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public AuditSingleInput()
{
}
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public AuditSingleInput(QTableMetaData table, QRecord record, String auditMessage)
{
setAuditTableName(table.getName());
setRecordId(record.getValueInteger(table.getPrimaryKeyField()));
setSecurityKeyValues(AuditAction.getRecordSecurityKeyValues(table, record, Optional.empty()));
setMessage(auditMessage);
}
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public AuditSingleInput(String tableName, QRecord record, String auditMessage)
{
this(QContext.getQInstance().getTable(tableName), record, auditMessage);
}
/******************************************************************************* /*******************************************************************************
** Getter for auditTableName ** Getter for auditTableName
*******************************************************************************/ *******************************************************************************/
@ -285,7 +246,7 @@ public class AuditSingleInput
setAuditTableName(table.getName()); setAuditTableName(table.getName());
this.securityKeyValues = new HashMap<>(); this.securityKeyValues = new HashMap<>();
for(RecordSecurityLock recordSecurityLock : RecordSecurityLockFilters.filterForReadLocks(CollectionUtils.nonNullList(table.getRecordSecurityLocks()))) for(RecordSecurityLock recordSecurityLock : CollectionUtils.nonNullList(table.getRecordSecurityLocks()))
{ {
this.securityKeyValues.put(recordSecurityLock.getFieldName(), record.getValueInteger(recordSecurityLock.getFieldName())); this.securityKeyValues.put(recordSecurityLock.getFieldName(), record.getValueInteger(recordSecurityLock.getFieldName()));
} }

View File

@ -131,17 +131,6 @@ public class ProcessSummaryFilterLink implements ProcessSummaryLineInterface
/*******************************************************************************
**
*******************************************************************************/
@Override
public String getMessage()
{
return getFullText();
}
/******************************************************************************* /*******************************************************************************
** Setter for status ** Setter for status
** **

View File

@ -182,7 +182,6 @@ public class ProcessSummaryLine implements ProcessSummaryLineInterface
** Getter for message ** Getter for message
** **
*******************************************************************************/ *******************************************************************************/
@Override
public String getMessage() public String getMessage()
{ {
return message; return message;

View File

@ -38,10 +38,6 @@ public interface ProcessSummaryLineInterface extends Serializable
*******************************************************************************/ *******************************************************************************/
Status getStatus(); Status getStatus();
/*******************************************************************************
**
*******************************************************************************/
String getMessage();
/******************************************************************************* /*******************************************************************************
** meant to be called by framework, after process is complete, give the ** meant to be called by framework, after process is complete, give the

View File

@ -95,17 +95,6 @@ public class ProcessSummaryRecordLink implements ProcessSummaryLineInterface
/*******************************************************************************
**
*******************************************************************************/
@Override
public String getMessage()
{
return getFullText();
}
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/

View File

@ -32,7 +32,6 @@ import com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput;
public class RunAssociatedScriptOutput extends AbstractActionOutput public class RunAssociatedScriptOutput extends AbstractActionOutput
{ {
private Serializable output; private Serializable output;
private Integer scriptRevisionId;
@ -68,36 +67,4 @@ public class RunAssociatedScriptOutput extends AbstractActionOutput
return (this); return (this);
} }
/*******************************************************************************
** Getter for scriptRevisionId
*******************************************************************************/
public Integer getScriptRevisionId()
{
return (this.scriptRevisionId);
}
/*******************************************************************************
** Setter for scriptRevisionId
*******************************************************************************/
public void setScriptRevisionId(Integer scriptRevisionId)
{
this.scriptRevisionId = scriptRevisionId;
}
/*******************************************************************************
** Fluent setter for scriptRevisionId
*******************************************************************************/
public RunAssociatedScriptOutput withScriptRevisionId(Integer scriptRevisionId)
{
this.scriptRevisionId = scriptRevisionId;
return (this);
}
} }

View File

@ -1,165 +0,0 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.actions.tables;
import java.util.Collection;
import java.util.List;
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryJoin;
/*******************************************************************************
** Common getters & setters, shared by both QueryInput and GetInput.
**
** Original impetus for this class is the setCommonParamsFrom() method - for cases
** where we need to change a Query to a Get, or vice-versa, and we want to copy over
** all of those input params.
*******************************************************************************/
public interface QueryOrGetInputInterface
{
/*******************************************************************************
** Set in THIS, the "common params" (e.g., common to both Query & Get inputs)
** from the parameter SOURCE object.
*******************************************************************************/
default void setCommonParamsFrom(QueryOrGetInputInterface source)
{
this.setTransaction(source.getTransaction());
this.setShouldTranslatePossibleValues(source.getShouldTranslatePossibleValues());
this.setShouldGenerateDisplayValues(source.getShouldGenerateDisplayValues());
this.setShouldFetchHeavyFields(source.getShouldFetchHeavyFields());
this.setShouldOmitHiddenFields(source.getShouldOmitHiddenFields());
this.setShouldMaskPasswords(source.getShouldMaskPasswords());
this.setIncludeAssociations(source.getIncludeAssociations());
this.setAssociationNamesToInclude(source.getAssociationNamesToInclude());
this.setQueryJoins(source.getQueryJoins());
}
/*******************************************************************************
** Getter for transaction
*******************************************************************************/
QBackendTransaction getTransaction();
/*******************************************************************************
** Setter for transaction
*******************************************************************************/
void setTransaction(QBackendTransaction transaction);
/*******************************************************************************
** Getter for shouldTranslatePossibleValues
*******************************************************************************/
boolean getShouldTranslatePossibleValues();
/*******************************************************************************
** Setter for shouldTranslatePossibleValues
*******************************************************************************/
void setShouldTranslatePossibleValues(boolean shouldTranslatePossibleValues);
/*******************************************************************************
** Getter for shouldGenerateDisplayValues
*******************************************************************************/
boolean getShouldGenerateDisplayValues();
/*******************************************************************************
** Setter for shouldGenerateDisplayValues
*******************************************************************************/
void setShouldGenerateDisplayValues(boolean shouldGenerateDisplayValues);
/*******************************************************************************
** Getter for shouldFetchHeavyFields
*******************************************************************************/
boolean getShouldFetchHeavyFields();
/*******************************************************************************
** Setter for shouldFetchHeavyFields
*******************************************************************************/
void setShouldFetchHeavyFields(boolean shouldFetchHeavyFields);
/*******************************************************************************
** Getter for shouldOmitHiddenFields
*******************************************************************************/
boolean getShouldOmitHiddenFields();
/*******************************************************************************
** Setter for shouldOmitHiddenFields
*******************************************************************************/
void setShouldOmitHiddenFields(boolean shouldOmitHiddenFields);
/*******************************************************************************
** Getter for shouldMaskPasswords
*******************************************************************************/
boolean getShouldMaskPasswords();
/*******************************************************************************
** Setter for shouldMaskPasswords
*******************************************************************************/
void setShouldMaskPasswords(boolean shouldMaskPasswords);
/*******************************************************************************
** Getter for includeAssociations
*******************************************************************************/
boolean getIncludeAssociations();
/*******************************************************************************
** Setter for includeAssociations
*******************************************************************************/
void setIncludeAssociations(boolean includeAssociations);
/*******************************************************************************
** Getter for associationNamesToInclude
*******************************************************************************/
Collection<String> getAssociationNamesToInclude();
/*******************************************************************************
** Setter for associationNamesToInclude
*******************************************************************************/
void setAssociationNamesToInclude(Collection<String> associationNamesToInclude);
/*******************************************************************************
** Getter for queryJoins
*******************************************************************************/
List<QueryJoin> getQueryJoins();
/*******************************************************************************
** Setter for queryJoins
**
*******************************************************************************/
void setQueryJoins(List<QueryJoin> queryJoins);
}

View File

@ -40,8 +40,6 @@ public class AggregateInput extends AbstractTableActionInput
private List<GroupBy> groupBys = new ArrayList<>(); private List<GroupBy> groupBys = new ArrayList<>();
private Integer limit; private Integer limit;
private Integer timeoutSeconds;
private List<QueryJoin> queryJoins = null; private List<QueryJoin> queryJoins = null;
@ -271,35 +269,4 @@ public class AggregateInput extends AbstractTableActionInput
return (this); return (this);
} }
/*******************************************************************************
** Getter for timeoutSeconds
*******************************************************************************/
public Integer getTimeoutSeconds()
{
return (this.timeoutSeconds);
}
/*******************************************************************************
** Setter for timeoutSeconds
*******************************************************************************/
public void setTimeoutSeconds(Integer timeoutSeconds)
{
this.timeoutSeconds = timeoutSeconds;
}
/*******************************************************************************
** Fluent setter for timeoutSeconds
*******************************************************************************/
public AggregateInput withTimeoutSeconds(Integer timeoutSeconds)
{
this.timeoutSeconds = timeoutSeconds;
return (this);
}
} }

View File

@ -37,8 +37,6 @@ public class CountInput extends AbstractTableActionInput
{ {
private QQueryFilter filter; private QQueryFilter filter;
private Integer timeoutSeconds;
private List<QueryJoin> queryJoins = null; private List<QueryJoin> queryJoins = null;
private Boolean includeDistinctCount = false; private Boolean includeDistinctCount = false;
@ -53,17 +51,6 @@ public class CountInput extends AbstractTableActionInput
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public CountInput(String tableName)
{
setTableName(tableName);
}
/******************************************************************************* /*******************************************************************************
** Getter for filter ** Getter for filter
** **
@ -165,46 +152,4 @@ public class CountInput extends AbstractTableActionInput
return (this); return (this);
} }
/*******************************************************************************
** Fluent setter for filter
*******************************************************************************/
public CountInput withFilter(QQueryFilter filter)
{
this.filter = filter;
return (this);
}
/*******************************************************************************
** Getter for timeoutSeconds
*******************************************************************************/
public Integer getTimeoutSeconds()
{
return (this.timeoutSeconds);
}
/*******************************************************************************
** Setter for timeoutSeconds
*******************************************************************************/
public void setTimeoutSeconds(Integer timeoutSeconds)
{
this.timeoutSeconds = timeoutSeconds;
}
/*******************************************************************************
** Fluent setter for timeoutSeconds
*******************************************************************************/
public CountInput withTimeoutSeconds(Integer timeoutSeconds)
{
this.timeoutSeconds = timeoutSeconds;
return (this);
}
} }

View File

@ -43,9 +43,6 @@ public class DeleteInput extends AbstractTableActionInput
private QQueryFilter queryFilter; private QQueryFilter queryFilter;
private InputSource inputSource = QInputSource.SYSTEM; private InputSource inputSource = QInputSource.SYSTEM;
private boolean omitDmlAudit = false;
private String auditContext = null;
/******************************************************************************* /*******************************************************************************
@ -57,29 +54,6 @@ public class DeleteInput extends AbstractTableActionInput
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public DeleteInput(String tableName)
{
setTableName(tableName);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public DeleteInput withTableName(String tableName)
{
super.withTableName(tableName);
return (this);
}
/******************************************************************************* /*******************************************************************************
** Getter for transaction ** Getter for transaction
** **
@ -214,66 +188,4 @@ public class DeleteInput extends AbstractTableActionInput
return (this); return (this);
} }
/*******************************************************************************
** Getter for omitDmlAudit
*******************************************************************************/
public boolean getOmitDmlAudit()
{
return (this.omitDmlAudit);
}
/*******************************************************************************
** Setter for omitDmlAudit
*******************************************************************************/
public void setOmitDmlAudit(boolean omitDmlAudit)
{
this.omitDmlAudit = omitDmlAudit;
}
/*******************************************************************************
** Fluent setter for omitDmlAudit
*******************************************************************************/
public DeleteInput withOmitDmlAudit(boolean omitDmlAudit)
{
this.omitDmlAudit = omitDmlAudit;
return (this);
}
/*******************************************************************************
** Getter for auditContext
*******************************************************************************/
public String getAuditContext()
{
return (this.auditContext);
}
/*******************************************************************************
** Setter for auditContext
*******************************************************************************/
public void setAuditContext(String auditContext)
{
this.auditContext = auditContext;
}
/*******************************************************************************
** Fluent setter for auditContext
*******************************************************************************/
public DeleteInput withAuditContext(String auditContext)
{
this.auditContext = auditContext;
return (this);
}
} }

View File

@ -23,21 +23,17 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.get;
import java.io.Serializable; import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.List;
import java.util.Map; import java.util.Map;
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction; import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput; import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.QueryOrGetInputInterface;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryJoin;
/******************************************************************************* /*******************************************************************************
** Input data for the Get action ** Input data for the Get action
** **
*******************************************************************************/ *******************************************************************************/
public class GetInput extends AbstractTableActionInput implements QueryOrGetInputInterface public class GetInput extends AbstractTableActionInput
{ {
private QBackendTransaction transaction; private QBackendTransaction transaction;
@ -50,7 +46,6 @@ public class GetInput extends AbstractTableActionInput implements QueryOrGetInpu
private boolean shouldOmitHiddenFields = true; private boolean shouldOmitHiddenFields = true;
private boolean shouldMaskPasswords = true; private boolean shouldMaskPasswords = true;
private List<QueryJoin> queryJoins = null;
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if you say you want to includeAssociations, you can limit which ones by passing them in associationNamesToInclude. // // if you say you want to includeAssociations, you can limit which ones by passing them in associationNamesToInclude. //
@ -71,29 +66,6 @@ public class GetInput extends AbstractTableActionInput implements QueryOrGetInpu
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public GetInput(String tableName)
{
setTableName(tableName);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public AbstractTableActionInput withTableName(String tableName)
{
super.withTableName(tableName);
return (this);
}
/******************************************************************************* /*******************************************************************************
** Getter for primaryKey ** Getter for primaryKey
** **
@ -415,51 +387,4 @@ public class GetInput extends AbstractTableActionInput implements QueryOrGetInpu
return (this); return (this);
} }
/*******************************************************************************
** Getter for queryJoins
*******************************************************************************/
public List<QueryJoin> getQueryJoins()
{
return (this.queryJoins);
}
/*******************************************************************************
** Setter for queryJoins
*******************************************************************************/
public void setQueryJoins(List<QueryJoin> queryJoins)
{
this.queryJoins = queryJoins;
}
/*******************************************************************************
** Fluent setter for queryJoins
*******************************************************************************/
public GetInput withQueryJoins(List<QueryJoin> queryJoins)
{
this.queryJoins = queryJoins;
return (this);
}
/*******************************************************************************
** Fluent setter for queryJoins
**
*******************************************************************************/
public GetInput withQueryJoin(QueryJoin queryJoin)
{
if(this.queryJoins == null)
{
this.queryJoins = new ArrayList<>();
}
this.queryJoins.add(queryJoin);
return (this);
}
} }

View File

@ -22,15 +22,12 @@
package com.kingsrook.qqq.backend.core.model.actions.tables.insert; package com.kingsrook.qqq.backend.core.model.actions.tables.insert;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction; import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput; import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.InputSource; import com.kingsrook.qqq.backend.core.model.actions.tables.InputSource;
import com.kingsrook.qqq.backend.core.model.actions.tables.QInputSource; import com.kingsrook.qqq.backend.core.model.actions.tables.QInputSource;
import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
/******************************************************************************* /*******************************************************************************
@ -46,7 +43,6 @@ public class InsertInput extends AbstractTableActionInput
private boolean skipUniqueKeyCheck = false; private boolean skipUniqueKeyCheck = false;
private boolean omitDmlAudit = false; private boolean omitDmlAudit = false;
private String auditContext = null;
@ -59,71 +55,6 @@ public class InsertInput extends AbstractTableActionInput
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public InsertInput(String tableName)
{
setTableName(tableName);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public InsertInput withTableName(String tableName)
{
super.withTableName(tableName);
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
public InsertInput withRecord(QRecord record)
{
if(records == null)
{
records = new ArrayList<>();
}
records.add(record);
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
public InsertInput withRecordEntity(QRecordEntity recordEntity)
{
return (withRecord(recordEntity.toQRecord()));
}
/*******************************************************************************
**
*******************************************************************************/
public InsertInput withRecordEntities(List<QRecordEntity> recordEntityList)
{
for(QRecordEntity recordEntity : CollectionUtils.nonNullList(recordEntityList))
{
withRecordEntity(recordEntity);
}
return (this);
}
/******************************************************************************* /*******************************************************************************
** Getter for transaction ** Getter for transaction
** **
@ -285,35 +216,4 @@ public class InsertInput extends AbstractTableActionInput
return (this); return (this);
} }
/*******************************************************************************
** Getter for auditContext
*******************************************************************************/
public String getAuditContext()
{
return (this.auditContext);
}
/*******************************************************************************
** Setter for auditContext
*******************************************************************************/
public void setAuditContext(String auditContext)
{
this.auditContext = auditContext;
}
/*******************************************************************************
** Fluent setter for auditContext
*******************************************************************************/
public InsertInput withAuditContext(String auditContext)
{
this.auditContext = auditContext;
return (this);
}
} }

View File

@ -30,21 +30,17 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors;
import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.LogPair;
import com.kingsrook.qqq.backend.core.logging.QLogger; 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.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData; import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock; import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLockFilters;
import com.kingsrook.qqq.backend.core.model.metadata.tables.ExposedJoin; import com.kingsrook.qqq.backend.core.model.metadata.tables.ExposedJoin;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.collections.MutableList; import com.kingsrook.qqq.backend.core.utils.collections.MutableList;
import org.apache.logging.log4j.Level;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
@ -64,7 +60,6 @@ public class JoinsContext
// note - will have entries for all tables, not just aliases. // // note - will have entries for all tables, not just aliases. //
//////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////
private final Map<String, String> aliasToTableNameMap = new HashMap<>(); private final Map<String, String> aliasToTableNameMap = new HashMap<>();
private Level logLevel = Level.OFF;
@ -74,23 +69,62 @@ public class JoinsContext
*******************************************************************************/ *******************************************************************************/
public JoinsContext(QInstance instance, String tableName, List<QueryJoin> queryJoins, QQueryFilter filter) throws QException public JoinsContext(QInstance instance, String tableName, List<QueryJoin> queryJoins, QQueryFilter filter) throws QException
{ {
log("--- START ----------------------------------------------------------------------", logPair("mainTable", tableName));
this.instance = instance; this.instance = instance;
this.mainTableName = tableName; this.mainTableName = tableName;
this.queryJoins = new MutableList<>(queryJoins); this.queryJoins = new MutableList<>(queryJoins);
for(QueryJoin queryJoin : this.queryJoins) for(QueryJoin queryJoin : this.queryJoins)
{ {
log("Processing input query join", logPair("joinTable", queryJoin.getJoinTable()), logPair("alias", queryJoin.getAlias()), logPair("baseTableOrAlias", queryJoin.getBaseTableOrAlias()), logPair("joinMetaDataName", () -> queryJoin.getJoinMetaData().getName()));
processQueryJoin(queryJoin); processQueryJoin(queryJoin);
} }
/////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////
// ensure any joins that contribute a recordLock are present // // ensure any joins that contribute a recordLock are present //
/////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////
for(RecordSecurityLock recordSecurityLock : RecordSecurityLockFilters.filterForReadLocks(CollectionUtils.nonNullList(instance.getTable(tableName).getRecordSecurityLocks()))) for(RecordSecurityLock recordSecurityLock : CollectionUtils.nonNullList(instance.getTable(tableName).getRecordSecurityLocks()))
{ {
ensureRecordSecurityLockIsRepresented(instance, tableName, recordSecurityLock); ///////////////////////////////////////////////////////////////////////////////////////////////////
// ok - so - the join name chain is going to be like this: //
// for a table: orderLineItemExtrinsic (that's 2 away from order, where the security field is): //
// - securityFieldName = order.clientId //
// - joinNameChain = orderJoinOrderLineItem, orderLineItemJoinOrderLineItemExtrinsic //
// so - to navigate from the table to the security field, we need to reverse the joinNameChain, //
// and step (via tmpTable variable) back to the securityField //
///////////////////////////////////////////////////////////////////////////////////////////////////
ArrayList<String> joinNameChain = new ArrayList<>(CollectionUtils.nonNullList(recordSecurityLock.getJoinNameChain()));
Collections.reverse(joinNameChain);
QTableMetaData tmpTable = instance.getTable(mainTableName);
for(String joinName : joinNameChain)
{
if(this.queryJoins.stream().anyMatch(queryJoin ->
{
QJoinMetaData joinMetaData = Objects.requireNonNullElseGet(queryJoin.getJoinMetaData(), () -> findJoinMetaData(instance, tableName, queryJoin.getJoinTable()));
return (joinMetaData != null && Objects.equals(joinMetaData.getName(), joinName));
}))
{
continue;
}
QJoinMetaData join = instance.getJoin(joinName);
if(join.getLeftTable().equals(tmpTable.getName()))
{
QueryJoin queryJoin = new ImplicitQueryJoinForSecurityLock().withJoinMetaData(join).withType(QueryJoin.Type.INNER);
this.addQueryJoin(queryJoin);
tmpTable = instance.getTable(join.getRightTable());
}
else if(join.getRightTable().equals(tmpTable.getName()))
{
QueryJoin queryJoin = new ImplicitQueryJoinForSecurityLock().withJoinMetaData(join.flip()).withType(QueryJoin.Type.INNER);
this.addQueryJoin(queryJoin); //
tmpTable = instance.getTable(join.getLeftTable());
}
else
{
throw (new QException("Error adding security lock joins to query - table name [" + tmpTable.getName() + "] not found in join [" + joinName + "]"));
}
}
} }
ensureFilterIsRepresented(filter); ensureFilterIsRepresented(filter);
@ -107,86 +141,6 @@ public class JoinsContext
} }
} }
*/ */
log("Constructed JoinsContext", logPair("mainTableName", this.mainTableName), logPair("queryJoins", this.queryJoins.stream().map(qj -> qj.getJoinTable()).collect(Collectors.joining(","))));
log("--- END ------------------------------------------------------------------------");
}
/*******************************************************************************
**
*******************************************************************************/
private void ensureRecordSecurityLockIsRepresented(QInstance instance, String tableName, RecordSecurityLock recordSecurityLock) throws QException
{
///////////////////////////////////////////////////////////////////////////////////////////////////
// ok - so - the join name chain is going to be like this: //
// for a table: orderLineItemExtrinsic (that's 2 away from order, where the security field is): //
// - securityFieldName = order.clientId //
// - joinNameChain = orderJoinOrderLineItem, orderLineItemJoinOrderLineItemExtrinsic //
// so - to navigate from the table to the security field, we need to reverse the joinNameChain, //
// and step (via tmpTable variable) back to the securityField //
///////////////////////////////////////////////////////////////////////////////////////////////////
ArrayList<String> joinNameChain = new ArrayList<>(CollectionUtils.nonNullList(recordSecurityLock.getJoinNameChain()));
Collections.reverse(joinNameChain);
log("Evaluating recordSecurityLock", logPair("recordSecurityLock", recordSecurityLock.getFieldName()), logPair("joinNameChain", joinNameChain));
QTableMetaData tmpTable = instance.getTable(mainTableName);
for(String joinName : joinNameChain)
{
///////////////////////////////////////////////////////////////////////////////////////////////////////
// check the joins currently in the query - if any are for this table, then we don't need to add one //
///////////////////////////////////////////////////////////////////////////////////////////////////////
List<QueryJoin> matchingJoins = this.queryJoins.stream().filter(queryJoin ->
{
QJoinMetaData joinMetaData = null;
if(queryJoin.getJoinMetaData() != null)
{
joinMetaData = queryJoin.getJoinMetaData();
}
else
{
joinMetaData = findJoinMetaData(instance, tableName, queryJoin.getJoinTable());
}
return (joinMetaData != null && Objects.equals(joinMetaData.getName(), joinName));
}).toList();
if(CollectionUtils.nullSafeHasContents(matchingJoins))
{
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// note - if a user added a join as an outer type, we need to change it to be inner, for the security purpose. //
// todo - is this always right? what about nulls-allowed? //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
log("- skipping join already in the query", logPair("joinName", joinName));
if(matchingJoins.get(0).getType().equals(QueryJoin.Type.LEFT) || matchingJoins.get(0).getType().equals(QueryJoin.Type.RIGHT))
{
log("- - although... it was here as an outer - so switching it to INNER", logPair("joinName", joinName));
matchingJoins.get(0).setType(QueryJoin.Type.INNER);
}
continue;
}
QJoinMetaData join = instance.getJoin(joinName);
if(join.getLeftTable().equals(tmpTable.getName()))
{
QueryJoin queryJoin = new ImplicitQueryJoinForSecurityLock().withJoinMetaData(join).withType(QueryJoin.Type.INNER);
this.addQueryJoin(queryJoin, "forRecordSecurityLock (non-flipped)");
tmpTable = instance.getTable(join.getRightTable());
}
else if(join.getRightTable().equals(tmpTable.getName()))
{
QueryJoin queryJoin = new ImplicitQueryJoinForSecurityLock().withJoinMetaData(join.flip()).withType(QueryJoin.Type.INNER);
this.addQueryJoin(queryJoin, "forRecordSecurityLock (flipped)");
tmpTable = instance.getTable(join.getLeftTable());
}
else
{
throw (new QException("Error adding security lock joins to query - table name [" + tmpTable.getName() + "] not found in join [" + joinName + "]"));
}
}
} }
@ -197,15 +151,8 @@ public class JoinsContext
** use this method to add to the list, instead of ever adding directly, as it's ** use this method to add to the list, instead of ever adding directly, as it's
** important do to that process step (and we've had bugs when it wasn't done). ** important do to that process step (and we've had bugs when it wasn't done).
*******************************************************************************/ *******************************************************************************/
private void addQueryJoin(QueryJoin queryJoin, String reason) throws QException private void addQueryJoin(QueryJoin queryJoin) throws QException
{ {
log("Adding query join to context",
logPair("reason", reason),
logPair("joinTable", queryJoin.getJoinTable()),
logPair("joinMetaData.name", () -> queryJoin.getJoinMetaData().getName()),
logPair("joinMetaData.leftTable", () -> queryJoin.getJoinMetaData().getLeftTable()),
logPair("joinMetaData.rightTable", () -> queryJoin.getJoinMetaData().getRightTable())
);
this.queryJoins.add(queryJoin); this.queryJoins.add(queryJoin);
processQueryJoin(queryJoin); processQueryJoin(queryJoin);
} }
@ -230,46 +177,10 @@ public class JoinsContext
addedJoin = false; addedJoin = false;
for(QueryJoin queryJoin : queryJoins) for(QueryJoin queryJoin : queryJoins)
{ {
/////////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////
// if the join has joinMetaData, then we don't need to process it... unless it needs flipped // // if the join has joinMetaData, then we don't need to process it. //
/////////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////
QJoinMetaData joinMetaData = queryJoin.getJoinMetaData(); if(queryJoin.getJoinMetaData() == null)
if(joinMetaData != null)
{
boolean isJoinLeftTableInQuery = false;
String joinMetaDataLeftTable = joinMetaData.getLeftTable();
if(joinMetaDataLeftTable.equals(mainTableName))
{
isJoinLeftTableInQuery = true;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// check the other joins in this query - if any of them have this join's left-table as their baseTable, then set the flag to true //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
for(QueryJoin otherJoin : queryJoins)
{
if(otherJoin == queryJoin)
{
continue;
}
if(Objects.equals(otherJoin.getBaseTableOrAlias(), joinMetaDataLeftTable))
{
isJoinLeftTableInQuery = true;
break;
}
}
/////////////////////////////////////////////////////////////////////////////////
// if the join's left-table isn't in the query, then we need to flip the join. //
/////////////////////////////////////////////////////////////////////////////////
if(!isJoinLeftTableInQuery)
{
log("Flipping queryJoin because its leftTable wasn't found in the query", logPair("joinMetaDataName", joinMetaData.getName()), logPair("leftTable", joinMetaDataLeftTable));
queryJoin.setJoinMetaData(joinMetaData.flip());
}
}
else
{ {
////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////
// try to find a direct join between the main table and this table. // // try to find a direct join between the main table and this table. //
@ -279,7 +190,6 @@ public class JoinsContext
QJoinMetaData found = findJoinMetaData(instance, baseTableName, queryJoin.getJoinTable()); QJoinMetaData found = findJoinMetaData(instance, baseTableName, queryJoin.getJoinTable());
if(found != null) if(found != null)
{ {
log("Found joinMetaData - setting it in queryJoin", logPair("joinMetaDataName", found.getName()), logPair("baseTableName", baseTableName), logPair("joinTable", queryJoin.getJoinTable()));
queryJoin.setJoinMetaData(found); queryJoin.setJoinMetaData(found);
} }
else else
@ -287,13 +197,15 @@ public class JoinsContext
///////////////////////////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// else, the join must be indirect - so look for an exposedJoin that will have a joinPath that will connect us // // else, the join must be indirect - so look for an exposedJoin that will have a joinPath that will connect us //
///////////////////////////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////////////////////////////
LOG.debug("Looking for an exposed join...", logPair("mainTable", mainTableName), logPair("joinTable", queryJoin.getJoinTable()));
QTableMetaData mainTable = instance.getTable(mainTableName); QTableMetaData mainTable = instance.getTable(mainTableName);
boolean addedAnyQueryJoins = false; boolean addedAnyQueryJoins = false;
for(ExposedJoin exposedJoin : CollectionUtils.nonNullList(mainTable.getExposedJoins())) for(ExposedJoin exposedJoin : CollectionUtils.nonNullList(mainTable.getExposedJoins()))
{ {
if(queryJoin.getJoinTable().equals(exposedJoin.getJoinTable())) if(queryJoin.getJoinTable().equals(exposedJoin.getJoinTable()))
{ {
log("Found an exposed join", logPair("mainTable", mainTableName), logPair("joinTable", queryJoin.getJoinTable()), logPair("joinPath", exposedJoin.getJoinPath())); LOG.debug("Found an exposed join", logPair("mainTable", mainTableName), logPair("joinTable", queryJoin.getJoinTable()), logPair("joinPath", exposedJoin.getJoinPath()));
///////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////
// loop backward through the join path (from the joinTable back to the main table) // // loop backward through the join path (from the joinTable back to the main table) //
@ -338,7 +250,7 @@ public class JoinsContext
QueryJoin queryJoinToAdd = makeQueryJoinFromJoinAndTableNames(nextTable, tmpTable, joinToAdd); QueryJoin queryJoinToAdd = makeQueryJoinFromJoinAndTableNames(nextTable, tmpTable, joinToAdd);
queryJoinToAdd.setType(queryJoin.getType()); queryJoinToAdd.setType(queryJoin.getType());
addedAnyQueryJoins = true; addedAnyQueryJoins = true;
this.addQueryJoin(queryJoinToAdd, "forExposedJoin"); this.addQueryJoin(queryJoinToAdd);
} }
} }
@ -465,9 +377,9 @@ public class JoinsContext
** **
** e.g., Given: ** e.g., Given:
** FROM `order` INNER JOIN line_item li ** FROM `order` INNER JOIN line_item li
** hasTable("order") => true ** hasAliasOrTable("order") => true
** hasTable("li") => false ** hasAliasOrTable("li") => false
** hasTable("line_item") => true ** hasAliasOrTable("line_item") => true
*******************************************************************************/ *******************************************************************************/
public boolean hasTable(String table) public boolean hasTable(String table)
{ {
@ -503,17 +415,15 @@ public class JoinsContext
for(String filterTable : filterTables) for(String filterTable : filterTables)
{ {
log("Evaluating filterTable", logPair("filterTable", filterTable));
if(!aliasToTableNameMap.containsKey(filterTable) && !Objects.equals(mainTableName, filterTable)) if(!aliasToTableNameMap.containsKey(filterTable) && !Objects.equals(mainTableName, filterTable))
{ {
log("- table not in query - adding it", logPair("filterTable", filterTable));
boolean found = false; boolean found = false;
for(QJoinMetaData join : CollectionUtils.nonNullMap(QContext.getQInstance().getJoins()).values()) for(QJoinMetaData join : CollectionUtils.nonNullMap(QContext.getQInstance().getJoins()).values())
{ {
QueryJoin queryJoin = makeQueryJoinFromJoinAndTableNames(mainTableName, filterTable, join); QueryJoin queryJoin = makeQueryJoinFromJoinAndTableNames(mainTableName, filterTable, join);
if(queryJoin != null) if(queryJoin != null)
{ {
this.addQueryJoin(queryJoin, "forFilter (join found in instance)"); this.addQueryJoin(queryJoin);
found = true; found = true;
break; break;
} }
@ -522,7 +432,7 @@ public class JoinsContext
if(!found) if(!found)
{ {
QueryJoin queryJoin = new QueryJoin().withJoinTable(filterTable).withType(QueryJoin.Type.INNER); QueryJoin queryJoin = new QueryJoin().withJoinTable(filterTable).withType(QueryJoin.Type.INNER);
this.addQueryJoin(queryJoin, "forFilter (join not found in instance)"); this.addQueryJoin(queryJoin);
} }
} }
} }
@ -659,14 +569,4 @@ public class JoinsContext
{ {
} }
/*******************************************************************************
**
*******************************************************************************/
private void log(String message, LogPair... logPairs)
{
LOG.log(logLevel, message, null, logPairs);
}
} }

View File

@ -26,9 +26,8 @@ import java.io.Serializable;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.kingsrook.qqq.backend.core.logging.QLogger; import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.serialization.QFilterCriteriaDeserializer; import com.kingsrook.qqq.backend.core.model.actions.tables.query.expressions.AbstractFilterExpression;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils; import com.kingsrook.qqq.backend.core.utils.StringUtils;
@ -37,7 +36,6 @@ import com.kingsrook.qqq.backend.core.utils.StringUtils;
* A single criteria Component of a Query * A single criteria Component of a Query
* *
*******************************************************************************/ *******************************************************************************/
@JsonDeserialize(using = QFilterCriteriaDeserializer.class)
public class QFilterCriteria implements Serializable, Cloneable public class QFilterCriteria implements Serializable, Cloneable
{ {
private static final QLogger LOG = QLogger.getLogger(QFilterCriteria.class); private static final QLogger LOG = QLogger.getLogger(QFilterCriteria.class);
@ -46,10 +44,8 @@ public class QFilterCriteria implements Serializable, Cloneable
private QCriteriaOperator operator; private QCriteriaOperator operator;
private List<Serializable> values; private List<Serializable> values;
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// private String otherFieldName;
// todo - probably implement this as a type of expression - though would require a little special handling i think when evaluating... // private AbstractFilterExpression<?> expression;
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
private String otherFieldName;
@ -102,6 +98,23 @@ public class QFilterCriteria implements Serializable, Cloneable
/*******************************************************************************
**
*******************************************************************************/
public QFilterCriteria(String fieldName, QCriteriaOperator operator, AbstractFilterExpression<?> expression)
{
this.fieldName = fieldName;
this.operator = operator;
this.expression = expression;
///////////////////////////////////////
// this guy doesn't like to be null? //
///////////////////////////////////////
this.values = new ArrayList<>();
}
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
@ -120,7 +133,7 @@ public class QFilterCriteria implements Serializable, Cloneable
} }
else else
{ {
this.values = new ArrayList<>(Arrays.stream(values).toList()); this.values = Arrays.stream(values).toList();
} }
} }
@ -319,4 +332,35 @@ public class QFilterCriteria implements Serializable, Cloneable
return (rs.toString()); return (rs.toString());
} }
/*******************************************************************************
** Getter for expression
*******************************************************************************/
public AbstractFilterExpression<?> getExpression()
{
return (this.expression);
}
/*******************************************************************************
** Setter for expression
*******************************************************************************/
public void setExpression(AbstractFilterExpression<?> expression)
{
this.expression = expression;
}
/*******************************************************************************
** Fluent setter for expression
*******************************************************************************/
public QFilterCriteria withExpression(AbstractFilterExpression<?> expression)
{
this.expression = expression;
return (this);
}
} }

View File

@ -29,20 +29,18 @@ import java.util.Set;
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction; import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
import com.kingsrook.qqq.backend.core.actions.reporting.RecordPipe; import com.kingsrook.qqq.backend.core.actions.reporting.RecordPipe;
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput; import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.QueryOrGetInputInterface;
/******************************************************************************* /*******************************************************************************
** Input data for the Query action ** Input data for the Query action
** **
*******************************************************************************/ *******************************************************************************/
public class QueryInput extends AbstractTableActionInput implements QueryOrGetInputInterface public class QueryInput extends AbstractTableActionInput
{ {
private QBackendTransaction transaction; private QBackendTransaction transaction;
private QQueryFilter filter; private QQueryFilter filter;
private RecordPipe recordPipe; private RecordPipe recordPipe;
private Integer timeoutSeconds;
private boolean shouldTranslatePossibleValues = false; private boolean shouldTranslatePossibleValues = false;
private boolean shouldGenerateDisplayValues = false; private boolean shouldGenerateDisplayValues = false;
@ -79,17 +77,6 @@ public class QueryInput extends AbstractTableActionInput implements QueryOrGetIn
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public QueryInput(String tableName)
{
setTableName(tableName);
}
/******************************************************************************* /*******************************************************************************
** Getter for filter ** Getter for filter
** **
@ -538,35 +525,4 @@ public class QueryInput extends AbstractTableActionInput implements QueryOrGetIn
return (this); return (this);
} }
/*******************************************************************************
** Getter for timeoutSeconds
*******************************************************************************/
public Integer getTimeoutSeconds()
{
return (this.timeoutSeconds);
}
/*******************************************************************************
** Setter for timeoutSeconds
*******************************************************************************/
public void setTimeoutSeconds(Integer timeoutSeconds)
{
this.timeoutSeconds = timeoutSeconds;
}
/*******************************************************************************
** Fluent setter for timeoutSeconds
*******************************************************************************/
public QueryInput withTimeoutSeconds(Integer timeoutSeconds)
{
this.timeoutSeconds = timeoutSeconds;
return (this);
}
} }

View File

@ -28,31 +28,10 @@ import java.io.Serializable;
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
public abstract class AbstractFilterExpression<T extends Serializable> implements Serializable public abstract class AbstractFilterExpression<T extends Serializable>
{ {
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
public abstract T evaluate(); public abstract T evaluate();
/*******************************************************************************
** To help with serialization, define a "type" in all subclasses
*******************************************************************************/
public String getType()
{
return (getClass().getSimpleName());
}
/*******************************************************************************
** noop - but here so serialization won't be upset about there being a type
** in a json object.
*******************************************************************************/
public void setType(String type)
{
}
} }

View File

@ -23,10 +23,6 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.query.expressions;
import java.time.Instant; import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.temporal.ChronoUnit;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -35,9 +31,9 @@ import java.util.concurrent.TimeUnit;
*******************************************************************************/ *******************************************************************************/
public class NowWithOffset extends AbstractFilterExpression<Instant> public class NowWithOffset extends AbstractFilterExpression<Instant>
{ {
private Operator operator; private final Operator operator;
private int amount; private final int amount;
private ChronoUnit timeUnit; private final TimeUnit timeUnit;
@ -50,17 +46,7 @@ public class NowWithOffset extends AbstractFilterExpression<Instant>
** Constructor ** Constructor
** **
*******************************************************************************/ *******************************************************************************/
public NowWithOffset() private NowWithOffset(Operator operator, int amount, TimeUnit timeUnit)
{
}
/*******************************************************************************
** Constructor
**
*******************************************************************************/
private NowWithOffset(Operator operator, int amount, ChronoUnit timeUnit)
{ {
this.operator = operator; this.operator = operator;
this.amount = amount; this.amount = amount;
@ -73,19 +59,7 @@ public class NowWithOffset extends AbstractFilterExpression<Instant>
** Factory ** Factory
** **
*******************************************************************************/ *******************************************************************************/
@Deprecated
public static NowWithOffset minus(int amount, TimeUnit timeUnit) public static NowWithOffset minus(int amount, TimeUnit timeUnit)
{
return (minus(amount, timeUnit.toChronoUnit()));
}
/*******************************************************************************
** Factory
**
*******************************************************************************/
public static NowWithOffset minus(int amount, ChronoUnit timeUnit)
{ {
return (new NowWithOffset(Operator.MINUS, amount, timeUnit)); return (new NowWithOffset(Operator.MINUS, amount, timeUnit));
} }
@ -96,19 +70,7 @@ public class NowWithOffset extends AbstractFilterExpression<Instant>
** Factory ** Factory
** **
*******************************************************************************/ *******************************************************************************/
@Deprecated
public static NowWithOffset plus(int amount, TimeUnit timeUnit) public static NowWithOffset plus(int amount, TimeUnit timeUnit)
{
return (plus(amount, timeUnit.toChronoUnit()));
}
/*******************************************************************************
** Factory
**
*******************************************************************************/
public static NowWithOffset plus(int amount, ChronoUnit timeUnit)
{ {
return (new NowWithOffset(Operator.PLUS, amount, timeUnit)); return (new NowWithOffset(Operator.PLUS, amount, timeUnit));
} }
@ -121,24 +83,14 @@ public class NowWithOffset extends AbstractFilterExpression<Instant>
@Override @Override
public Instant evaluate() public Instant evaluate()
{ {
/////////////////////////////////////////////////////////////////////////////
// Instant doesn't let us plus/minus WEEK, MONTH, or YEAR... //
// but LocalDateTime does. So, make a LDT in UTC, do the plus/minus, then //
// convert back to Instant @ UTC //
/////////////////////////////////////////////////////////////////////////////
LocalDateTime now = LocalDateTime.now(ZoneId.of("UTC"));
LocalDateTime then;
if(operator.equals(Operator.PLUS)) if(operator.equals(Operator.PLUS))
{ {
then = now.plus(amount, timeUnit); return (Instant.now().plus(amount, timeUnit.toChronoUnit()));
} }
else else
{ {
then = now.minus(amount, timeUnit); return (Instant.now().minus(amount, timeUnit.toChronoUnit()));
} }
return (then.toInstant(ZoneOffset.UTC));
} }
@ -169,7 +121,7 @@ public class NowWithOffset extends AbstractFilterExpression<Instant>
** Getter for timeUnit ** Getter for timeUnit
** **
*******************************************************************************/ *******************************************************************************/
public ChronoUnit getTimeUnit() public TimeUnit getTimeUnit()
{ {
return timeUnit; return timeUnit;
} }

View File

@ -1,178 +0,0 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.actions.tables.query.expressions;
import java.time.DayOfWeek;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.temporal.ChronoField;
import java.time.temporal.ChronoUnit;
import com.kingsrook.qqq.backend.core.exceptions.QRuntimeException;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
/*******************************************************************************
**
*******************************************************************************/
public class ThisOrLastPeriod extends AbstractFilterExpression<Instant>
{
private Operator operator;
private ChronoUnit timeUnit;
public enum Operator
{THIS, LAST}
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public ThisOrLastPeriod()
{
}
/*******************************************************************************
** Constructor
**
*******************************************************************************/
private ThisOrLastPeriod(Operator operator, ChronoUnit timeUnit)
{
this.operator = operator;
this.timeUnit = timeUnit;
}
/*******************************************************************************
** Factory
**
*******************************************************************************/
public static ThisOrLastPeriod this_(ChronoUnit timeUnit)
{
return (new ThisOrLastPeriod(Operator.THIS, timeUnit));
}
/*******************************************************************************
** Factory
**
*******************************************************************************/
public static ThisOrLastPeriod last(int amount, ChronoUnit timeUnit)
{
return (new ThisOrLastPeriod(Operator.LAST, timeUnit));
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public Instant evaluate()
{
ZoneId zoneId = ValueUtils.getSessionOrInstanceZoneId();
switch(timeUnit)
{
case HOURS ->
{
if(operator.equals(Operator.THIS))
{
return Instant.now().truncatedTo(ChronoUnit.HOURS);
}
else
{
return Instant.now().minus(1, ChronoUnit.HOURS).truncatedTo(ChronoUnit.HOURS);
}
}
case DAYS ->
{
Instant startOfToday = ValueUtils.getStartOfTodayInZoneId(zoneId.getId());
return operator.equals(Operator.THIS) ? startOfToday : startOfToday.minus(1, ChronoUnit.DAYS);
}
case WEEKS ->
{
Instant startOfToday = ValueUtils.getStartOfTodayInZoneId(zoneId.getId());
LocalDateTime startOfThisWeekLDT = LocalDateTime.ofInstant(startOfToday, zoneId);
while(startOfThisWeekLDT.get(ChronoField.DAY_OF_WEEK) != DayOfWeek.SUNDAY.getValue())
{
////////////////////////////////////////
// go backwards until sunday is found //
////////////////////////////////////////
startOfThisWeekLDT = startOfThisWeekLDT.minus(1, ChronoUnit.DAYS);
}
Instant startOfThisWeek = startOfThisWeekLDT.toInstant(zoneId.getRules().getOffset(startOfThisWeekLDT));
return operator.equals(Operator.THIS) ? startOfThisWeek : startOfThisWeek.minus(7, ChronoUnit.DAYS);
}
case MONTHS ->
{
Instant startOfThisMonth = ValueUtils.getStartOfMonthInZoneId(zoneId.getId());
LocalDateTime startOfThisMonthLDT = LocalDateTime.ofInstant(startOfThisMonth, ZoneId.of(zoneId.getId()));
LocalDateTime startOfLastMonthLDT = startOfThisMonthLDT.minus(1, ChronoUnit.MONTHS);
Instant startOfLastMonth = startOfLastMonthLDT.toInstant(ZoneId.of(zoneId.getId()).getRules().getOffset(Instant.now()));
return operator.equals(Operator.THIS) ? startOfThisMonth : startOfLastMonth;
}
case YEARS ->
{
Instant startOfThisYear = ValueUtils.getStartOfYearInZoneId(zoneId.getId());
LocalDateTime startOfThisYearLDT = LocalDateTime.ofInstant(startOfThisYear, zoneId);
LocalDateTime startOfLastYearLDT = startOfThisYearLDT.minus(1, ChronoUnit.YEARS);
Instant startOfLastYear = startOfLastYearLDT.toInstant(zoneId.getRules().getOffset(Instant.now()));
return operator.equals(Operator.THIS) ? startOfThisYear : startOfLastYear;
}
default -> throw (new QRuntimeException("Unsupported timeUnit: " + timeUnit));
}
}
/*******************************************************************************
** Getter for operator
**
*******************************************************************************/
public Operator getOperator()
{
return operator;
}
/*******************************************************************************
** Getter for timeUnit
**
*******************************************************************************/
public ChronoUnit getTimeUnit()
{
return timeUnit;
}
}

View File

@ -1,129 +0,0 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.actions.tables.query.serialization;
import java.io.IOException;
import java.io.Serializable;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import com.fasterxml.jackson.core.JacksonException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
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.expressions.AbstractFilterExpression;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
/*******************************************************************************
** Custom jackson deserializer, to deal w/ abstract expression field
*******************************************************************************/
public class QFilterCriteriaDeserializer extends StdDeserializer<QFilterCriteria>
{
/*******************************************************************************
**
*******************************************************************************/
public QFilterCriteriaDeserializer()
{
this(null);
}
/*******************************************************************************
**
*******************************************************************************/
public QFilterCriteriaDeserializer(Class<?> vc)
{
super(vc);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public QFilterCriteria deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JacksonException
{
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
ObjectMapper objectMapper = new ObjectMapper();
/////////////////////////////////
// get values out of json node //
/////////////////////////////////
List<Serializable> values = objectMapper.treeToValue(node.get("values"), List.class);
String fieldName = objectMapper.treeToValue(node.get("fieldName"), String.class);
QCriteriaOperator operator = objectMapper.treeToValue(node.get("operator"), QCriteriaOperator.class);
String otherFieldName = objectMapper.treeToValue(node.get("otherFieldName"), String.class);
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// look at all the values - if any of them are actually meant to be an Expression (instance of subclass of AbstractFilterExpression) //
// they'll have deserialized as a Map, with a "type" key. If that's the case, then re/de serialize them into the proper expression type //
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
ListIterator<Serializable> valuesIterator = CollectionUtils.nonNullList(values).listIterator();
while(valuesIterator.hasNext())
{
Object value = valuesIterator.next();
if(value instanceof Map<?, ?> map && map.containsKey("type"))
{
String expressionType = ValueUtils.getValueAsString(map.get("type"));
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// right now, we'll assume that all expression subclasses are in the same package as AbstractFilterExpression //
// so, we can just do a Class.forName on that name, and use JsonUtils.toObject requesting that class. //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
try
{
String assumedExpressionJSON = JsonUtils.toJson(map);
String className = AbstractFilterExpression.class.getName().replace(AbstractFilterExpression.class.getSimpleName(), expressionType);
Serializable replacementValue = (Serializable) JsonUtils.toObject(assumedExpressionJSON, Class.forName(className));
valuesIterator.set(replacementValue);
}
catch(Exception e)
{
throw (new IOException("Error deserializing criteria value which appeared to be an expression of type [" + expressionType + "] inside QFilterCriteria", e));
}
}
}
///////////////////////////////////
// put fields into return object //
///////////////////////////////////
QFilterCriteria criteria = new QFilterCriteria();
criteria.setFieldName(fieldName);
criteria.setOperator(operator);
criteria.setValues(values);
criteria.setOtherFieldName(otherFieldName);
return (criteria);
}
}

View File

@ -1,210 +0,0 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.actions.tables.replace;
import java.util.List;
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey;
/*******************************************************************************
**
*******************************************************************************/
public class ReplaceInput extends AbstractTableActionInput
{
private QBackendTransaction transaction;
private UniqueKey key;
private List<QRecord> records;
private QQueryFilter filter;
private boolean omitDmlAudit = false;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public ReplaceInput()
{
}
/*******************************************************************************
** Getter for transaction
*******************************************************************************/
public QBackendTransaction getTransaction()
{
return (this.transaction);
}
/*******************************************************************************
** Setter for transaction
*******************************************************************************/
public void setTransaction(QBackendTransaction transaction)
{
this.transaction = transaction;
}
/*******************************************************************************
** Fluent setter for transaction
*******************************************************************************/
public ReplaceInput withTransaction(QBackendTransaction transaction)
{
this.transaction = transaction;
return (this);
}
/*******************************************************************************
** Getter for records
*******************************************************************************/
public List<QRecord> getRecords()
{
return (this.records);
}
/*******************************************************************************
** Setter for records
*******************************************************************************/
public void setRecords(List<QRecord> records)
{
this.records = records;
}
/*******************************************************************************
** Fluent setter for records
*******************************************************************************/
public ReplaceInput withRecords(List<QRecord> records)
{
this.records = records;
return (this);
}
/*******************************************************************************
** Getter for filter
*******************************************************************************/
public QQueryFilter getFilter()
{
return (this.filter);
}
/*******************************************************************************
** Setter for filter
*******************************************************************************/
public void setFilter(QQueryFilter filter)
{
this.filter = filter;
}
/*******************************************************************************
** Fluent setter for filter
*******************************************************************************/
public ReplaceInput withFilter(QQueryFilter filter)
{
this.filter = filter;
return (this);
}
/*******************************************************************************
** Getter for key
*******************************************************************************/
public UniqueKey getKey()
{
return (this.key);
}
/*******************************************************************************
** Setter for key
*******************************************************************************/
public void setKey(UniqueKey key)
{
this.key = key;
}
/*******************************************************************************
** Fluent setter for key
*******************************************************************************/
public ReplaceInput withKey(UniqueKey key)
{
this.key = key;
return (this);
}
/*******************************************************************************
** Getter for omitDmlAudit
*******************************************************************************/
public boolean getOmitDmlAudit()
{
return (this.omitDmlAudit);
}
/*******************************************************************************
** Setter for omitDmlAudit
*******************************************************************************/
public void setOmitDmlAudit(boolean omitDmlAudit)
{
this.omitDmlAudit = omitDmlAudit;
}
/*******************************************************************************
** Fluent setter for omitDmlAudit
*******************************************************************************/
public ReplaceInput withOmitDmlAudit(boolean omitDmlAudit)
{
this.omitDmlAudit = omitDmlAudit;
return (this);
}
}

View File

@ -1,133 +0,0 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.actions.tables.replace;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateOutput;
/*******************************************************************************
**
*******************************************************************************/
public class ReplaceOutput extends AbstractActionOutput
{
private InsertOutput insertOutput;
private UpdateOutput updateOutput;
private DeleteOutput deleteOutput;
/*******************************************************************************
** Getter for insertOutput
*******************************************************************************/
public InsertOutput getInsertOutput()
{
return (this.insertOutput);
}
/*******************************************************************************
** Setter for insertOutput
*******************************************************************************/
public void setInsertOutput(InsertOutput insertOutput)
{
this.insertOutput = insertOutput;
}
/*******************************************************************************
** Fluent setter for insertOutput
*******************************************************************************/
public ReplaceOutput withInsertOutput(InsertOutput insertOutput)
{
this.insertOutput = insertOutput;
return (this);
}
/*******************************************************************************
** Getter for updateOutput
*******************************************************************************/
public UpdateOutput getUpdateOutput()
{
return (this.updateOutput);
}
/*******************************************************************************
** Setter for updateOutput
*******************************************************************************/
public void setUpdateOutput(UpdateOutput updateOutput)
{
this.updateOutput = updateOutput;
}
/*******************************************************************************
** Fluent setter for updateOutput
*******************************************************************************/
public ReplaceOutput withUpdateOutput(UpdateOutput updateOutput)
{
this.updateOutput = updateOutput;
return (this);
}
/*******************************************************************************
** Getter for deleteOutput
*******************************************************************************/
public DeleteOutput getDeleteOutput()
{
return (this.deleteOutput);
}
/*******************************************************************************
** Setter for deleteOutput
*******************************************************************************/
public void setDeleteOutput(DeleteOutput deleteOutput)
{
this.deleteOutput = deleteOutput;
}
/*******************************************************************************
** Fluent setter for deleteOutput
*******************************************************************************/
public ReplaceOutput withDeleteOutput(DeleteOutput deleteOutput)
{
this.deleteOutput = deleteOutput;
return (this);
}
}

View File

@ -22,15 +22,12 @@
package com.kingsrook.qqq.backend.core.model.actions.tables.update; package com.kingsrook.qqq.backend.core.model.actions.tables.update;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction; import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput; import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.InputSource; import com.kingsrook.qqq.backend.core.model.actions.tables.InputSource;
import com.kingsrook.qqq.backend.core.model.actions.tables.QInputSource; import com.kingsrook.qqq.backend.core.model.actions.tables.QInputSource;
import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
/******************************************************************************* /*******************************************************************************
@ -65,71 +62,6 @@ public class UpdateInput extends AbstractTableActionInput
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public UpdateInput(String tableName)
{
setTableName(tableName);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public UpdateInput withTableName(String tableName)
{
super.withTableName(tableName);
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
public UpdateInput withRecord(QRecord record)
{
if(records == null)
{
records = new ArrayList<>();
}
records.add(record);
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
public UpdateInput withRecordEntity(QRecordEntity recordEntity)
{
return (withRecord(recordEntity.toQRecord()));
}
/*******************************************************************************
**
*******************************************************************************/
public UpdateInput withRecordEntities(List<QRecordEntity> recordEntityList)
{
for(QRecordEntity recordEntity : CollectionUtils.nonNullList(recordEntityList))
{
withRecordEntity(recordEntity);
}
return (this);
}
/******************************************************************************* /*******************************************************************************
** Getter for transaction ** Getter for transaction
** **

View File

@ -29,6 +29,7 @@ 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.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.audits.AuditLevel; import com.kingsrook.qqq.backend.core.model.metadata.audits.AuditLevel;
import com.kingsrook.qqq.backend.core.model.metadata.audits.QAuditRules; import com.kingsrook.qqq.backend.core.model.metadata.audits.QAuditRules;
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.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior; import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior;
@ -36,9 +37,13 @@ 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.JoinType;
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData; import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource; import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.Association;
import com.kingsrook.qqq.backend.core.model.metadata.tables.Capability; 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.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey; import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey;
import com.kingsrook.qqq.backend.core.processes.implementations.audits.GetRecordAuditsStep;
/******************************************************************************* /*******************************************************************************
@ -50,6 +55,10 @@ public class AuditsMetaDataProvider
public static final String TABLE_NAME_AUDIT_USER = "auditUser"; public static final String TABLE_NAME_AUDIT_USER = "auditUser";
public static final String TABLE_NAME_AUDIT = "audit"; public static final String TABLE_NAME_AUDIT = "audit";
public static final String TABLE_NAME_AUDIT_DETAIL = "auditDetail"; public static final String TABLE_NAME_AUDIT_DETAIL = "auditDetail";
public static final String TABLE_NAME_AUDIT_TREE = "auditTree";
public static final String AUDIT_TREE_JOIN_AUDIT_TABLE_FOR_ROOT = "auditTreeJoinAuditTableForRoot";
public static final String AUDIT_TREE_JOIN_AUDIT_TABLE_FOR_NODE = "auditTreeJoinAuditTableForNode";
@ -61,6 +70,21 @@ public class AuditsMetaDataProvider
defineStandardAuditTables(instance, backendName, backendDetailEnricher); defineStandardAuditTables(instance, backendName, backendDetailEnricher);
defineStandardAuditPossibleValueSources(instance); defineStandardAuditPossibleValueSources(instance);
defineStandardAuditJoins(instance); defineStandardAuditJoins(instance);
defineProcessGetRecordAudits(instance);
}
/*******************************************************************************
**
*******************************************************************************/
private void defineProcessGetRecordAudits(QInstance instance)
{
instance.addProcess(new QProcessMetaData()
.withName("getRecordAudits")
.withStepList(List.of(new QBackendStepMetaData()
.withName("step")
.withCode(new QCodeReference(GetRecordAuditsStep.class)))));
} }
@ -91,6 +115,20 @@ public class AuditsMetaDataProvider
.withType(JoinType.ONE_TO_MANY) .withType(JoinType.ONE_TO_MANY)
.withJoinOn(new JoinOn("id", "auditId"))); .withJoinOn(new JoinOn("id", "auditId")));
instance.addJoin(new QJoinMetaData()
.withLeftTable(TABLE_NAME_AUDIT_TREE)
.withRightTable(TABLE_NAME_AUDIT_TABLE)
.withName(AUDIT_TREE_JOIN_AUDIT_TABLE_FOR_ROOT)
.withType(JoinType.MANY_TO_ONE)
.withJoinOn(new JoinOn("rootAuditTableId", "id")));
instance.addJoin(new QJoinMetaData()
.withLeftTable(TABLE_NAME_AUDIT_TREE)
.withRightTable(TABLE_NAME_AUDIT_TABLE)
.withName(AUDIT_TREE_JOIN_AUDIT_TABLE_FOR_NODE)
.withType(JoinType.MANY_TO_ONE)
.withJoinOn(new JoinOn("nodeAuditTableId", "id")));
} }
@ -116,19 +154,16 @@ public class AuditsMetaDataProvider
instance.addPossibleValueSource(new QPossibleValueSource() instance.addPossibleValueSource(new QPossibleValueSource()
.withName(TABLE_NAME_AUDIT_TABLE) .withName(TABLE_NAME_AUDIT_TABLE)
.withTableName(TABLE_NAME_AUDIT_TABLE) .withTableName(TABLE_NAME_AUDIT_TABLE)
.withOrderByField("name")
); );
instance.addPossibleValueSource(new QPossibleValueSource() instance.addPossibleValueSource(new QPossibleValueSource()
.withName(TABLE_NAME_AUDIT_USER) .withName(TABLE_NAME_AUDIT_USER)
.withTableName(TABLE_NAME_AUDIT_USER) .withTableName(TABLE_NAME_AUDIT_USER)
.withOrderByField("name")
); );
instance.addPossibleValueSource(new QPossibleValueSource() instance.addPossibleValueSource(new QPossibleValueSource()
.withName(TABLE_NAME_AUDIT) .withName(TABLE_NAME_AUDIT)
.withTableName(TABLE_NAME_AUDIT) .withTableName(TABLE_NAME_AUDIT)
.withOrderByField("id", false)
); );
} }
@ -144,6 +179,7 @@ public class AuditsMetaDataProvider
rs.add(enrich(backendDetailEnricher, defineAuditTableTable(backendName))); rs.add(enrich(backendDetailEnricher, defineAuditTableTable(backendName)));
rs.add(enrich(backendDetailEnricher, defineAuditTable(backendName))); rs.add(enrich(backendDetailEnricher, defineAuditTable(backendName)));
rs.add(enrich(backendDetailEnricher, defineAuditDetailTable(backendName))); rs.add(enrich(backendDetailEnricher, defineAuditDetailTable(backendName)));
rs.add(enrich(backendDetailEnricher, defineAuditTreeTable(backendName)));
return (rs); return (rs);
} }
@ -220,6 +256,10 @@ public class AuditsMetaDataProvider
.withRecordLabelFormat("%s %s") .withRecordLabelFormat("%s %s")
.withRecordLabelFields("auditTableId", "recordId") .withRecordLabelFields("auditTableId", "recordId")
.withPrimaryKeyField("id") .withPrimaryKeyField("id")
.withAssociation(new Association()
.withName("auditDetails")
.withAssociatedTableName(TABLE_NAME_AUDIT_DETAIL)
.withJoinName(QJoinMetaData.makeInferredJoinName(TABLE_NAME_AUDIT, TABLE_NAME_AUDIT_DETAIL)))
.withField(new QFieldMetaData("id", QFieldType.INTEGER)) .withField(new QFieldMetaData("id", QFieldType.INTEGER))
.withField(new QFieldMetaData("auditTableId", QFieldType.INTEGER).withPossibleValueSourceName(TABLE_NAME_AUDIT_TABLE)) .withField(new QFieldMetaData("auditTableId", QFieldType.INTEGER).withPossibleValueSourceName(TABLE_NAME_AUDIT_TABLE))
.withField(new QFieldMetaData("auditUserId", QFieldType.INTEGER).withPossibleValueSourceName(TABLE_NAME_AUDIT_USER)) .withField(new QFieldMetaData("auditUserId", QFieldType.INTEGER).withPossibleValueSourceName(TABLE_NAME_AUDIT_USER))
@ -252,4 +292,26 @@ public class AuditsMetaDataProvider
.withoutCapabilities(Capability.TABLE_INSERT, Capability.TABLE_UPDATE, Capability.TABLE_DELETE); .withoutCapabilities(Capability.TABLE_INSERT, Capability.TABLE_UPDATE, Capability.TABLE_DELETE);
} }
/*******************************************************************************
**
*******************************************************************************/
private QTableMetaData defineAuditTreeTable(String backendName)
{
return new QTableMetaData()
.withName(TABLE_NAME_AUDIT_TREE)
.withBackendName(backendName)
.withAuditRules(new QAuditRules().withAuditLevel(AuditLevel.NONE))
.withRecordLabelFormat("%s")
.withRecordLabelFields("id")
.withPrimaryKeyField("id")
.withField(new QFieldMetaData("id", QFieldType.INTEGER))
.withField(new QFieldMetaData("rootAuditTableId", QFieldType.INTEGER).withPossibleValueSourceName(TABLE_NAME_AUDIT))
.withField(new QFieldMetaData("rootRecordId", QFieldType.INTEGER))
.withField(new QFieldMetaData("nodeAuditTableId", QFieldType.INTEGER).withPossibleValueSourceName(TABLE_NAME_AUDIT))
.withField(new QFieldMetaData("nodeRecordId", QFieldType.INTEGER))
.withoutCapabilities(Capability.TABLE_INSERT, Capability.TABLE_UPDATE, Capability.TABLE_DELETE);
}
} }

View File

@ -48,8 +48,6 @@ public class ChartData extends QWidgetData
private boolean isCurrency = false; private boolean isCurrency = false;
private int height; private int height;
private ChartSubheaderData chartSubheaderData;
/******************************************************************************* /*******************************************************************************
@ -389,7 +387,6 @@ public class ChartData extends QWidgetData
private String color; private String color;
private String backgroundColor; private String backgroundColor;
private List<String> urls; private List<String> urls;
private List<String> backgroundColors;
@ -426,17 +423,6 @@ public class ChartData extends QWidgetData
/*******************************************************************************
** Getter for backgroundColors
**
*******************************************************************************/
public List<String> getBackgroundColors()
{
return backgroundColors;
}
/******************************************************************************* /*******************************************************************************
** Setter for backgroundColor ** Setter for backgroundColor
** **
@ -448,17 +434,6 @@ public class ChartData extends QWidgetData
/*******************************************************************************
** Setter for backgroundColor
**
*******************************************************************************/
public void setBackgroundColors(List<String> backgroundColors)
{
this.backgroundColors = backgroundColors;
}
/******************************************************************************* /*******************************************************************************
** Fluent setter for backgroundColor ** Fluent setter for backgroundColor
** **
@ -471,18 +446,6 @@ public class ChartData extends QWidgetData
/*******************************************************************************
** Fluent setter for backgroundColor
**
*******************************************************************************/
public Dataset withBackgroundColors(List<String> backgroundColors)
{
this.backgroundColors = backgroundColors;
return (this);
}
/******************************************************************************* /*******************************************************************************
** Getter for color ** Getter for color
** **
@ -596,36 +559,4 @@ public class ChartData extends QWidgetData
} }
} }
} }
/*******************************************************************************
** Getter for chartSubheaderData
*******************************************************************************/
public ChartSubheaderData getChartSubheaderData()
{
return (this.chartSubheaderData);
}
/*******************************************************************************
** Setter for chartSubheaderData
*******************************************************************************/
public void setChartSubheaderData(ChartSubheaderData chartSubheaderData)
{
this.chartSubheaderData = chartSubheaderData;
}
/*******************************************************************************
** Fluent setter for chartSubheaderData
*******************************************************************************/
public ChartData withChartSubheaderData(ChartSubheaderData chartSubheaderData)
{
this.chartSubheaderData = chartSubheaderData;
return (this);
}
} }

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