From 4efb818bfef18fedd32ca2e8750f6a4d48860985 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Fri, 16 Jun 2023 16:44:53 -0500 Subject: [PATCH 01/10] Add override executeWithStringDetails --- .../backend/core/actions/audits/AuditAction.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/audits/AuditAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/audits/AuditAction.java index 4bbd6881..41015e57 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/audits/AuditAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/audits/AuditAction.java @@ -76,6 +76,21 @@ public class AuditAction extends AbstractQActionFunction securityKeyValues, String message, List detailMessages) + { + List detailRecords = null; + if(CollectionUtils.nullSafeHasContents(detailMessages)) + { + detailRecords = detailMessages.stream().map(m -> new QRecord().withValue("message", m)).toList(); + } + execute(tableName, recordId, securityKeyValues, message, detailRecords); + } + + + /******************************************************************************* ** Execute to insert 1 audit, with a list of detail child records *******************************************************************************/ From 1cf83fb44176a005d69f27a0dc7ae4b82449871c Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Fri, 16 Jun 2023 16:45:10 -0500 Subject: [PATCH 02/10] Add factory method: newForTable --- .../possiblevalues/QPossibleValueSource.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/possiblevalues/QPossibleValueSource.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/possiblevalues/QPossibleValueSource.java index 76a03b90..c0d0076b 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/possiblevalues/QPossibleValueSource.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/possiblevalues/QPossibleValueSource.java @@ -77,6 +77,21 @@ public class QPossibleValueSource implements TopLevelMetaDataInterface + /******************************************************************************* + ** Create a new possible value source, for a table, with default settings. + ** e.g., name & table name from the tableName parameter; type=TABLE; and LABEL_ONLY format + *******************************************************************************/ + public static QPossibleValueSource newForTable(String tableName) + { + return new QPossibleValueSource() + .withName(tableName) + .withType(QPossibleValueSourceType.TABLE) + .withTableName(tableName) + .withValueFormatAndFields(PVSValueFormatAndFields.LABEL_ONLY); + } + + + /******************************************************************************* ** *******************************************************************************/ From 3772cf725f03e5d2abbc43da21b2666faaaeaf51 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Mon, 19 Jun 2023 12:03:50 -0500 Subject: [PATCH 03/10] Make table-triggers respect saved filters --- .../PollingAutomationPerTableRunner.java | 47 ++- .../core/model/automation/TableTrigger.java | 3 +- .../core/model/savedfilters/SavedFilter.java | 283 ++++++++++++++++++ .../SavedFiltersMetaDataProvider.java | 88 ++++++ .../scripts/ScriptsMetaDataProvider.java | 7 +- 5 files changed, 416 insertions(+), 12 deletions(-) create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedfilters/SavedFilter.java create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedfilters/SavedFiltersMetaDataProvider.java diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/automation/polling/PollingAutomationPerTableRunner.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/automation/polling/PollingAutomationPerTableRunner.java index 2f0b3106..2bb0a470 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/automation/polling/PollingAutomationPerTableRunner.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/automation/polling/PollingAutomationPerTableRunner.java @@ -39,12 +39,15 @@ import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader; import com.kingsrook.qqq.backend.core.actions.processes.QProcessCallback; import com.kingsrook.qqq.backend.core.actions.processes.RunProcessAction; import com.kingsrook.qqq.backend.core.actions.reporting.RecordPipe; +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.logging.QLogger; import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput; import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessOutput; +import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy; @@ -61,11 +64,14 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.Automatio import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.QTableAutomationDetails; import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.TableAutomationAction; import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.TriggerEvent; +import com.kingsrook.qqq.backend.core.model.savedfilters.SavedFilter; import com.kingsrook.qqq.backend.core.model.session.QSession; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; +import com.kingsrook.qqq.backend.core.utils.JsonUtils; import com.kingsrook.qqq.backend.core.utils.StringUtils; import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder; import org.apache.commons.lang.NotImplementedException; +import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; /******************************************************************************* @@ -270,16 +276,37 @@ public class PollingAutomationPerTableRunner implements Runnable QueryOutput queryOutput = new QueryAction().execute(queryInput); for(QRecord record : queryOutput.getRecords()) { - // todo - get filter if there is/was one - rs.add(new TableAutomationAction() - .withName("Script:" + record.getValue("scriptId")) - .withFilter(null) - .withTriggerEvent(triggerEvent) - .withPriority(record.getValueInteger("priority")) - .withCodeReference(new QCodeReference(RunRecordScriptAutomationHandler.class)) - .withValues(MapBuilder.of("scriptId", record.getValue("scriptId"))) - .withIncludeRecordAssociations(true) - ); + TableTrigger tableTrigger = new TableTrigger(record); + + try + { + Integer filterId = tableTrigger.getFilterId(); + + GetInput getInput = new GetInput(); + getInput.setTableName(SavedFilter.TABLE_NAME); + getInput.setPrimaryKey(filterId); + GetOutput getOutput = new GetAction().execute(getInput); + QQueryFilter filter = null; + if(getOutput.getRecord() != null) + { + SavedFilter savedFilter = new SavedFilter(getOutput.getRecord()); + filter = JsonUtils.toObject(savedFilter.getFilterJson(), QQueryFilter.class); + } + + rs.add(new TableAutomationAction() + .withName("Script:" + tableTrigger.getScriptId()) + .withFilter(filter) + .withTriggerEvent(triggerEvent) + .withPriority(tableTrigger.getPriority()) + .withCodeReference(new QCodeReference(RunRecordScriptAutomationHandler.class)) + .withValues(MapBuilder.of("scriptId", tableTrigger.getScriptId())) + .withIncludeRecordAssociations(true) + ); + } + catch(Exception e) + { + LOG.error("Error setting up table trigger", e, logPair("tableTriggerId", tableTrigger.getId())); + } } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/automation/TableTrigger.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/automation/TableTrigger.java index a21aedf0..a3c82b4f 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/automation/TableTrigger.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/automation/TableTrigger.java @@ -28,6 +28,7 @@ import com.kingsrook.qqq.backend.core.model.data.QField; import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.data.QRecordEntity; import com.kingsrook.qqq.backend.core.model.metadata.tables.TablesPossibleValueSourceMetaDataProvider; +import com.kingsrook.qqq.backend.core.model.savedfilters.SavedFilter; import com.kingsrook.qqq.backend.core.model.scripts.Script; @@ -50,7 +51,7 @@ public class TableTrigger extends QRecordEntity @QField(possibleValueSourceName = TablesPossibleValueSourceMetaDataProvider.NAME) private String tableName; - @QField(/* todo possibleValueSourceName = */) + @QField(possibleValueSourceName = SavedFilter.TABLE_NAME) private Integer filterId; @QField(possibleValueSourceName = Script.TABLE_NAME) diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedfilters/SavedFilter.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedfilters/SavedFilter.java new file mode 100644 index 00000000..9263b5d5 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedfilters/SavedFilter.java @@ -0,0 +1,283 @@ +/* + * 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 . + */ + +package com.kingsrook.qqq.backend.core.model.savedfilters; + + +import java.time.Instant; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.data.QField; +import com.kingsrook.qqq.backend.core.model.data.QRecord; +import com.kingsrook.qqq.backend.core.model.data.QRecordEntity; + + +/******************************************************************************* + ** Entity bean for the saved filter table + *******************************************************************************/ +public class SavedFilter extends QRecordEntity +{ + public static final String TABLE_NAME = "savedFilter"; + + @QField(isEditable = false) + private Integer id; + + @QField(isEditable = false) + private Instant createDate; + + @QField(isEditable = false) + private Instant modifyDate; + + @QField(isRequired = true) + private String label; + + @QField(isEditable = false) + private String tableName; + + @QField(isEditable = false) + private String userId; + + @QField(isEditable = false) + private String filterJson; + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public SavedFilter() + { + } + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public SavedFilter(QRecord qRecord) throws QException + { + populateFromQRecord(qRecord); + } + + + + /******************************************************************************* + ** Getter for id + ** + *******************************************************************************/ + public Integer getId() + { + return id; + } + + + + /******************************************************************************* + ** Setter for id + ** + *******************************************************************************/ + public void setId(Integer id) + { + this.id = id; + } + + + + /******************************************************************************* + ** Getter for createDate + ** + *******************************************************************************/ + public Instant getCreateDate() + { + return createDate; + } + + + + /******************************************************************************* + ** Setter for createDate + ** + *******************************************************************************/ + public void setCreateDate(Instant createDate) + { + this.createDate = createDate; + } + + + + /******************************************************************************* + ** Getter for modifyDate + ** + *******************************************************************************/ + public Instant getModifyDate() + { + return modifyDate; + } + + + + /******************************************************************************* + ** Setter for modifyDate + ** + *******************************************************************************/ + public void setModifyDate(Instant modifyDate) + { + this.modifyDate = modifyDate; + } + + + + /******************************************************************************* + ** Getter for label + ** + *******************************************************************************/ + public String getLabel() + { + return label; + } + + + + /******************************************************************************* + ** Setter for label + ** + *******************************************************************************/ + public void setLabel(String label) + { + this.label = label; + } + + + + /******************************************************************************* + ** Fluent setter for label + ** + *******************************************************************************/ + public SavedFilter withLabel(String label) + { + this.label = label; + return (this); + } + + + + /******************************************************************************* + ** Getter for tableName + ** + *******************************************************************************/ + public String getTableName() + { + return tableName; + } + + + + /******************************************************************************* + ** Setter for tableName + ** + *******************************************************************************/ + public void setTableName(String tableName) + { + this.tableName = tableName; + } + + + + /******************************************************************************* + ** Fluent setter for tableName + ** + *******************************************************************************/ + public SavedFilter withTableName(String tableName) + { + this.tableName = tableName; + return (this); + } + + + + /******************************************************************************* + ** Getter for userId + ** + *******************************************************************************/ + public String getUserId() + { + return userId; + } + + + + /******************************************************************************* + ** Setter for userId + ** + *******************************************************************************/ + public void setUserId(String userId) + { + this.userId = userId; + } + + + + /******************************************************************************* + ** Fluent setter for userId + ** + *******************************************************************************/ + public SavedFilter withUserId(String userId) + { + this.userId = userId; + return (this); + } + + + + /******************************************************************************* + ** Getter for filterJson + ** + *******************************************************************************/ + public String getFilterJson() + { + return filterJson; + } + + + + /******************************************************************************* + ** Setter for filterJson + ** + *******************************************************************************/ + public void setFilterJson(String filterJson) + { + this.filterJson = filterJson; + } + + + + /******************************************************************************* + ** Fluent setter for filterJson + ** + *******************************************************************************/ + public SavedFilter withFilterJson(String filterJson) + { + this.filterJson = filterJson; + return (this); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedfilters/SavedFiltersMetaDataProvider.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedfilters/SavedFiltersMetaDataProvider.java new file mode 100644 index 00000000..dd004fa0 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedfilters/SavedFiltersMetaDataProvider.java @@ -0,0 +1,88 @@ +/* + * 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 . + */ + +package com.kingsrook.qqq.backend.core.model.savedfilters; + + +import java.util.function.Consumer; +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.possiblevalues.PVSValueFormatAndFields; +import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource; +import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSourceType; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class SavedFiltersMetaDataProvider +{ + + + /******************************************************************************* + ** + *******************************************************************************/ + public void defineAll(QInstance instance, String backendName, Consumer backendDetailEnricher) throws QException + { + instance.addTable(defineSavedFilterTable(backendName, backendDetailEnricher)); + instance.addPossibleValueSource(defineSavedFilterPossibleValueSource()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private QTableMetaData defineSavedFilterTable(String backendName, Consumer backendDetailEnricher) throws QException + { + QTableMetaData table = new QTableMetaData() + .withName(SavedFilter.TABLE_NAME) + .withLabel("Saved Filter") + .withRecordLabelFormat("%s") + .withRecordLabelFields("label") + .withBackendName(backendName) + .withPrimaryKeyField("id") + .withFieldsFromEntity(SavedFilter.class); + + if(backendDetailEnricher != null) + { + backendDetailEnricher.accept(table); + } + + return (table); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private QPossibleValueSource defineSavedFilterPossibleValueSource() + { + return new QPossibleValueSource() + .withName(SavedFilter.TABLE_NAME) + .withType(QPossibleValueSourceType.TABLE) + .withTableName(SavedFilter.TABLE_NAME) + .withValueFormatAndFields(PVSValueFormatAndFields.LABEL_ONLY); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/scripts/ScriptsMetaDataProvider.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/scripts/ScriptsMetaDataProvider.java index de3399b9..1cb972a6 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/scripts/ScriptsMetaDataProvider.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/scripts/ScriptsMetaDataProvider.java @@ -327,7 +327,12 @@ public class ScriptsMetaDataProvider .withSection(new QFieldSection("dates", new QIcon().withName("calendar_month"), Tier.T3, List.of("createDate", "modifyDate"))); tableMetaData.getField("scriptId").withPossibleValueSourceFilter(new QQueryFilter( - new QFilterCriteria("scriptType.name", QCriteriaOperator.EQUALS, SCRIPT_TYPE_NAME_RECORD) + new QFilterCriteria("scriptType.name", QCriteriaOperator.EQUALS, SCRIPT_TYPE_NAME_RECORD), + new QFilterCriteria("script.tableName", QCriteriaOperator.EQUALS, "${input.tableName}") + )); + + tableMetaData.getField("filterId").withPossibleValueSourceFilter(new QQueryFilter( + new QFilterCriteria("tableName", QCriteriaOperator.EQUALS, "${input.tableName}") )); return tableMetaData; From 2c192a3fd91689640b852c7e5e94ab9994326e71 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Mon, 19 Jun 2023 12:14:26 -0500 Subject: [PATCH 04/10] Add SavedFiltersMetaDataProvider (as we've introduced a dependency between it and ScriptsMetaDataProvider (through tableTriggers) --- .../java/com/kingsrook/qqq/backend/javalin/TestUtils.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java index aaf4dd0e..0245e235 100644 --- a/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java +++ b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java @@ -66,6 +66,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportView; import com.kingsrook.qqq.backend.core.model.metadata.reporting.ReportType; 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.savedfilters.SavedFiltersMetaDataProvider; import com.kingsrook.qqq.backend.core.model.scripts.ScriptsMetaDataProvider; import com.kingsrook.qqq.backend.core.processes.implementations.mock.MockBackendStep; import com.kingsrook.qqq.backend.module.rdbms.jdbc.ConnectionManager; @@ -156,6 +157,7 @@ public class TestUtils qInstance.addBackend(defineMemoryBackend()); try { + new SavedFiltersMetaDataProvider().defineAll(qInstance, defineMemoryBackend().getName(), null); new ScriptsMetaDataProvider().defineAll(qInstance, defineMemoryBackend().getName(), null); } catch(Exception e) @@ -362,8 +364,7 @@ public class TestUtils .withType(QPossibleValueSourceType.TABLE) .withTableName(TABLE_NAME_PERSON) .withValueFormatAndFields(PVSValueFormatAndFields.LABEL_PARENS_ID) - .withOrderByField("id") - ); + .withOrderByField("id")); } From 79304adcb05ba2fd7ff4ed1d2ee80661f5018a06 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Mon, 19 Jun 2023 12:19:11 -0500 Subject: [PATCH 05/10] Move saved filter processes to qqq --- .../SavedFiltersMetaDataProvider.java | 6 + .../DeleteSavedFilterProcess.java | 88 +++++++++++++ .../savedfilters/QuerySavedFilterProcess.java | 117 ++++++++++++++++++ .../savedfilters/StoreSavedFilterProcess.java | 117 ++++++++++++++++++ 4 files changed, 328 insertions(+) create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedfilters/DeleteSavedFilterProcess.java create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedfilters/QuerySavedFilterProcess.java create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedfilters/StoreSavedFilterProcess.java diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedfilters/SavedFiltersMetaDataProvider.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedfilters/SavedFiltersMetaDataProvider.java index dd004fa0..cb03e071 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedfilters/SavedFiltersMetaDataProvider.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedfilters/SavedFiltersMetaDataProvider.java @@ -29,6 +29,9 @@ import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.PVSValueForm import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource; import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSourceType; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.processes.implementations.savedfilters.DeleteSavedFilterProcess; +import com.kingsrook.qqq.backend.core.processes.implementations.savedfilters.QuerySavedFilterProcess; +import com.kingsrook.qqq.backend.core.processes.implementations.savedfilters.StoreSavedFilterProcess; /******************************************************************************* @@ -45,6 +48,9 @@ public class SavedFiltersMetaDataProvider { instance.addTable(defineSavedFilterTable(backendName, backendDetailEnricher)); instance.addPossibleValueSource(defineSavedFilterPossibleValueSource()); + instance.addProcess(QuerySavedFilterProcess.getProcessMetaData()); + instance.addProcess(StoreSavedFilterProcess.getProcessMetaData()); + instance.addProcess(DeleteSavedFilterProcess.getProcessMetaData()); } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedfilters/DeleteSavedFilterProcess.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedfilters/DeleteSavedFilterProcess.java new file mode 100644 index 00000000..a0a6f1f8 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedfilters/DeleteSavedFilterProcess.java @@ -0,0 +1,88 @@ +/* + * 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 . + */ + +package com.kingsrook.qqq.backend.core.processes.implementations.savedfilters; + + +import java.util.List; +import com.kingsrook.qqq.backend.core.actions.ActionHelper; +import com.kingsrook.qqq.backend.core.actions.processes.BackendStep; +import com.kingsrook.qqq.backend.core.actions.tables.DeleteAction; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.logging.QLogger; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput; +import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; +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.savedfilters.SavedFilter; + + +/******************************************************************************* + ** Process used by the delete filter dialog + *******************************************************************************/ +public class DeleteSavedFilterProcess implements BackendStep +{ + private static final QLogger LOG = QLogger.getLogger(DeleteSavedFilterProcess.class); + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static QProcessMetaData getProcessMetaData() + { + return (new QProcessMetaData() + .withName("deleteSavedFilter") + .withStepList(List.of( + new QBackendStepMetaData() + .withCode(new QCodeReference(DeleteSavedFilterProcess.class)) + .withName("delete") + ))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException + { + ActionHelper.validateSession(runBackendStepInput); + + try + { + Integer savedFilterId = runBackendStepInput.getValueInteger("id"); + + DeleteInput input = new DeleteInput(); + input.setTableName(SavedFilter.TABLE_NAME); + input.setPrimaryKeys(List.of(savedFilterId)); + new DeleteAction().execute(input); + } + catch(Exception e) + { + LOG.warn("Error deleting saved filter", e); + throw (e); + } + } +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedfilters/QuerySavedFilterProcess.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedfilters/QuerySavedFilterProcess.java new file mode 100644 index 00000000..dc50ed17 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedfilters/QuerySavedFilterProcess.java @@ -0,0 +1,117 @@ +/* + * 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 . + */ + +package com.kingsrook.qqq.backend.core.processes.implementations.savedfilters; + + +import java.io.Serializable; +import java.util.List; +import com.kingsrook.qqq.backend.core.actions.ActionHelper; +import com.kingsrook.qqq.backend.core.actions.processes.BackendStep; +import com.kingsrook.qqq.backend.core.actions.tables.GetAction; +import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.logging.QLogger; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput; +import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; +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.savedfilters.SavedFilter; + + +/******************************************************************************* + ** Process used by the saved filter dialogs + *******************************************************************************/ +public class QuerySavedFilterProcess implements BackendStep +{ + private static final QLogger LOG = QLogger.getLogger(QuerySavedFilterProcess.class); + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static QProcessMetaData getProcessMetaData() + { + return (new QProcessMetaData() + .withName("querySavedFilter") + .withStepList(List.of( + new QBackendStepMetaData() + .withCode(new QCodeReference(QuerySavedFilterProcess.class)) + .withName("query") + ))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException + { + ActionHelper.validateSession(runBackendStepInput); + + try + { + Integer savedFilterId = runBackendStepInput.getValueInteger("id"); + if(savedFilterId != null) + { + GetInput input = new GetInput(); + input.setTableName(SavedFilter.TABLE_NAME); + input.setPrimaryKey(savedFilterId); + + GetOutput output = new GetAction().execute(input); + runBackendStepOutput.addRecord(output.getRecord()); + runBackendStepOutput.addValue("savedFilter", output.getRecord()); + runBackendStepOutput.addValue("savedFilterList", (Serializable) List.of(output.getRecord())); + } + else + { + String tableName = runBackendStepInput.getValueString("tableName"); + + QueryInput input = new QueryInput(); + input.setTableName(SavedFilter.TABLE_NAME); + input.setFilter(new QQueryFilter() + .withCriteria(new QFilterCriteria("tableName", QCriteriaOperator.EQUALS, tableName)) + .withOrderBy(new QFilterOrderBy("label"))); + + QueryOutput output = new QueryAction().execute(input); + runBackendStepOutput.setRecords(output.getRecords()); + runBackendStepOutput.addValue("savedFilterList", (Serializable) output.getRecords()); + } + } + catch(Exception e) + { + LOG.warn("Error deleting saved filter", e); + throw (e); + } + } +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedfilters/StoreSavedFilterProcess.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedfilters/StoreSavedFilterProcess.java new file mode 100644 index 00000000..37bc167e --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedfilters/StoreSavedFilterProcess.java @@ -0,0 +1,117 @@ +/* + * 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 . + */ + +package com.kingsrook.qqq.backend.core.processes.implementations.savedfilters; + + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import com.kingsrook.qqq.backend.core.actions.ActionHelper; +import com.kingsrook.qqq.backend.core.actions.processes.BackendStep; +import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; +import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.logging.QLogger; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput; +import com.kingsrook.qqq.backend.core.model.actions.tables.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.UpdateOutput; +import com.kingsrook.qqq.backend.core.model.data.QRecord; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; +import com.kingsrook.qqq.backend.core.model.savedfilters.SavedFilter; + + +/******************************************************************************* + ** Process used by the saved filter dialog + *******************************************************************************/ +public class StoreSavedFilterProcess implements BackendStep +{ + private static final QLogger LOG = QLogger.getLogger(StoreSavedFilterProcess.class); + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static QProcessMetaData getProcessMetaData() + { + return (new QProcessMetaData() + .withName("storeSavedFilter") + .withStepList(List.of( + new QBackendStepMetaData() + .withCode(new QCodeReference(StoreSavedFilterProcess.class)) + .withName("store") + ))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException + { + ActionHelper.validateSession(runBackendStepInput); + + try + { + QRecord qRecord = new QRecord() + .withValue("id", runBackendStepInput.getValueInteger("id")) + .withValue("label", runBackendStepInput.getValueString("label")) + .withValue("tableName", runBackendStepInput.getValueString("tableName")) + .withValue("filterJson", runBackendStepInput.getValueString("filterJson")) + .withValue("userId", runBackendStepInput.getSession().getUser().getIdReference()); + + List savedFilterList = new ArrayList<>(); + if(qRecord.getValueInteger("id") == null) + { + InsertInput input = new InsertInput(); + input.setTableName(SavedFilter.TABLE_NAME); + input.setRecords(List.of(qRecord)); + + InsertOutput output = new InsertAction().execute(input); + savedFilterList = output.getRecords(); + } + else + { + UpdateInput input = new UpdateInput(); + input.setTableName(SavedFilter.TABLE_NAME); + input.setRecords(List.of(qRecord)); + + UpdateOutput output = new UpdateAction().execute(input); + savedFilterList = output.getRecords(); + } + + runBackendStepOutput.addValue("savedFilterList", (Serializable) savedFilterList); + } + catch(Exception e) + { + LOG.warn("Error storing data saved filter", e); + throw (e); + } + } +} From 3e113a12b3e59017643d359b33257dd0a4066f92 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Mon, 19 Jun 2023 12:30:10 -0500 Subject: [PATCH 06/10] Initial checkin --- .../savedfilters/SavedFilterProcessTests.java | 143 ++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100644 qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/savedfilters/SavedFilterProcessTests.java diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/savedfilters/SavedFilterProcessTests.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/savedfilters/SavedFilterProcessTests.java new file mode 100644 index 00000000..d3c0bf5b --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/savedfilters/SavedFilterProcessTests.java @@ -0,0 +1,143 @@ +/* + * 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 . + */ + +package com.kingsrook.qqq.backend.core.processes.implementations.savedfilters; + + +import java.util.List; +import com.kingsrook.qqq.backend.core.BaseTest; +import com.kingsrook.qqq.backend.core.actions.processes.RunProcessAction; +import com.kingsrook.qqq.backend.core.context.QContext; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessOutput; +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.data.QRecord; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.savedfilters.SavedFiltersMetaDataProvider; +import com.kingsrook.qqq.backend.core.utils.JsonUtils; +import com.kingsrook.qqq.backend.core.utils.TestUtils; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + + +/******************************************************************************* + ** Unit test for all saved filter processes + *******************************************************************************/ +class SavedFilterProcessTests extends BaseTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void test() throws QException + { + QInstance qInstance = QContext.getQInstance(); + new SavedFiltersMetaDataProvider().defineAll(qInstance, TestUtils.MEMORY_BACKEND_NAME, null); + String tableName = TestUtils.TABLE_NAME_PERSON_MEMORY; + + { + /////////////////////////////////////////// + // query - should be no filters to start // + /////////////////////////////////////////// + RunProcessInput runProcessInput = new RunProcessInput(); + runProcessInput.setProcessName(QuerySavedFilterProcess.getProcessMetaData().getName()); + runProcessInput.addValue("tableName", tableName); + RunProcessOutput runProcessOutput = new RunProcessAction().execute(runProcessInput); + assertEquals(0, ((List) runProcessOutput.getValues().get("savedFilterList")).size()); + } + + Integer savedFilterId; + { + //////////////////////// + // store a new filter // + //////////////////////// + RunProcessInput runProcessInput = new RunProcessInput(); + runProcessInput.setProcessName(StoreSavedFilterProcess.getProcessMetaData().getName()); + runProcessInput.addValue("label", "My Filter"); + runProcessInput.addValue("tableName", tableName); + runProcessInput.addValue("filterJson", JsonUtils.toJson(new QQueryFilter(new QFilterCriteria("id", QCriteriaOperator.EQUALS, 47)))); + RunProcessOutput runProcessOutput = new RunProcessAction().execute(runProcessInput); + List savedFilterList = (List) runProcessOutput.getValues().get("savedFilterList"); + assertEquals(1, savedFilterList.size()); + savedFilterId = savedFilterList.get(0).getValueInteger("id"); + assertNotNull(savedFilterId); + } + + { + //////////////////////////////////// + // query - should find our filter // + //////////////////////////////////// + RunProcessInput runProcessInput = new RunProcessInput(); + runProcessInput.setProcessName(QuerySavedFilterProcess.getProcessMetaData().getName()); + runProcessInput.addValue("tableName", tableName); + RunProcessOutput runProcessOutput = new RunProcessAction().execute(runProcessInput); + List savedFilterList = (List) runProcessOutput.getValues().get("savedFilterList"); + assertEquals(1, savedFilterList.size()); + assertEquals(1, savedFilterList.get(0).getValueInteger("id")); + assertEquals("My Filter", savedFilterList.get(0).getValueString("label")); + } + + { + /////////////////////// + // update our filter // + /////////////////////// + RunProcessInput runProcessInput = new RunProcessInput(); + runProcessInput.setProcessName(StoreSavedFilterProcess.getProcessMetaData().getName()); + runProcessInput.addValue("id", savedFilterId); + runProcessInput.addValue("label", "My Updated Filter"); + runProcessInput.addValue("tableName", tableName); + runProcessInput.addValue("filterJson", JsonUtils.toJson(new QQueryFilter(new QFilterCriteria("id", QCriteriaOperator.EQUALS, 47)))); + RunProcessOutput runProcessOutput = new RunProcessAction().execute(runProcessInput); + List savedFilterList = (List) runProcessOutput.getValues().get("savedFilterList"); + assertEquals(1, savedFilterList.size()); + assertEquals(1, savedFilterList.get(0).getValueInteger("id")); + assertEquals("My Updated Filter", savedFilterList.get(0).getValueString("label")); + } + + { + /////////////////////// + // delete our filter // + /////////////////////// + RunProcessInput runProcessInput = new RunProcessInput(); + runProcessInput.setProcessName(DeleteSavedFilterProcess.getProcessMetaData().getName()); + runProcessInput.addValue("id", savedFilterId); + RunProcessOutput runProcessOutput = new RunProcessAction().execute(runProcessInput); + } + + { + //////////////////////////////////////// + // query - should be no filters again // + //////////////////////////////////////// + RunProcessInput runProcessInput = new RunProcessInput(); + runProcessInput.setProcessName(QuerySavedFilterProcess.getProcessMetaData().getName()); + runProcessInput.addValue("tableName", tableName); + RunProcessOutput runProcessOutput = new RunProcessAction().execute(runProcessInput); + assertEquals(0, ((List) runProcessOutput.getValues().get("savedFilterList")).size()); + } + + } + +} \ No newline at end of file From 0f799339d648219f0a229b904cd1febbc150e68f Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Mon, 19 Jun 2023 12:33:01 -0500 Subject: [PATCH 07/10] Set user in new sessions --- .../test/java/com/kingsrook/qqq/backend/core/BaseTest.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/BaseTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/BaseTest.java index f436eb11..1cae9018 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/BaseTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/BaseTest.java @@ -26,6 +26,7 @@ import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.logging.QLogger; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.session.QSession; +import com.kingsrook.qqq.backend.core.model.session.QUser; import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryRecordStore; import com.kingsrook.qqq.backend.core.utils.TestUtils; import org.junit.jupiter.api.AfterEach; @@ -47,7 +48,10 @@ public class BaseTest @BeforeEach void baseBeforeEach() { - QContext.init(TestUtils.defineInstance(), new QSession()); + QContext.init(TestUtils.defineInstance(), new QSession() + .withUser(new QUser() + .withIdReference("001") + .withFullName("Anonymous"))); resetMemoryRecordStore(); } From 3791c069c7c6e210eca69f8b422863d796e0eded Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Tue, 20 Jun 2023 09:06:57 -0500 Subject: [PATCH 08/10] Add convertObjectToJava to code executors - for converting language objects to java objects --- .../actions/scripts/ExecuteCodeAction.java | 11 +++ .../core/actions/scripts/QCodeExecutor.java | 10 ++ .../actions/scripts/QCodeExecutorAware.java | 36 +++++++ .../javascript/QJavaScriptExecutor.java | 59 +++++++++++ .../javascript/ExecuteCodeActionTest.java | 97 +++++++++++++++++++ .../qqq/api/utils/ApiScriptUtils.java | 65 ++++++++++++- 6 files changed, 277 insertions(+), 1 deletion(-) create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/QCodeExecutorAware.java diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/ExecuteCodeAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/ExecuteCodeAction.java index c142fc1f..875cf2f5 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/ExecuteCodeAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/ExecuteCodeAction.java @@ -101,6 +101,17 @@ public class ExecuteCodeAction context.putAll(input.getInput()); } + ///////////////////////////////////////////////////////////////////////////////// + // set the qCodeExecutor into any context objects which are QCodeExecutorAware // + ///////////////////////////////////////////////////////////////////////////////// + for(Serializable value : context.values()) + { + if(value instanceof QCodeExecutorAware qCodeExecutorAware) + { + qCodeExecutorAware.setQCodeExecutor(qCodeExecutor); + } + } + Serializable codeOutput = qCodeExecutor.execute(codeReference, context, executionLogger); output.setOutput(codeOutput); executionLogger.acceptExecutionEnd(codeOutput); diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/QCodeExecutor.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/QCodeExecutor.java index e6cd808a..a0e06f81 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/QCodeExecutor.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/QCodeExecutor.java @@ -41,4 +41,14 @@ public interface QCodeExecutor *******************************************************************************/ Serializable execute(QCodeReference codeReference, Map inputContext, QCodeExecutionLoggerInterface executionLogger) throws QCodeException; + /******************************************************************************* + ** Process an object from the script's language/runtime into a (more) native java object. + ** e.g., a Nashorn ScriptObjectMirror will end up as a "primitive", or a List or Map of such + ** + *******************************************************************************/ + default Object convertObjectToJava(Object object) + { + return (object); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/QCodeExecutorAware.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/QCodeExecutorAware.java new file mode 100644 index 00000000..e15dd2a6 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/scripts/QCodeExecutorAware.java @@ -0,0 +1,36 @@ +/* + * 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 . + */ + +package com.kingsrook.qqq.backend.core.actions.scripts; + + +/******************************************************************************* + ** Interface for classes that can accept a QCodeExecutor object via a setter. + *******************************************************************************/ +public interface QCodeExecutorAware +{ + + /******************************************************************************* + ** + *******************************************************************************/ + void setQCodeExecutor(QCodeExecutor qCodeExecutor); + +} diff --git a/qqq-language-support-javascript/src/main/java/com/kingsrook/qqq/languages/javascript/QJavaScriptExecutor.java b/qqq-language-support-javascript/src/main/java/com/kingsrook/qqq/languages/javascript/QJavaScriptExecutor.java index 15f7ac98..7a708ba7 100644 --- a/qqq-language-support-javascript/src/main/java/com/kingsrook/qqq/languages/javascript/QJavaScriptExecutor.java +++ b/qqq-language-support-javascript/src/main/java/com/kingsrook/qqq/languages/javascript/QJavaScriptExecutor.java @@ -27,6 +27,10 @@ import javax.script.ScriptEngine; import javax.script.ScriptEngineManager; import javax.script.ScriptException; import java.io.Serializable; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; import java.util.Map; import com.kingsrook.qqq.backend.core.actions.scripts.QCodeExecutor; import com.kingsrook.qqq.backend.core.actions.scripts.logging.QCodeExecutionLoggerInterface; @@ -36,8 +40,10 @@ import com.kingsrook.qqq.backend.core.utils.ExceptionUtils; import com.kingsrook.qqq.backend.core.utils.StringUtils; import org.apache.commons.lang.NotImplementedException; import org.openjdk.nashorn.api.scripting.NashornScriptEngineFactory; +import org.openjdk.nashorn.api.scripting.ScriptObjectMirror; import org.openjdk.nashorn.internal.runtime.ECMAException; import org.openjdk.nashorn.internal.runtime.ParserException; +import org.openjdk.nashorn.internal.runtime.Undefined; /******************************************************************************* @@ -59,6 +65,59 @@ public class QJavaScriptExecutor implements QCodeExecutor + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public Object convertObjectToJava(Object object) + { + if(object == null || object instanceof String || object instanceof Boolean || object instanceof Integer || object instanceof Long || object instanceof BigDecimal) + { + return (object); + } + else if(object instanceof Float f) + { + return (new BigDecimal(f)); + } + else if(object instanceof Double d) + { + return (new BigDecimal(d)); + } + else if(object instanceof Undefined) + { + //////////////////////////////////////////////////////////////////////////////////////////////////////////// + // well, we always said we wanted javascript to treat null & undefined the same way... here's our chance // + //////////////////////////////////////////////////////////////////////////////////////////////////////////// + return (null); + } + + if(object instanceof ScriptObjectMirror scriptObjectMirror) + { + if(scriptObjectMirror.isArray()) + { + List result = new ArrayList<>(); + for(String key : scriptObjectMirror.keySet()) + { + result.add(Integer.parseInt(key), convertObjectToJava(scriptObjectMirror.get(key))); + } + return (result); + } + else + { + Map result = new HashMap<>(); + for(String key : scriptObjectMirror.keySet()) + { + result.put(key, convertObjectToJava(scriptObjectMirror.get(key))); + } + return (result); + } + } + + return QCodeExecutor.super.convertObjectToJava(object); + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-language-support-javascript/src/test/java/com/kingsrook/qqq/languages/javascript/ExecuteCodeActionTest.java b/qqq-language-support-javascript/src/test/java/com/kingsrook/qqq/languages/javascript/ExecuteCodeActionTest.java index 3c1bf009..43338983 100644 --- a/qqq-language-support-javascript/src/test/java/com/kingsrook/qqq/languages/javascript/ExecuteCodeActionTest.java +++ b/qqq-language-support-javascript/src/test/java/com/kingsrook/qqq/languages/javascript/ExecuteCodeActionTest.java @@ -23,7 +23,12 @@ package com.kingsrook.qqq.languages.javascript; import java.io.Serializable; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import com.kingsrook.qqq.backend.core.actions.scripts.ExecuteCodeAction; +import com.kingsrook.qqq.backend.core.actions.scripts.QCodeExecutor; +import com.kingsrook.qqq.backend.core.actions.scripts.QCodeExecutorAware; import com.kingsrook.qqq.backend.core.exceptions.QCodeException; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeInput; @@ -31,6 +36,7 @@ import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeOutput; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType; +import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; @@ -241,10 +247,50 @@ class ExecuteCodeActionTest extends BaseTest + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testConvertObjectToJava() throws QException + { + TestQCodeExecutorAware converter = new TestQCodeExecutorAware(); + testOne(1, """ + converter.convertObject("one", 1); + converter.convertObject("two", "two"); + converter.convertObject("true", true); + converter.convertObject("null", null); + converter.convertObject("undefined", undefined); + converter.convertObject("flatMap", {"a": 1, "b": "c"}); + converter.convertObject("flatList", ["a", 1, "b", "c"]); + converter.convertObject("mixedMap", {"a": [1, {"2": "3"}], "b": {"c": ["d"]}}); + """, MapBuilder.of("converter", converter)); + + assertEquals(1, converter.getConvertedObject("one")); + assertEquals("two", converter.getConvertedObject("two")); + assertEquals(true, converter.getConvertedObject("true")); + assertNull(converter.getConvertedObject("null")); + assertNull(converter.getConvertedObject("undefined")); + assertEquals(Map.of("a", 1, "b", "c"), converter.getConvertedObject("flatMap")); + assertEquals(List.of("a", 1, "b", "c"), converter.getConvertedObject("flatList")); + assertEquals(Map.of("a", List.of(1, Map.of("2", "3")), "b", Map.of("c", List.of("d"))), converter.getConvertedObject("mixedMap")); + } + + + /******************************************************************************* ** *******************************************************************************/ private OneTestOutput testOne(Integer inputValueC, String code) throws QException + { + return (testOne(inputValueC, code, null)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private OneTestOutput testOne(Integer inputValueC, String code, Map additionalContext) throws QException { System.out.println(); QInstance instance = TestUtils.defineInstance(); @@ -259,6 +305,14 @@ class ExecuteCodeActionTest extends BaseTest input.withContext("input", testInput); input.withContext("output", testOutput); + if(additionalContext != null) + { + for(Map.Entry entry : additionalContext.entrySet()) + { + input.withContext(entry.getKey(), entry.getValue()); + } + } + ExecuteCodeOutput output = new ExecuteCodeOutput(); ExecuteCodeAction executeCodeAction = new ExecuteCodeAction(); @@ -269,6 +323,49 @@ class ExecuteCodeActionTest extends BaseTest + /******************************************************************************* + ** + *******************************************************************************/ + public static class TestQCodeExecutorAware implements QCodeExecutorAware, Serializable + { + private QCodeExecutor qCodeExecutor; + + private Map convertedObjectMap = new HashMap<>(); + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void setQCodeExecutor(QCodeExecutor qCodeExecutor) + { + this.qCodeExecutor = qCodeExecutor; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public void convertObject(String name, Object inputObject) + { + convertedObjectMap.put(name, qCodeExecutor.convertObjectToJava(inputObject)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public Object getConvertedObject(String name) + { + return (convertedObjectMap.get(name)); + } + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/utils/ApiScriptUtils.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/utils/ApiScriptUtils.java index f06585d2..af776782 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/utils/ApiScriptUtils.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/utils/ApiScriptUtils.java @@ -32,19 +32,24 @@ import com.kingsrook.qqq.api.actions.QRecordApiAdapter; import com.kingsrook.qqq.api.model.APIVersion; import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaData; import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaDataContainer; +import com.kingsrook.qqq.backend.core.actions.scripts.QCodeExecutor; +import com.kingsrook.qqq.backend.core.actions.scripts.QCodeExecutorAware; import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.data.QRecord; +import com.kingsrook.qqq.backend.core.utils.JsonUtils; /******************************************************************************* ** Object injected into script context, for interfacing with a QQQ API. *******************************************************************************/ -public class ApiScriptUtils implements Serializable +public class ApiScriptUtils implements QCodeExecutorAware, Serializable { private String apiName; private String apiVersion; + private QCodeExecutor qCodeExecutor; + /******************************************************************************* @@ -165,6 +170,7 @@ public class ApiScriptUtils implements Serializable public Map insert(String tableApiName, Object body) throws QException { validateApiNameAndVersion("insert(" + tableApiName + ")"); + body = processBodyToJsonString(body); return (ApiImplementation.insert(getApiInstanceMetaData(), apiVersion, tableApiName, String.valueOf(body))); } @@ -176,6 +182,7 @@ public class ApiScriptUtils implements Serializable public List> bulkInsert(String tableApiName, Object body) throws QException { validateApiNameAndVersion("bulkInsert(" + tableApiName + ")"); + body = processBodyToJsonString(body); return (ApiImplementation.bulkInsert(getApiInstanceMetaData(), apiVersion, tableApiName, String.valueOf(body))); } @@ -187,17 +194,61 @@ public class ApiScriptUtils implements Serializable public void update(String tableApiName, Object primaryKey, Object body) throws QException { validateApiNameAndVersion("update(" + tableApiName + "," + primaryKey + ")"); + body = processBodyToJsonString(body); ApiImplementation.update(getApiInstanceMetaData(), apiVersion, tableApiName, String.valueOf(primaryKey), String.valueOf(body)); } + /******************************************************************************* + ** Take a "body" object, which maybe defined in the script's language/run-time, + ** and try to process it into a JSON String (which is what the API Implementation wants) + *******************************************************************************/ + private Object processBodyToJsonString(Object body) + { + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // if the caller already supplied the object as a string, then return that string. // + // and in case it can't be parsed as json, well, let that error come out of the api implementation, and go back to the caller. // + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + if(body instanceof String) + { + return (body); + } + + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // if the input body wasn't a json string, try to convert it from a language-type object (e.g., javscript) to a java-object, // + // then make JSON out of that for the APIImplementation // + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + Object bodyJavaObject = processInputObjectViaCodeExecutor(body); + return JsonUtils.toJson(bodyJavaObject); + } + + + + /******************************************************************************* + ** Use the QCodeExecutor (if we have one) to process an input object from the + ** script's language into a (more) native java object. + ** e.g., a Nashorn ScriptObjectMirror will end up as a "primitive", or a List or Map of such + *******************************************************************************/ + private Object processInputObjectViaCodeExecutor(Object body) + { + if(qCodeExecutor == null || body == null) + { + return (body); + } + + return (qCodeExecutor.convertObjectToJava(body)); + } + + + /******************************************************************************* ** *******************************************************************************/ public List> bulkUpdate(String tableApiName, Object body) throws QException { validateApiNameAndVersion("bulkUpdate(" + tableApiName + ")"); + body = processBodyToJsonString(body); return (ApiImplementation.bulkUpdate(getApiInstanceMetaData(), apiVersion, tableApiName, String.valueOf(body))); } @@ -220,6 +271,7 @@ public class ApiScriptUtils implements Serializable public List> bulkDelete(String tableApiName, Object body) throws QException { validateApiNameAndVersion("bulkDelete(" + tableApiName + ")"); + body = processBodyToJsonString(body); return (ApiImplementation.bulkDelete(getApiInstanceMetaData(), apiVersion, tableApiName, String.valueOf(body))); } @@ -257,4 +309,15 @@ public class ApiScriptUtils implements Serializable } return paramMap; } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void setQCodeExecutor(QCodeExecutor qCodeExecutor) + { + this.qCodeExecutor = qCodeExecutor; + } } From 57569e4c846a6aef35b31ab515d0916dbc9490e9 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Tue, 20 Jun 2023 09:07:15 -0500 Subject: [PATCH 09/10] Escape identifiers in column names --- .../qqq/backend/module/rdbms/actions/RDBMSUpdateAction.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSUpdateAction.java b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSUpdateAction.java index 26af479b..035794ee 100644 --- a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSUpdateAction.java +++ b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSUpdateAction.java @@ -248,7 +248,7 @@ public class RDBMSUpdateAction extends AbstractRDBMSAction implements UpdateInte private String writeUpdateSQLPrefix(QTableMetaData table, List fieldsBeingUpdated) { String columns = fieldsBeingUpdated.stream() - .map(f -> this.getColumnName(table.getField(f)) + " = ?") + .map(f -> escapeIdentifier(this.getColumnName(table.getField(f))) + " = ?") .collect(Collectors.joining(", ")); String tableName = escapeIdentifier(getTableName(table)); From 7af5ad2655400c02a2cfe4f8a535e1322bf31d37 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Tue, 20 Jun 2023 10:22:21 -0500 Subject: [PATCH 10/10] Fix to support null-filter id on table-triggers --- .../PollingAutomationPerTableRunner.java | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/automation/polling/PollingAutomationPerTableRunner.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/automation/polling/PollingAutomationPerTableRunner.java index 2bb0a470..52cc7646 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/automation/polling/PollingAutomationPerTableRunner.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/automation/polling/PollingAutomationPerTableRunner.java @@ -280,17 +280,19 @@ public class PollingAutomationPerTableRunner implements Runnable try { - Integer filterId = tableTrigger.getFilterId(); - - GetInput getInput = new GetInput(); - getInput.setTableName(SavedFilter.TABLE_NAME); - getInput.setPrimaryKey(filterId); - GetOutput getOutput = new GetAction().execute(getInput); - QQueryFilter filter = null; - if(getOutput.getRecord() != null) + QQueryFilter filter = null; + Integer filterId = tableTrigger.getFilterId(); + if(filterId != null) { - SavedFilter savedFilter = new SavedFilter(getOutput.getRecord()); - filter = JsonUtils.toObject(savedFilter.getFilterJson(), QQueryFilter.class); + GetInput getInput = new GetInput(); + getInput.setTableName(SavedFilter.TABLE_NAME); + getInput.setPrimaryKey(filterId); + GetOutput getOutput = new GetAction().execute(getInput); + if(getOutput.getRecord() != null) + { + SavedFilter savedFilter = new SavedFilter(getOutput.getRecord()); + filter = JsonUtils.toObject(savedFilter.getFilterJson(), QQueryFilter.class); + } } rs.add(new TableAutomationAction()