diff --git a/src/main/java/com/kingsrook/qqq/backend/core/actions/async/AsyncJobCallback.java b/src/main/java/com/kingsrook/qqq/backend/core/actions/async/AsyncJobCallback.java index 2822f81c..fd5993c3 100644 --- a/src/main/java/com/kingsrook/qqq/backend/core/actions/async/AsyncJobCallback.java +++ b/src/main/java/com/kingsrook/qqq/backend/core/actions/async/AsyncJobCallback.java @@ -52,7 +52,7 @@ public class AsyncJobCallback /******************************************************************************* - ** + ** Update the message *******************************************************************************/ public void updateStatus(String message) { @@ -63,7 +63,20 @@ public class AsyncJobCallback /******************************************************************************* - ** + ** Update all 3 status fields + *******************************************************************************/ + public void updateStatus(String message, int current, int total) + { + this.asyncJobStatus.setMessage(message); + this.asyncJobStatus.setCurrent(current); + this.asyncJobStatus.setTotal(total); + storeUpdatedStatus(); + } + + + + /******************************************************************************* + ** Update the current and total fields (e.g., 1 of 2, 2 of 2, 3 of 2) *******************************************************************************/ public void updateStatus(int current, int total) { @@ -75,13 +88,12 @@ public class AsyncJobCallback /******************************************************************************* - ** + ** Remove the values from the current & total fields *******************************************************************************/ - public void updateStatus(String message, int current, int total) + public void clearCurrentAndTotal() { - this.asyncJobStatus.setMessage(message); - this.asyncJobStatus.setCurrent(current); - this.asyncJobStatus.setTotal(total); + this.asyncJobStatus.setCurrent(null); + this.asyncJobStatus.setTotal(null); storeUpdatedStatus(); } diff --git a/src/main/java/com/kingsrook/qqq/backend/core/actions/interfaces/DeleteInterface.java b/src/main/java/com/kingsrook/qqq/backend/core/actions/interfaces/DeleteInterface.java index 22f15c17..669967d2 100644 --- a/src/main/java/com/kingsrook/qqq/backend/core/actions/interfaces/DeleteInterface.java +++ b/src/main/java/com/kingsrook/qqq/backend/core/actions/interfaces/DeleteInterface.java @@ -37,4 +37,17 @@ public interface DeleteInterface ** *******************************************************************************/ DeleteOutput execute(DeleteInput deleteInput) throws QException; + + /******************************************************************************* + ** Specify whether this particular module's delete action natively supports + ** receiving a queryFilter as input (e.g., SQL does). If the module doesn't + ** support a query filter, then the qqq framework (DeleteAction) will, if it + ** receives a queryFilter in its input, it will execute the query, and pass + ** the list of primary keys down into the module's delete implementation. + *******************************************************************************/ + default boolean supportsQueryFilterInput() + { + return (false); + } + } diff --git a/src/main/java/com/kingsrook/qqq/backend/core/actions/processes/RunProcessAction.java b/src/main/java/com/kingsrook/qqq/backend/core/actions/processes/RunProcessAction.java index bc34c62b..67d32747 100644 --- a/src/main/java/com/kingsrook/qqq/backend/core/actions/processes/RunProcessAction.java +++ b/src/main/java/com/kingsrook/qqq/backend/core/actions/processes/RunProcessAction.java @@ -127,7 +127,7 @@ public class RunProcessAction /////////////////////// // Run backend steps // /////////////////////// - runBackendStep(runProcessInput, process, runProcessOutput, stateKey, backendStepMetaData, processState); + runBackendStep(runProcessInput, process, runProcessOutput, stateKey, backendStepMetaData, process, processState); } else { @@ -225,11 +225,12 @@ public class RunProcessAction /******************************************************************************* ** Run a single backend step. *******************************************************************************/ - private void runBackendStep(RunProcessInput runProcessInput, QProcessMetaData process, RunProcessOutput runProcessOutput, UUIDAndTypeStateKey stateKey, QBackendStepMetaData backendStep, ProcessState processState) throws Exception + private void runBackendStep(RunProcessInput runProcessInput, QProcessMetaData process, RunProcessOutput runProcessOutput, UUIDAndTypeStateKey stateKey, QBackendStepMetaData backendStep, QProcessMetaData qProcessMetaData, ProcessState processState) throws Exception { RunBackendStepInput runBackendStepInput = new RunBackendStepInput(runProcessInput.getInstance(), processState); runBackendStepInput.setProcessName(process.getName()); runBackendStepInput.setStepName(backendStep.getName()); + runBackendStepInput.setTableName(process.getTableName()); runBackendStepInput.setSession(runProcessInput.getSession()); runBackendStepInput.setCallback(runProcessInput.getCallback()); runBackendStepInput.setAsyncJobCallback(runProcessInput.getAsyncJobCallback()); diff --git a/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/DeleteAction.java b/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/DeleteAction.java index 08880eb3..7b3ed6d8 100644 --- a/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/DeleteAction.java +++ b/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/DeleteAction.java @@ -22,12 +22,21 @@ package com.kingsrook.qqq.backend.core.actions.tables; +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.interfaces.DeleteInterface; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput; import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteOutput; +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.modules.backend.QBackendModuleDispatcher; import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; /******************************************************************************* @@ -36,6 +45,10 @@ import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface; *******************************************************************************/ public class DeleteAction { + private static final Logger LOG = LogManager.getLogger(DeleteAction.class); + + + /******************************************************************************* ** *******************************************************************************/ @@ -44,10 +57,64 @@ public class DeleteAction ActionHelper.validateSession(deleteInput); QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher(); - QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(deleteInput.getBackend()); + QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(deleteInput.getBackend()); // todo pre-customization - just get to modify the request? - DeleteOutput deleteResult = qModule.getDeleteInterface().execute(deleteInput); + + if(CollectionUtils.nullSafeHasContents(deleteInput.getPrimaryKeys()) && deleteInput.getQueryFilter() != null) + { + throw (new QException("A delete request may not contain both a list of primary keys and a query filter.")); + } + + DeleteInterface deleteInterface = qModule.getDeleteInterface(); + if(deleteInput.getQueryFilter() != null && !deleteInterface.supportsQueryFilterInput()) + { + LOG.info("Querying for primary keys, for backend module " + qModule.getBackendType() + " which does not support queryFilter input for deletes"); + List primaryKeyList = getPrimaryKeysFromQueryFilter(deleteInput); + deleteInput.setPrimaryKeys(primaryKeyList); + + if(primaryKeyList.isEmpty()) + { + LOG.info("0 primaryKeys found. Returning with no-op"); + DeleteOutput deleteOutput = new DeleteOutput(); + deleteOutput.setRecordsWithErrors(new ArrayList<>()); + return (deleteOutput); + } + } + + DeleteOutput deleteResult = deleteInterface.execute(deleteInput); // todo post-customization - can do whatever w/ the result if you want + return deleteResult; } + + + + /******************************************************************************* + ** For an implementation that doesn't support a queryFilter as its input, + ** but a scenario where a query filter was passed in - run the query, to + ** get a list of primary keys. + *******************************************************************************/ + public static List getPrimaryKeysFromQueryFilter(DeleteInput deleteInput) throws QException + { + try + { + QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher(); + QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(deleteInput.getBackend()); + + QueryInput queryInput = new QueryInput(deleteInput.getInstance(), deleteInput.getSession()); + queryInput.setTableName(deleteInput.getTableName()); + queryInput.setFilter(deleteInput.getQueryFilter()); + QueryOutput queryOutput = qModule.getQueryInterface().execute(queryInput); + + return (queryOutput.getRecords().stream() + .map(r -> r.getValue(deleteInput.getTable().getPrimaryKeyField())) + .toList()); + } + catch(Exception e) + { + LOG.warn("Error getting primary keys from query filter before bulk-delete", e); + throw (new QException("Error getting keys from filter prior to delete.", e)); + } + } + } diff --git a/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricher.java b/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricher.java index 7a3ba317..a23f5255 100644 --- a/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricher.java +++ b/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricher.java @@ -22,14 +22,31 @@ package com.kingsrook.qqq.backend.core.instances; +import java.util.ArrayList; +import java.util.List; import java.util.Locale; +import java.util.stream.Collectors; import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QComponentType; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendComponentMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionInputMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionOutputMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QRecordListMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.processes.implementations.bulk.delete.BulkDeleteStoreStep; +import com.kingsrook.qqq.backend.core.processes.implementations.bulk.edit.BulkEditReceiveValuesStep; +import com.kingsrook.qqq.backend.core.processes.implementations.bulk.edit.BulkEditStoreRecordsStep; +import com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert.BulkInsertReceiveFileStep; +import com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert.BulkInsertStoreRecordsStep; +import com.kingsrook.qqq.backend.core.processes.implementations.general.LoadInitialRecordsStep; import com.kingsrook.qqq.backend.core.utils.StringUtils; @@ -48,6 +65,7 @@ public class QInstanceEnricher if(qInstance.getTables() != null) { qInstance.getTables().values().forEach(this::enrich); + defineTableBulkProcesses(qInstance); } if(qInstance.getProcesses() != null) @@ -122,7 +140,7 @@ public class QInstanceEnricher step.getInputFields().forEach(this::enrich); step.getOutputFields().forEach(this::enrich); - if (step instanceof QFrontendStepMetaData frontendStepMetaData) + if(step instanceof QFrontendStepMetaData frontendStepMetaData) { if(frontendStepMetaData.getFormFields() != null) { @@ -167,4 +185,229 @@ public class QInstanceEnricher return (name.substring(0, 1).toUpperCase(Locale.ROOT) + name.substring(1).replaceAll("([A-Z])", " $1")); } + + + /******************************************************************************* + ** Add bulk insert/edit/delete processes to all tables (unless the meta data + ** already had these processes defined (e.g., the user defined custom ones) + *******************************************************************************/ + private void defineTableBulkProcesses(QInstance qInstance) + { + for(QTableMetaData table : qInstance.getTables().values()) + { + if(table.getFields() == null) + { + ///////////////////////////////////////////////////////////////// + // these processes can't be defined if there aren't any fields // + ///////////////////////////////////////////////////////////////// + continue; + } + + // todo - add idea of 'supportsBulkX' + String bulkInsertProcessName = table.getName() + ".bulkInsert"; + if(qInstance.getProcess(bulkInsertProcessName) == null) + { + defineTableBulkInsert(qInstance, table, bulkInsertProcessName); + } + + String bulkEditProcessName = table.getName() + ".bulkEdit"; + if(qInstance.getProcess(bulkEditProcessName) == null) + { + defineTableBulkEdit(qInstance, table, bulkEditProcessName); + } + + String bulkDeleteProcessName = table.getName() + ".bulkDelete"; + if(qInstance.getProcess(bulkDeleteProcessName) == null) + { + defineTableBulkDelete(qInstance, table, bulkDeleteProcessName); + } + + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private void defineTableBulkInsert(QInstance qInstance, QTableMetaData table, String processName) + { + List editableFields = table.getFields().values().stream() + .filter(QFieldMetaData::getIsEditable) + .toList(); + + String fieldsForHelpText = editableFields.stream() + .map(QFieldMetaData::getLabel) + .collect(Collectors.joining(", ")); + + QFrontendStepMetaData uploadScreen = new QFrontendStepMetaData() + .withName("upload") + .withLabel("Upload File") + .withFormField(new QFieldMetaData("theFile", QFieldType.BLOB).withIsRequired(true)) + .withComponent(new QFrontendComponentMetaData() + .withType(QComponentType.HELP_TEXT) + .withValue("text", "Upload a CSV or XLSX file with the following columns: " + fieldsForHelpText)); + + QBackendStepMetaData receiveFileStep = new QBackendStepMetaData() + .withName("receiveFile") + .withCode(new QCodeReference(BulkInsertReceiveFileStep.class)) + .withInputData(new QFunctionInputMetaData() + // todo - our upload file as a field? problem is, its type... + .withFieldList(List.of())) + .withOutputMetaData(new QFunctionOutputMetaData() + .withFieldList(List.of(new QFieldMetaData("noOfFileRows", QFieldType.INTEGER)))); + + QFrontendStepMetaData reviewScreen = new QFrontendStepMetaData() + .withName("review") + .withRecordListFields(editableFields) + .withComponent(new QFrontendComponentMetaData() + .withType(QComponentType.HELP_TEXT) + .withValue("text", "The records below were parsed from your file, and will be inserted if you click Submit.")) + .withViewField(new QFieldMetaData("noOfFileRows", QFieldType.INTEGER).withLabel("# of file rows")); + + QBackendStepMetaData storeStep = new QBackendStepMetaData() + .withName("storeRecords") + .withCode(new QCodeReference(BulkInsertStoreRecordsStep.class)) + .withInputData(new QFunctionInputMetaData() + // todo - our upload file as a field? problem is, its type... + .withFieldList(List.of())) + .withOutputMetaData(new QFunctionOutputMetaData() + .withFieldList(List.of(new QFieldMetaData("noOfFileRows", QFieldType.INTEGER)))); + + QFrontendStepMetaData resultsScreen = new QFrontendStepMetaData() + .withName("results") + .withRecordListFields(new ArrayList<>(table.getFields().values())) + .withComponent(new QFrontendComponentMetaData() + .withType(QComponentType.HELP_TEXT) + .withValue("text", "The records below have been inserted.")) + .withViewField(new QFieldMetaData("noOfFileRows", QFieldType.INTEGER).withLabel("# of file rows")); + + qInstance.addProcess( + new QProcessMetaData() + .withName(processName) + .withLabel(table.getLabel() + " Bulk Insert") + .withTableName(table.getName()) + .withIsHidden(true) + .withStepList(List.of( + uploadScreen, + receiveFileStep, + reviewScreen, + storeStep, + resultsScreen + ))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private void defineTableBulkEdit(QInstance qInstance, QTableMetaData table, String processName) + { + List editableFields = table.getFields().values().stream() + .filter(QFieldMetaData::getIsEditable) + .toList(); + + QFrontendStepMetaData editScreen = new QFrontendStepMetaData() + .withName("edit") + .withLabel("Edit Values") + .withFormFields(editableFields) + .withComponent(new QFrontendComponentMetaData() + .withType(QComponentType.HELP_TEXT) + .withValue("text", """ + Flip the switches next to the fields that you want to edit. + The values you supply here will be updated in all of the records you are bulk editing. + You can clear out the value in a field by flipping the switch on for that field and leaving the input field blank. + Fields whose switches are off will not be updated.""")) + .withComponent(new QFrontendComponentMetaData() + .withType(QComponentType.BULK_EDIT_FORM) + ); + + QBackendStepMetaData receiveValuesStep = new QBackendStepMetaData() + .withName("receiveValues") + .withCode(new QCodeReference(BulkEditReceiveValuesStep.class)) + .withInputData(new QFunctionInputMetaData() + .withRecordListMetaData(new QRecordListMetaData().withTableName(table.getName())) + .withField(new QFieldMetaData(BulkEditReceiveValuesStep.FIELD_ENABLED_FIELDS, QFieldType.STRING)) + .withFields(editableFields)); + + QFrontendStepMetaData reviewScreen = new QFrontendStepMetaData() + .withName("review") + .withRecordListFields(editableFields) + .withViewField(new QFieldMetaData(BulkEditReceiveValuesStep.FIELD_VALUES_BEING_UPDATED, QFieldType.STRING)) + .withComponent(new QFrontendComponentMetaData() + .withType(QComponentType.HELP_TEXT) + .withValue("text", "The records below will be updated if you click Submit.")); + + QBackendStepMetaData storeStep = new QBackendStepMetaData() + .withName("storeRecords") + .withCode(new QCodeReference(BulkEditStoreRecordsStep.class)) + .withInputData(new QFunctionInputMetaData() + // todo - our upload file as a field? problem is, its type... + .withFieldList(List.of())) + .withOutputMetaData(new QFunctionOutputMetaData() + .withFieldList(List.of(new QFieldMetaData("noOfFileRows", QFieldType.INTEGER)))); + + QFrontendStepMetaData resultsScreen = new QFrontendStepMetaData() + .withName("results") + .withRecordListFields(new ArrayList<>(table.getFields().values())) + .withComponent(new QFrontendComponentMetaData() + .withType(QComponentType.HELP_TEXT) + .withValue("text", "The records below have been updated.")); + + qInstance.addProcess( + new QProcessMetaData() + .withName(processName) + .withLabel(table.getLabel() + " Bulk Edit") + .withTableName(table.getName()) + .withIsHidden(true) + .withStepList(List.of( + LoadInitialRecordsStep.defineMetaData(table.getName()), + editScreen, + receiveValuesStep, + reviewScreen, + storeStep, + resultsScreen + ))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private void defineTableBulkDelete(QInstance qInstance, QTableMetaData table, String processName) + { + QFrontendStepMetaData reviewScreen = new QFrontendStepMetaData() + .withName("review") + .withRecordListFields(new ArrayList<>(table.getFields().values())) + .withComponent(new QFrontendComponentMetaData() + .withType(QComponentType.HELP_TEXT) + .withValue("text", "The records below will be deleted if you click Submit.")); + + QBackendStepMetaData storeStep = new QBackendStepMetaData() + .withName("delete") + .withCode(new QCodeReference(BulkDeleteStoreStep.class)); + + QFrontendStepMetaData resultsScreen = new QFrontendStepMetaData() + .withName("results") + .withRecordListFields(new ArrayList<>(table.getFields().values())) + .withComponent(new QFrontendComponentMetaData() + .withType(QComponentType.HELP_TEXT) + .withValue("text", "The records below have been deleted.")); + + qInstance.addProcess( + new QProcessMetaData() + .withName(processName) + .withLabel(table.getLabel() + " Bulk Delete") + .withTableName(table.getName()) + .withIsHidden(true) + .withStepList(List.of( + LoadInitialRecordsStep.defineMetaData(table.getName()), + reviewScreen, + storeStep, + resultsScreen + ))); + } + } diff --git a/src/main/java/com/kingsrook/qqq/backend/core/model/actions/AbstractActionInput.java b/src/main/java/com/kingsrook/qqq/backend/core/model/actions/AbstractActionInput.java index c8cc23b7..91c3e6f4 100644 --- a/src/main/java/com/kingsrook/qqq/backend/core/model/actions/AbstractActionInput.java +++ b/src/main/java/com/kingsrook/qqq/backend/core/model/actions/AbstractActionInput.java @@ -22,6 +22,9 @@ package com.kingsrook.qqq.backend.core.model.actions; +import java.util.UUID; +import com.kingsrook.qqq.backend.core.actions.async.AsyncJobCallback; +import com.kingsrook.qqq.backend.core.actions.async.AsyncJobStatus; import com.kingsrook.qqq.backend.core.exceptions.QInstanceValidationException; import com.kingsrook.qqq.backend.core.instances.QInstanceValidator; import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationMetaData; @@ -40,7 +43,9 @@ public abstract class AbstractActionInput private static final Logger LOG = LogManager.getLogger(AbstractActionInput.class); protected QInstance instance; - protected QSession session; + protected QSession session; + + private AsyncJobCallback asyncJobCallback; @@ -59,7 +64,16 @@ public abstract class AbstractActionInput public AbstractActionInput(QInstance instance) { this.instance = instance; + validateInstance(instance); + } + + + /******************************************************************************* + ** performance instance validation (if not previously done). + *******************************************************************************/ + private void validateInstance(QInstance instance) + { //////////////////////////////////////////////////////////// // if this instance hasn't been validated yet, do so now // // noting that this will also enrich any missing metaData // @@ -107,6 +121,7 @@ public abstract class AbstractActionInput *******************************************************************************/ public void setInstance(QInstance instance) { + validateInstance(instance); this.instance = instance; } @@ -131,4 +146,33 @@ public abstract class AbstractActionInput { this.session = session; } + + + + /******************************************************************************* + ** Getter for asyncJobCallback + ** + *******************************************************************************/ + public AsyncJobCallback getAsyncJobCallback() + { + if(asyncJobCallback == null) + { + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // don't return null here (too easy to NPE). instead, if someone wants one of these, create one and give it to them. // + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + asyncJobCallback = new AsyncJobCallback(UUID.randomUUID(), new AsyncJobStatus()); + } + return asyncJobCallback; + } + + + + /******************************************************************************* + ** Setter for asyncJobCallback + ** + *******************************************************************************/ + public void setAsyncJobCallback(AsyncJobCallback asyncJobCallback) + { + this.asyncJobCallback = asyncJobCallback; + } } diff --git a/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/QUploadedFile.java b/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/QUploadedFile.java new file mode 100644 index 00000000..7fdf8347 --- /dev/null +++ b/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/QUploadedFile.java @@ -0,0 +1,79 @@ +/* + * 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.actions.processes; + + +import java.io.Serializable; + + +/******************************************************************************* + ** Model a file that a user uploaded (or otherwise submitted to the qqq backend). + *******************************************************************************/ +public class QUploadedFile implements Serializable +{ + private String filename; + private byte[] bytes; + + + + /******************************************************************************* + ** Getter for filename + ** + *******************************************************************************/ + public String getFilename() + { + return filename; + } + + + + /******************************************************************************* + ** Setter for filename + ** + *******************************************************************************/ + public void setFilename(String filename) + { + this.filename = filename; + } + + + + /******************************************************************************* + ** Getter for bytes + ** + *******************************************************************************/ + public byte[] getBytes() + { + return bytes; + } + + + + /******************************************************************************* + ** Setter for bytes + ** + *******************************************************************************/ + public void setBytes(byte[] bytes) + { + this.bytes = bytes; + } +} diff --git a/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunBackendStepInput.java b/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunBackendStepInput.java index f8e2febf..520f0277 100644 --- a/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunBackendStepInput.java +++ b/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunBackendStepInput.java @@ -33,6 +33,7 @@ import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput; import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.utils.ValueUtils; @@ -44,6 +45,7 @@ public class RunBackendStepInput extends AbstractActionInput { private ProcessState processState; private String processName; + private String tableName; private String stepName; private QProcessCallback callback; private AsyncJobCallback asyncJobCallback; @@ -126,6 +128,55 @@ public class RunBackendStepInput extends AbstractActionInput + /******************************************************************************* + ** Getter for tableName + ** + *******************************************************************************/ + public String getTableName() + { + return tableName; + } + + + + /******************************************************************************* + ** Setter for tableName + ** + *******************************************************************************/ + public void setTableName(String tableName) + { + this.tableName = tableName; + } + + + + /******************************************************************************* + ** Fluent setter for tableName + ** + *******************************************************************************/ + public RunBackendStepInput withTableName(String tableName) + { + this.tableName = tableName; + return (this); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public QTableMetaData getTable() + { + if(tableName == null) + { + return (null); + } + + return (instance.getTable(tableName)); + } + + + /******************************************************************************* ** Getter for functionName ** @@ -334,7 +385,7 @@ public class RunBackendStepInput extends AbstractActionInput *******************************************************************************/ public AsyncJobCallback getAsyncJobCallback() { - if (asyncJobCallback == null) + if(asyncJobCallback == null) { ///////////////////////////////////////////////////////////////////////// // avoid NPE in case we didn't have one of these! create a new one... // diff --git a/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunBackendStepOutput.java b/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunBackendStepOutput.java index 8746a9ca..c11df1fb 100644 --- a/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunBackendStepOutput.java +++ b/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunBackendStepOutput.java @@ -27,6 +27,7 @@ import java.util.List; import java.util.Map; import com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput; import com.kingsrook.qqq.backend.core.model.data.QRecord; +import com.kingsrook.qqq.backend.core.utils.ValueUtils; /******************************************************************************* @@ -183,4 +184,38 @@ public class RunBackendStepOutput extends AbstractActionOutput { return exception; } + + + + /******************************************************************************* + ** Getter for a single field's value + ** + *******************************************************************************/ + public Serializable getValue(String fieldName) + { + return (processState.getValues().get(fieldName)); + } + + + + /******************************************************************************* + ** Getter for a single field's value + ** + *******************************************************************************/ + public String getValueString(String fieldName) + { + return ((String) getValue(fieldName)); + } + + + + /******************************************************************************* + ** Getter for a single field's value + ** + *******************************************************************************/ + public Integer getValueInteger(String fieldName) + { + return (ValueUtils.getValueAsInteger(getValue(fieldName))); + } + } diff --git a/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/delete/DeleteInput.java b/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/delete/DeleteInput.java index ab18830d..40da4720 100644 --- a/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/delete/DeleteInput.java +++ b/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/delete/DeleteInput.java @@ -25,6 +25,7 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.delete; import java.io.Serializable; import java.util.List; 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.metadata.QInstance; @@ -35,6 +36,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.QInstance; public class DeleteInput extends AbstractTableActionInput { private List primaryKeys; + private QQueryFilter queryFilter; @@ -88,4 +90,37 @@ public class DeleteInput extends AbstractTableActionInput this.primaryKeys = primaryKeys; return (this); } + + + /******************************************************************************* + ** Getter for queryFilter + ** + *******************************************************************************/ + public QQueryFilter getQueryFilter() + { + return queryFilter; + } + + + + /******************************************************************************* + ** Setter for queryFilter + ** + *******************************************************************************/ + public void setQueryFilter(QQueryFilter queryFilter) + { + this.queryFilter = queryFilter; + } + + + /******************************************************************************* + ** Fluent setter for queryFilter + ** + *******************************************************************************/ + public DeleteInput withQueryFilter(QQueryFilter queryFilter) + { + this.queryFilter = queryFilter; + return this; + } + } diff --git a/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/delete/DeleteOutput.java b/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/delete/DeleteOutput.java index 0c32ceac..9516272f 100644 --- a/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/delete/DeleteOutput.java +++ b/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/delete/DeleteOutput.java @@ -22,6 +22,8 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.delete; +import java.io.Serializable; +import java.util.ArrayList; import java.util.List; import com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput; import com.kingsrook.qqq.backend.core.model.data.QRecord; @@ -31,18 +33,31 @@ import com.kingsrook.qqq.backend.core.model.data.QRecord; * Output for a delete action * *******************************************************************************/ -public class DeleteOutput extends AbstractActionOutput +public class DeleteOutput extends AbstractActionOutput implements Serializable { - private List records; + private int deletedRecordCount = 0; + private List recordsWithErrors; /******************************************************************************* + ** Getter for deletedRecordCount ** *******************************************************************************/ - public List getRecords() + public int getDeletedRecordCount() { - return records; + return deletedRecordCount; + } + + + + /******************************************************************************* + ** Setter for deletedRecordCount + ** + *******************************************************************************/ + public void setDeletedRecordCount(int deletedRecordCount) + { + this.deletedRecordCount = deletedRecordCount; } @@ -50,8 +65,40 @@ public class DeleteOutput extends AbstractActionOutput /******************************************************************************* ** *******************************************************************************/ - public void setRecords(List records) + public List getRecordsWithErrors() { - this.records = records; + return recordsWithErrors; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public void setRecordsWithErrors(List recordsWithErrors) + { + this.recordsWithErrors = recordsWithErrors; + } + + + /******************************************************************************* + ** + *******************************************************************************/ + public void addRecordWithError(QRecord recordWithError) + { + if(this.recordsWithErrors == null) + { + this.recordsWithErrors = new ArrayList<>(); + } + this.recordsWithErrors.add(recordWithError); + } + + + /******************************************************************************* + ** + *******************************************************************************/ + public void addToDeletedRecordCount(int i) + { + deletedRecordCount += i; } } diff --git a/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QFilterCriteria.java b/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QFilterCriteria.java index 0aa20860..d4c5959b 100644 --- a/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QFilterCriteria.java +++ b/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QFilterCriteria.java @@ -30,7 +30,7 @@ import java.util.List; * A single criteria Component of a Query * *******************************************************************************/ -public class QFilterCriteria +public class QFilterCriteria implements Serializable { private String fieldName; private QCriteriaOperator operator; diff --git a/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QFilterOrderBy.java b/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QFilterOrderBy.java index 74d03266..1c6933af 100644 --- a/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QFilterOrderBy.java +++ b/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QFilterOrderBy.java @@ -22,11 +22,14 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.query; +import java.io.Serializable; + + /******************************************************************************* ** Bean representing an element of a query order-by clause. ** *******************************************************************************/ -public class QFilterOrderBy +public class QFilterOrderBy implements Serializable { private String fieldName; private boolean isAscending = true; diff --git a/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QQueryFilter.java b/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QQueryFilter.java index 41a794dc..42321380 100644 --- a/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QQueryFilter.java +++ b/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QQueryFilter.java @@ -22,6 +22,7 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.query; +import java.io.Serializable; import java.util.ArrayList; import java.util.List; @@ -30,7 +31,7 @@ import java.util.List; * Full "filter" for a query - a list of criteria and order-bys * *******************************************************************************/ -public class QQueryFilter +public class QQueryFilter implements Serializable { private List criteria = new ArrayList<>(); private List orderBys = new ArrayList<>(); diff --git a/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QueryInput.java b/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QueryInput.java index febe1e9e..daaa5ab7 100644 --- a/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QueryInput.java +++ b/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QueryInput.java @@ -25,6 +25,7 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.query; 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.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.session.QSession; /******************************************************************************* @@ -60,6 +61,17 @@ public class QueryInput extends AbstractTableActionInput + /******************************************************************************* + ** + *******************************************************************************/ + public QueryInput(QInstance instance, QSession session) + { + super(instance); + setSession(session); + } + + + /******************************************************************************* ** Getter for filter ** diff --git a/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/update/UpdateInput.java b/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/update/UpdateInput.java index 5d2f5dcd..fca9ee57 100644 --- a/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/update/UpdateInput.java +++ b/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/update/UpdateInput.java @@ -36,6 +36,14 @@ public class UpdateInput extends AbstractTableActionInput { private List records; + //////////////////////////////////////////////////////////////////////////////////////////// + // allow a caller to specify that they KNOW this optimization (e.g., in SQL) can be made. // + // If you set this to true, but it isn't, then you may not get an accurate update. // + // If you set this to false, but it isn't, then you may not get the best performance. // + // Just leave it null if you don't know what you're dong. // + //////////////////////////////////////////////////////////////////////////////////////////// + private Boolean areAllValuesBeingUpdatedTheSame = null; + /******************************************************************************* @@ -76,4 +84,27 @@ public class UpdateInput extends AbstractTableActionInput { this.records = records; } + + + + /******************************************************************************* + ** Getter for areAllValuesBeingUpdatedTheSame + ** + *******************************************************************************/ + public Boolean getAreAllValuesBeingUpdatedTheSame() + { + return areAllValuesBeingUpdatedTheSame; + } + + + + /******************************************************************************* + ** Setter for areAllValuesBeingUpdatedTheSame + ** + *******************************************************************************/ + public void setAreAllValuesBeingUpdatedTheSame(Boolean areAllValuesBeingUpdatedTheSame) + { + this.areAllValuesBeingUpdatedTheSame = areAllValuesBeingUpdatedTheSame; + } + } diff --git a/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecord.java b/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecord.java index 6151c672..8ccd91a6 100644 --- a/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecord.java +++ b/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecord.java @@ -25,9 +25,12 @@ package com.kingsrook.qqq.backend.core.model.data; import java.io.Serializable; import java.math.BigDecimal; import java.time.LocalDate; +import java.util.ArrayList; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.utils.ValueUtils; @@ -54,7 +57,7 @@ public class QRecord implements Serializable private Map values = new LinkedHashMap<>(); private Map displayValues = new LinkedHashMap<>(); private Map backendDetails = new LinkedHashMap<>(); - // todo private List errors = new ArrayList<>(); + private List errors = new ArrayList<>(); @@ -66,6 +69,16 @@ public class QRecord implements Serializable } + /******************************************************************************* + ** + *******************************************************************************/ + public QRecord(QTableMetaData tableMetaData, Serializable primaryKeyValue) + { + setTableName(tableMetaData.getName()); + setValue(tableMetaData.getPrimaryKeyField(), primaryKeyValue); + } + + /******************************************************************************* ** Copy constructor. @@ -77,7 +90,7 @@ public class QRecord implements Serializable this.values = record.values; this.displayValues = record.displayValues; this.backendDetails = record.backendDetails; - // todo! this.errors = record.errors; + this.errors = record.errors; } @@ -344,6 +357,49 @@ public class QRecord implements Serializable } + /******************************************************************************* + ** Getter for errors + ** + *******************************************************************************/ + public List getErrors() + { + return (errors); + } + + + + /******************************************************************************* + ** Setter for errors + ** + *******************************************************************************/ + public void setErrors(List errors) + { + this.errors = errors; + } + + + + /******************************************************************************* + ** Add one error to this record + ** + *******************************************************************************/ + public void addError(String error) + { + this.errors.add(error); + } + + + + /******************************************************************************* + ** Fluently Add one error to this record + ** + *******************************************************************************/ + public QRecord withError(String error) + { + addError(error); + return (this); + } + /******************************************************************************* ** Convert this record to an QRecordEntity @@ -352,4 +408,5 @@ public class QRecord implements Serializable { return (QRecordEntity.fromQRecord(c, this)); } + } diff --git a/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/code/QCodeReference.java b/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/code/QCodeReference.java index 6ecdbfb8..c1093ca9 100644 --- a/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/code/QCodeReference.java +++ b/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/code/QCodeReference.java @@ -22,17 +22,74 @@ package com.kingsrook.qqq.backend.core.model.metadata.code; +import com.kingsrook.qqq.backend.core.actions.processes.BackendStep; + + /******************************************************************************* - ** + ** Pointer to code to be ran by the qqq framework, e.g., for custom behavior - + ** maybe process steps, maybe customization to a table, etc. *******************************************************************************/ public class QCodeReference { - private String name; - private QCodeType codeType; + private String name; + private QCodeType codeType; private QCodeUsage codeUsage; + /******************************************************************************* + ** Default empty constructor + *******************************************************************************/ + public QCodeReference() + { + } + + + + /******************************************************************************* + ** Constructor that takes all args + *******************************************************************************/ + public QCodeReference(String name, QCodeType codeType, QCodeUsage codeUsage) + { + this.name = name; + this.codeType = codeType; + this.codeUsage = codeUsage; + } + + + + /******************************************************************************* + ** Constructor that just takes a java class, and infers the other fields. + *******************************************************************************/ + public QCodeReference(Class javaClass) + { + this.name = javaClass.getName(); + this.codeType = QCodeType.JAVA; + + if(BackendStep.class.isAssignableFrom(javaClass)) + { + this.codeUsage = QCodeUsage.BACKEND_STEP; + } + else + { + throw (new IllegalStateException("Unable to infer code usage type for class: " + javaClass.getName())); + } + } + + + + /******************************************************************************* + ** Constructor that just takes a java class and code usage. + *******************************************************************************/ + public QCodeReference(Class javaClass, QCodeUsage codeUsage) + { + this.name = javaClass.getName(); + this.codeType = QCodeType.JAVA; + this.codeUsage = codeUsage; + } + + + /******************************************************************************* ** Getter for name ** diff --git a/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QFieldMetaData.java b/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QFieldMetaData.java index 6046da09..c5fb9e72 100644 --- a/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QFieldMetaData.java +++ b/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QFieldMetaData.java @@ -40,6 +40,12 @@ public class QFieldMetaData private String backendName; private QFieldType type; private boolean isRequired = false; + private boolean isEditable = true; + + /////////////////////////////////////////////////////////////////////////////////// + // if we need "only edit on insert" or "only edit on update" in the future, // + // propose doing that in a secondary field, e.g., "onlyEditableOn=insert|update" // + /////////////////////////////////////////////////////////////////////////////////// private Serializable defaultValue; private String possibleValueSourceName; @@ -315,4 +321,37 @@ public class QFieldMetaData return (this); } + + + /******************************************************************************* + ** Getter for isEditable + ** + *******************************************************************************/ + public boolean getIsEditable() + { + return isEditable; + } + + + + /******************************************************************************* + ** Setter for isEditable + ** + *******************************************************************************/ + public void setIsEditable(boolean isEditable) + { + this.isEditable = isEditable; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public QFieldMetaData withIsEditable(boolean isEditable) + { + this.isEditable = isEditable; + return (this); + } + } diff --git a/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QFieldType.java b/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QFieldType.java index 16e94591..3f4f47fa 100644 --- a/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QFieldType.java +++ b/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QFieldType.java @@ -40,7 +40,8 @@ public enum QFieldType DATE_TIME, TEXT, HTML, - PASSWORD; + PASSWORD, + BLOB; diff --git a/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendFieldMetaData.java b/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendFieldMetaData.java index 819a217a..f57a05ff 100644 --- a/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendFieldMetaData.java +++ b/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendFieldMetaData.java @@ -40,6 +40,7 @@ public class QFrontendFieldMetaData private String label; private QFieldType type; private boolean isRequired; + private boolean isEditable; ////////////////////////////////////////////////////////////////////////////////// // do not add setters. take values from the source-object in the constructor!! // @@ -48,7 +49,7 @@ public class QFrontendFieldMetaData /******************************************************************************* - ** + ** Constructor *******************************************************************************/ public QFrontendFieldMetaData(QFieldMetaData fieldMetaData) { @@ -56,6 +57,7 @@ public class QFrontendFieldMetaData this.label = fieldMetaData.getLabel(); this.type = fieldMetaData.getType(); this.isRequired = fieldMetaData.getIsRequired(); + this.isEditable = fieldMetaData.getIsEditable(); } @@ -101,4 +103,16 @@ public class QFrontendFieldMetaData { return isRequired; } + + + + /******************************************************************************* + ** Getter for isEditable + ** + *******************************************************************************/ + public boolean getIsEditable() + { + return isEditable; + } + } diff --git a/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QComponentType.java b/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QComponentType.java new file mode 100644 index 00000000..5c0ba61e --- /dev/null +++ b/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QComponentType.java @@ -0,0 +1,35 @@ +/* + * 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.metadata.processes; + + +/******************************************************************************* + ** Types of UI Components that can be specified in frontend process steps. + *******************************************************************************/ +public enum QComponentType +{ + HELP_TEXT, + BULK_EDIT_FORM; + /////////////////////////////////////////////////////////////////////////// + // keep these values in sync with QComponentType.ts in qqq-frontend-core // + /////////////////////////////////////////////////////////////////////////// +} diff --git a/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QFrontendComponentMetaData.java b/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QFrontendComponentMetaData.java new file mode 100644 index 00000000..aa5a1f6e --- /dev/null +++ b/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QFrontendComponentMetaData.java @@ -0,0 +1,123 @@ +/* + * 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.metadata.processes; + + +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; + + +/******************************************************************************* + ** Definition of a UI component in a frontend process steps. + *******************************************************************************/ +public class QFrontendComponentMetaData +{ + private QComponentType type; + + private Map values; + + + + /******************************************************************************* + ** Getter for type + ** + *******************************************************************************/ + public QComponentType getType() + { + return type; + } + + + + /******************************************************************************* + ** Setter for type + ** + *******************************************************************************/ + public void setType(QComponentType type) + { + this.type = type; + } + + + + /******************************************************************************* + ** Fluent setter for type + ** + *******************************************************************************/ + public QFrontendComponentMetaData withType(QComponentType type) + { + this.type = type; + return (this); + } + + + + /******************************************************************************* + ** Getter for values + ** + *******************************************************************************/ + public Map getValues() + { + return values; + } + + + + /******************************************************************************* + ** Setter for values + ** + *******************************************************************************/ + public void setValues(Map values) + { + this.values = values; + } + + + + /******************************************************************************* + ** Fluent setter for values + ** + *******************************************************************************/ + public QFrontendComponentMetaData withValues(Map values) + { + this.values = values; + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for values + ** + *******************************************************************************/ + public QFrontendComponentMetaData withValue(String key, Serializable value) + { + if(values == null) + { + values = new HashMap<>(); + } + values.put(key, value); + return (this); + } + +} diff --git a/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QFrontendStepMetaData.java b/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QFrontendStepMetaData.java index 2c9384d8..55a9424d 100644 --- a/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QFrontendStepMetaData.java +++ b/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QFrontendStepMetaData.java @@ -34,9 +34,10 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; *******************************************************************************/ public class QFrontendStepMetaData extends QStepMetaData { - private List formFields; - private List viewFields; - private List recordListFields; + private List components; + private List formFields; + private List viewFields; + private List recordListFields; @@ -50,6 +51,56 @@ public class QFrontendStepMetaData extends QStepMetaData + /******************************************************************************* + ** Getter for components + ** + *******************************************************************************/ + public List getComponents() + { + return components; + } + + + + /******************************************************************************* + ** Setter for components + ** + *******************************************************************************/ + public void setComponents(List components) + { + this.components = components; + } + + + + /******************************************************************************* + ** Fluent setter for adding 1 component + ** + *******************************************************************************/ + public QFrontendStepMetaData withComponent(QFrontendComponentMetaData component) + { + if(this.components == null) + { + this.components = new ArrayList<>(); + } + this.components.add(component); + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for components + ** + *******************************************************************************/ + public QFrontendStepMetaData withComponents(List components) + { + this.components = components; + return (this); + } + + + /******************************************************************************* ** Getter for formFields ** diff --git a/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QFunctionInputMetaData.java b/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QFunctionInputMetaData.java index 9237d50f..e706a1e2 100644 --- a/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QFunctionInputMetaData.java +++ b/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QFunctionInputMetaData.java @@ -124,7 +124,23 @@ public class QFunctionInputMetaData /******************************************************************************* - ** Setter for fieldList + ** Fluently ADD a list of fields to this object's existing list + ** + *******************************************************************************/ + public QFunctionInputMetaData withFields(List fieldList) + { + if(this.fieldList == null) + { + this.fieldList = new ArrayList<>(); + } + this.fieldList.addAll(fieldList); + return (this); + } + + + + /******************************************************************************* + ** Fluent Setter for fieldList - e.g., will overwrite any previously set fields!! ** *******************************************************************************/ public QFunctionInputMetaData withFieldList(List fieldList) @@ -136,10 +152,10 @@ public class QFunctionInputMetaData /******************************************************************************* - ** Setter for fieldList + ** Fluently add a field to the list ** *******************************************************************************/ - public QFunctionInputMetaData addField(QFieldMetaData field) + public QFunctionInputMetaData withField(QFieldMetaData field) { if(this.fieldList == null) { @@ -149,4 +165,17 @@ public class QFunctionInputMetaData return (this); } + + + /******************************************************************************* + ** Use withField instead, please. + ** + ** @deprecated + *******************************************************************************/ + @Deprecated + public QFunctionInputMetaData addField(QFieldMetaData field) + { + return (withField(field)); + } + } diff --git a/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QRecordListMetaData.java b/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QRecordListMetaData.java index cf3bab30..b73bc22b 100644 --- a/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QRecordListMetaData.java +++ b/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QRecordListMetaData.java @@ -32,7 +32,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; *******************************************************************************/ public class QRecordListMetaData { - private String tableName; + private String tableName; private Map fields; @@ -116,7 +116,7 @@ public class QRecordListMetaData /******************************************************************************* ** *******************************************************************************/ - public QRecordListMetaData addField(QFieldMetaData field) + public QRecordListMetaData withField(QFieldMetaData field) { if(this.fields == null) { @@ -126,4 +126,15 @@ public class QRecordListMetaData return (this); } + + + /******************************************************************************* + ** + *******************************************************************************/ + @Deprecated + public QRecordListMetaData addField(QFieldMetaData field) + { + return (withField(field)); + } + } diff --git a/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/mock/MockDeleteAction.java b/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/mock/MockDeleteAction.java index 99ffaa63..097b4896 100644 --- a/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/mock/MockDeleteAction.java +++ b/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/mock/MockDeleteAction.java @@ -26,7 +26,6 @@ import com.kingsrook.qqq.backend.core.actions.interfaces.DeleteInterface; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput; import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteOutput; -import com.kingsrook.qqq.backend.core.model.data.QRecord; /******************************************************************************* @@ -45,11 +44,7 @@ public class MockDeleteAction implements DeleteInterface { DeleteOutput rs = new DeleteOutput(); - rs.setRecords(deleteInput.getPrimaryKeys().stream().map(primaryKey -> - new QRecord() - .withTableName(deleteInput.getTableName()) - .withValue("id", primaryKey)) - .toList()); + rs.setDeletedRecordCount(deleteInput.getPrimaryKeys().size()); return rs; } diff --git a/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/mock/MockInsertAction.java b/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/mock/MockInsertAction.java index 043a2105..1522799c 100644 --- a/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/mock/MockInsertAction.java +++ b/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/mock/MockInsertAction.java @@ -26,6 +26,7 @@ import com.kingsrook.qqq.backend.core.actions.interfaces.InsertInterface; import com.kingsrook.qqq.backend.core.exceptions.QException; 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.data.QRecord; /******************************************************************************* @@ -46,6 +47,16 @@ public class MockInsertAction implements InsertInterface rs.setRecords(insertInput.getRecords()); + String primaryKeyField = insertInput.getTable().getPrimaryKeyField(); + int i = 1; + for(QRecord record : rs.getRecords()) + { + if(record.getValue(primaryKeyField) == null) + { + record.setValue(primaryKeyField, i++); + } + } + return rs; } catch(Exception e) diff --git a/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/bulk/delete/BulkDeleteStoreStep.java b/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/bulk/delete/BulkDeleteStoreStep.java new file mode 100644 index 00000000..8d9c2a66 --- /dev/null +++ b/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/bulk/delete/BulkDeleteStoreStep.java @@ -0,0 +1,91 @@ +/* + * 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.processes.implementations.bulk.delete; + + +import java.io.IOException; +import java.io.Serializable; +import java.util.List; +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.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.actions.tables.delete.DeleteOutput; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; +import com.kingsrook.qqq.backend.core.utils.JsonUtils; +import com.kingsrook.qqq.backend.core.utils.StringUtils; + + +/******************************************************************************* + ** Backend step to do a bulk delete. + *******************************************************************************/ +public class BulkDeleteStoreStep implements BackendStep +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException + { + runBackendStepInput.getAsyncJobCallback().updateStatus("Deleting records in database..."); + runBackendStepInput.getAsyncJobCallback().clearCurrentAndTotal(); + + DeleteInput deleteInput = new DeleteInput(runBackendStepInput.getInstance()); + deleteInput.setSession(runBackendStepInput.getSession()); + deleteInput.setAsyncJobCallback(runBackendStepInput.getAsyncJobCallback()); + deleteInput.setTableName(runBackendStepInput.getTableName()); + + String queryFilterJSON = runBackendStepInput.getValueString("queryFilterJSON"); + if(StringUtils.hasContent(queryFilterJSON)) + { + try + { + deleteInput.setQueryFilter(JsonUtils.toObject(queryFilterJSON, QQueryFilter.class)); + } + catch(IOException e) + { + throw (new QException("Error loading record query filter from process", e)); + } + } + else if(CollectionUtils.nullSafeHasContents(runBackendStepInput.getRecords())) + { + String primaryKeyField = runBackendStepInput.getTable().getPrimaryKeyField(); + List primaryKeyList = runBackendStepInput.getRecords().stream() + .map(r -> r.getValue(primaryKeyField)) + .toList(); + deleteInput.setPrimaryKeys(primaryKeyList); + } + + DeleteAction deleteAction = new DeleteAction(); + DeleteOutput deleteOutput = deleteAction.execute(deleteInput); + + // todo - something with the output!! + deleteOutput.getRecordsWithErrors(); + + runBackendStepOutput.setRecords(runBackendStepInput.getRecords()); + } + +} diff --git a/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/bulk/edit/BulkEditReceiveValuesStep.java b/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/bulk/edit/BulkEditReceiveValuesStep.java new file mode 100644 index 00000000..bc8a896d --- /dev/null +++ b/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/bulk/edit/BulkEditReceiveValuesStep.java @@ -0,0 +1,94 @@ +/* + * 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.processes.implementations.bulk.edit; + + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import com.kingsrook.qqq.backend.core.actions.processes.BackendStep; +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.data.QRecord; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.utils.StringUtils; +import com.kingsrook.qqq.backend.core.utils.ValueUtils; + + +/******************************************************************************* + ** Backend step to receive values for a bulk edit. + *******************************************************************************/ +public class BulkEditReceiveValuesStep implements BackendStep +{ + public static final String FIELD_ENABLED_FIELDS = "bulkEditEnabledFields"; + public static final String FIELD_VALUES_BEING_UPDATED = "valuesBeingUpdated"; + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException + { + String enabledFieldsString = runBackendStepInput.getValueString(FIELD_ENABLED_FIELDS); + String[] enabledFields = enabledFieldsString.split(","); + + //////////////////////////////////////////////////////////////////////////////////////////// + // put the value in all the records (note, this is just for display on the review screen, // + // and/or if we wanted to do some validation - this is NOT what will be store, as the // + // Update action only wants fields that are being changed. // + //////////////////////////////////////////////////////////////////////////////////////////// + for(QRecord record : runBackendStepInput.getRecords()) + { + for(String fieldName : enabledFields) + { + Serializable value = runBackendStepInput.getValue(fieldName); + record.setValue(fieldName, value); + } + } + + ///////////////////////////////////////////////////////////////////// + // build the string to show the user what fields are being changed // + ///////////////////////////////////////////////////////////////////// + List valuesBeingUpdated = new ArrayList<>(); + QTableMetaData table = runBackendStepInput.getTable(); + for(String fieldName : enabledFields) + { + String label = table.getField(fieldName).getLabel(); + Serializable value = runBackendStepInput.getValue(fieldName); + + if(StringUtils.hasContent(ValueUtils.getValueAsString(value))) + { + valuesBeingUpdated.add(label + " will be set to: " + value); + } + else + { + valuesBeingUpdated.add(label + " will be cleared out."); + } + } + runBackendStepOutput.addValue(FIELD_VALUES_BEING_UPDATED, String.join("\n", valuesBeingUpdated)); + + runBackendStepOutput.setRecords(runBackendStepInput.getRecords()); + } +} diff --git a/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/bulk/edit/BulkEditStoreRecordsStep.java b/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/bulk/edit/BulkEditStoreRecordsStep.java new file mode 100644 index 00000000..5928b801 --- /dev/null +++ b/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/bulk/edit/BulkEditStoreRecordsStep.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.processes.implementations.bulk.edit; + + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import com.kingsrook.qqq.backend.core.actions.processes.BackendStep; +import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction; +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.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; + + +/******************************************************************************* + ** Backend step to store the records from a bulk insert file + *******************************************************************************/ +public class BulkEditStoreRecordsStep implements BackendStep +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException + { + String enabledFieldsString = runBackendStepInput.getValueString(BulkEditReceiveValuesStep.FIELD_ENABLED_FIELDS); + String[] enabledFields = enabledFieldsString.split(","); + QTableMetaData table = runBackendStepInput.getTable(); + List recordsToUpdate = new ArrayList<>(); + + runBackendStepInput.getAsyncJobCallback().updateStatus("Updating values in records..."); + int i = 1; + for(QRecord record : runBackendStepInput.getRecords()) + { + runBackendStepInput.getAsyncJobCallback().updateStatus(i++, runBackendStepInput.getRecords().size()); + QRecord recordToUpdate = new QRecord(); + recordsToUpdate.add(recordToUpdate); + + recordToUpdate.setValue(table.getPrimaryKeyField(), record.getValue(table.getPrimaryKeyField())); + for(String fieldName : enabledFields) + { + Serializable value = runBackendStepInput.getValue(fieldName); + recordToUpdate.setValue(fieldName, value); + } + } + + runBackendStepInput.getAsyncJobCallback().updateStatus("Updating database..."); + runBackendStepInput.getAsyncJobCallback().clearCurrentAndTotal(); + + UpdateInput updateInput = new UpdateInput(runBackendStepInput.getInstance()); + updateInput.setSession(runBackendStepInput.getSession()); + updateInput.setAsyncJobCallback(runBackendStepInput.getAsyncJobCallback()); + updateInput.setAreAllValuesBeingUpdatedTheSame(true); + updateInput.setTableName(runBackendStepInput.getTableName()); + updateInput.setRecords(recordsToUpdate); + + UpdateAction updateAction = new UpdateAction(); + UpdateOutput updateOutput = updateAction.execute(updateInput); + + runBackendStepOutput.setRecords(updateOutput.getRecords()); + } + +} diff --git a/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/bulk/insert/BulkInsertReceiveFileStep.java b/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/bulk/insert/BulkInsertReceiveFileStep.java new file mode 100644 index 00000000..7844ef9c --- /dev/null +++ b/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/bulk/insert/BulkInsertReceiveFileStep.java @@ -0,0 +1,49 @@ +/* + * 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.processes.implementations.bulk.insert; + + +import java.util.List; +import com.kingsrook.qqq.backend.core.actions.processes.BackendStep; +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.data.QRecord; + + +/******************************************************************************* + ** Backend step to receive a bulk-insert upload file + *******************************************************************************/ +public class BulkInsertReceiveFileStep implements BackendStep +{ + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException + { + List qRecords = BulkInsertUtils.getQRecordsFromFile(runBackendStepInput); + + runBackendStepOutput.addValue("noOfFileRows", qRecords.size()); + runBackendStepOutput.setRecords(qRecords); + } +} diff --git a/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/bulk/insert/BulkInsertStoreRecordsStep.java b/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/bulk/insert/BulkInsertStoreRecordsStep.java new file mode 100644 index 00000000..1f6710d5 --- /dev/null +++ b/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/bulk/insert/BulkInsertStoreRecordsStep.java @@ -0,0 +1,59 @@ +/* + * 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.processes.implementations.bulk.insert; + + +import java.util.List; +import com.kingsrook.qqq.backend.core.actions.processes.BackendStep; +import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; +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.tables.insert.InsertInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput; +import com.kingsrook.qqq.backend.core.model.data.QRecord; + + +/******************************************************************************* + ** Backend step to store the records from a bulk insert file + *******************************************************************************/ +public class BulkInsertStoreRecordsStep implements BackendStep +{ + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException + { + List qRecords = BulkInsertUtils.getQRecordsFromFile(runBackendStepInput); + + InsertInput insertInput = new InsertInput(runBackendStepInput.getInstance()); + insertInput.setSession(runBackendStepInput.getSession()); + insertInput.setTableName(runBackendStepInput.getTableName()); + insertInput.setRecords(qRecords); + + InsertAction insertAction = new InsertAction(); + InsertOutput insertOutput = insertAction.execute(insertInput); + + runBackendStepOutput.setRecords(insertOutput.getRecords()); + } +} diff --git a/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/bulk/insert/BulkInsertUtils.java b/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/bulk/insert/BulkInsertUtils.java new file mode 100644 index 00000000..f5684a72 --- /dev/null +++ b/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/bulk/insert/BulkInsertUtils.java @@ -0,0 +1,92 @@ +/* + * 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.processes.implementations.bulk.insert; + + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import com.kingsrook.qqq.backend.core.adapters.CsvToQRecordAdapter; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.processes.QUploadedFile; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput; +import com.kingsrook.qqq.backend.core.model.actions.shared.mapping.QKeyBasedFieldMapping; +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.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.state.AbstractStateKey; +import com.kingsrook.qqq.backend.core.state.TempFileStateProvider; + + +/******************************************************************************* + ** Utility methods used by bulk insert steps + *******************************************************************************/ +public class BulkInsertUtils +{ + /******************************************************************************* + ** + *******************************************************************************/ + static List getQRecordsFromFile(RunBackendStepInput runBackendStepInput) throws QException + { + AbstractStateKey stateKey = (AbstractStateKey) runBackendStepInput.getValue("uploadedFileKey"); + Optional optionalUploadedFile = TempFileStateProvider.getInstance().get(QUploadedFile.class, stateKey); + if(optionalUploadedFile.isEmpty()) + { + throw (new QException("Could not find uploaded file")); + } + + byte[] bytes = optionalUploadedFile.get().getBytes(); + + ///////////////////////////////////////////////////// + // let the user specify field labels instead names // + ///////////////////////////////////////////////////// + QTableMetaData table = runBackendStepInput.getTable(); + QKeyBasedFieldMapping mapping = new QKeyBasedFieldMapping(); + for(Map.Entry entry : table.getFields().entrySet()) + { + mapping.addMapping(entry.getKey(), entry.getValue().getLabel()); + } + + // todo - sniff out file type... + String tableName = runBackendStepInput.getTableName(); + List qRecords = new CsvToQRecordAdapter().buildRecordsFromCsv(new String(bytes), runBackendStepInput.getInstance().getTable(tableName), mapping); + + //////////////////////////////////////////////// + // remove values from any non-editable fields // + //////////////////////////////////////////////// + List nonEditableFields = table.getFields().values().stream() + .filter(f -> !f.getIsEditable()) + .toList(); + if(!nonEditableFields.isEmpty()) + { + for(QRecord qRecord : qRecords) + { + for(QFieldMetaData nonEditableField : nonEditableFields) + { + qRecord.setValue(nonEditableField.getName(), null); + } + } + } + + return (qRecords); + } +} diff --git a/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/general/LoadInitialRecordsStep.java b/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/general/LoadInitialRecordsStep.java index 42c72eee..b3ccd187 100644 --- a/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/general/LoadInitialRecordsStep.java +++ b/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/general/LoadInitialRecordsStep.java @@ -26,12 +26,14 @@ import com.kingsrook.qqq.backend.core.actions.processes.BackendStep; 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.tables.query.QQueryFilter; 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.QCodeUsage; import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionInputMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QRecordListMetaData; +import com.kingsrook.qqq.backend.core.utils.JsonUtils; /******************************************************************************* @@ -49,8 +51,12 @@ public class LoadInitialRecordsStep implements BackendStep public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException { ///////////////////////////////////////////////////////////////////////////////////////////////// - // actually, this is a no-op... we Just need a backendStep to be the first step in the process // + // basically this is a no-op... we Just need a backendStep to be the first step in the process // + // but, while we're here, go ahead and put the query filter in the payload as a value, in case // + // someone else wants it (see BulkDelete) // ///////////////////////////////////////////////////////////////////////////////////////////////// + QQueryFilter queryFilter = runBackendStepInput.getCallback().getQueryFilter(); + runBackendStepOutput.addValue("queryFilterJSON", JsonUtils.toJson(queryFilter)); } diff --git a/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/mock/MockBackendStep.java b/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/mock/MockBackendStep.java index 959c1d1e..da042f3c 100644 --- a/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/mock/MockBackendStep.java +++ b/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/mock/MockBackendStep.java @@ -49,20 +49,22 @@ public class MockBackendStep implements BackendStep @Override public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException { - runBackendStepOutput.getRecords().forEach(r -> - { - r.setValue(FIELD_MOCK_VALUE, "Ha ha!"); - LOG.info("We are mocking {}: {}", r.getValueString("firstName"), r.getValue(FIELD_MOCK_VALUE)); - }); - - runBackendStepOutput.setValues(runBackendStepInput.getValues()); - runBackendStepOutput.addValue(FIELD_MOCK_VALUE, MOCK_VALUE); - ///////////////////////////////// // mock the "greet" process... // ///////////////////////////////// runBackendStepOutput.addValue("outputMessage", runBackendStepInput.getValueString(FIELD_GREETING_PREFIX) + " X " + runBackendStepInput.getValueString(FIELD_GREETING_SUFFIX)); + runBackendStepInput.getRecords().forEach(r -> + { + LOG.info("We are mocking {}: {}", r.getValueString("firstName"), r.getValue(FIELD_MOCK_VALUE)); + r.setValue(FIELD_MOCK_VALUE, "Ha ha!"); + r.setValue("greetingMessage", runBackendStepInput.getValueString(FIELD_GREETING_PREFIX) + " " + r.getValueString("firstName") + " " + runBackendStepInput.getValueString(FIELD_GREETING_SUFFIX)); + }); + + runBackendStepOutput.setValues(runBackendStepInput.getValues()); + runBackendStepOutput.addValue(FIELD_MOCK_VALUE, MOCK_VALUE); + runBackendStepOutput.addValue("noOfPeopleGreeted", runBackendStepInput.getRecords().size()); + if("there".equalsIgnoreCase(runBackendStepInput.getValueString(FIELD_GREETING_SUFFIX))) { throw (new QException("You said Hello There, didn't you...")); diff --git a/src/main/java/com/kingsrook/qqq/backend/core/state/AbstractStateKey.java b/src/main/java/com/kingsrook/qqq/backend/core/state/AbstractStateKey.java index 9da763fb..f6fe0e1a 100644 --- a/src/main/java/com/kingsrook/qqq/backend/core/state/AbstractStateKey.java +++ b/src/main/java/com/kingsrook/qqq/backend/core/state/AbstractStateKey.java @@ -22,10 +22,13 @@ package com.kingsrook.qqq.backend.core.state; +import java.io.Serializable; + + /******************************************************************************* ** *******************************************************************************/ -public abstract class AbstractStateKey +public abstract class AbstractStateKey implements Serializable { /******************************************************************************* ** Make the key give a unique string to identify itself. diff --git a/src/main/java/com/kingsrook/qqq/backend/core/state/StateType.java b/src/main/java/com/kingsrook/qqq/backend/core/state/StateType.java index 14af21b5..47c26a71 100644 --- a/src/main/java/com/kingsrook/qqq/backend/core/state/StateType.java +++ b/src/main/java/com/kingsrook/qqq/backend/core/state/StateType.java @@ -31,5 +31,7 @@ package com.kingsrook.qqq.backend.core.state; public enum StateType { PROCESS_STATUS, - ASYNC_JOB_STATUS + ASYNC_JOB_STATUS, + ASYNC_JOB_RESULT, + UPLOADED_FILE } diff --git a/src/main/java/com/kingsrook/qqq/backend/core/state/UUIDAndTypeStateKey.java b/src/main/java/com/kingsrook/qqq/backend/core/state/UUIDAndTypeStateKey.java index 5e4ecf09..38659ccc 100644 --- a/src/main/java/com/kingsrook/qqq/backend/core/state/UUIDAndTypeStateKey.java +++ b/src/main/java/com/kingsrook/qqq/backend/core/state/UUIDAndTypeStateKey.java @@ -22,6 +22,7 @@ package com.kingsrook.qqq.backend.core.state; +import java.io.Serializable; import java.util.Objects; import java.util.UUID; @@ -29,7 +30,7 @@ import java.util.UUID; /******************************************************************************* ** *******************************************************************************/ -public class UUIDAndTypeStateKey extends AbstractStateKey +public class UUIDAndTypeStateKey extends AbstractStateKey implements Serializable { private final UUID uuid; private final StateType stateType; diff --git a/src/test/java/com/kingsrook/qqq/backend/core/actions/processes/RunBackendStepActionTest.java b/src/test/java/com/kingsrook/qqq/backend/core/actions/processes/RunBackendStepActionTest.java index 4820bf95..3b88e502 100644 --- a/src/test/java/com/kingsrook/qqq/backend/core/actions/processes/RunBackendStepActionTest.java +++ b/src/test/java/com/kingsrook/qqq/backend/core/actions/processes/RunBackendStepActionTest.java @@ -109,6 +109,7 @@ public class RunBackendStepActionTest XYZ"""; case HTML -> "Oh my"; case PASSWORD -> "myPa**word"; + case BLOB -> new byte[] { 1, 2, 3, 4 }; }); } return (rs); diff --git a/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/DeleteActionTest.java b/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/DeleteActionTest.java index b92732a2..f5d5c5bf 100644 --- a/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/DeleteActionTest.java +++ b/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/DeleteActionTest.java @@ -26,10 +26,14 @@ import java.util.List; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput; import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteOutput; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; 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; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; /******************************************************************************* @@ -53,8 +57,28 @@ class DeleteActionTest request.setPrimaryKeys(List.of(1, 2)); DeleteOutput result = new DeleteAction().execute(request); assertNotNull(result); - assertEquals(2, result.getRecords().size()); - // todo - add errors to QRecord? assertTrue(result.getRecords().stream().allMatch(r -> r.getErrors() == null)); + assertEquals(2, result.getDeletedRecordCount()); + assertTrue(CollectionUtils.nullSafeIsEmpty(result.getRecordsWithErrors())); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testErrorIfBothPrimaryKeysAndFilter() + { + DeleteInput request = new DeleteInput(TestUtils.defineInstance()); + request.setSession(TestUtils.getMockSession()); + request.setTableName("person"); + request.setPrimaryKeys(List.of(1, 2)); + request.setQueryFilter(new QQueryFilter()); + + assertThrows(QException.class, () -> + { + new DeleteAction().execute(request); + }); } } diff --git a/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/bulk/delete/BulkDeleteStoreStepTest.java b/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/bulk/delete/BulkDeleteStoreStepTest.java new file mode 100644 index 00000000..db642bc4 --- /dev/null +++ b/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/bulk/delete/BulkDeleteStoreStepTest.java @@ -0,0 +1,71 @@ +/* + * 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.processes.implementations.bulk.delete; + + +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.tables.query.QQueryFilter; +import com.kingsrook.qqq.backend.core.utils.JsonUtils; +import com.kingsrook.qqq.backend.core.utils.TestUtils; +import org.junit.jupiter.api.Test; + + +/******************************************************************************* + ** Unit test for BulkDeleteStoreStep + *******************************************************************************/ +class BulkDeleteStoreStepTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testWithoutFilter() throws QException + { + RunBackendStepInput stepInput = new RunBackendStepInput(TestUtils.defineInstance()); + stepInput.setSession(TestUtils.getMockSession()); + stepInput.setTableName(TestUtils.defineTablePerson().getName()); + stepInput.setRecords(TestUtils.queryTable(TestUtils.defineTablePerson().getName())); + + RunBackendStepOutput stepOutput = new RunBackendStepOutput(); + new BulkDeleteStoreStep().run(stepInput, stepOutput); + } + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testWithFilter() throws QException + { + RunBackendStepInput stepInput = new RunBackendStepInput(TestUtils.defineInstance()); + stepInput.setSession(TestUtils.getMockSession()); + stepInput.setTableName(TestUtils.defineTablePerson().getName()); + stepInput.addValue("queryFilterJSON", JsonUtils.toJson(new QQueryFilter())); + + RunBackendStepOutput stepOutput = new RunBackendStepOutput(); + new BulkDeleteStoreStep().run(stepInput, stepOutput); + } + +} \ No newline at end of file diff --git a/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/bulk/edit/BulkEditReceiveValuesStepTest.java b/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/bulk/edit/BulkEditReceiveValuesStepTest.java new file mode 100644 index 00000000..0619d13a --- /dev/null +++ b/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/bulk/edit/BulkEditReceiveValuesStepTest.java @@ -0,0 +1,78 @@ +/* + * 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.processes.implementations.bulk.edit; + + +import java.util.List; +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.data.QRecord; +import com.kingsrook.qqq.backend.core.utils.TestUtils; +import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + + +/******************************************************************************* + ** Unit test for BulkEditReceiveValuesStep + *******************************************************************************/ +class BulkEditReceiveValuesStepTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void test() throws QException + { + RunBackendStepInput stepInput = new RunBackendStepInput(TestUtils.defineInstance()); + stepInput.setSession(TestUtils.getMockSession()); + stepInput.setTableName(TestUtils.defineTablePerson().getName()); + stepInput.addValue(BulkEditReceiveValuesStep.FIELD_ENABLED_FIELDS, "firstName,email,birthDate"); + stepInput.addValue("firstName", "Johnny"); + stepInput.addValue("email", null); + stepInput.addValue("birthDate", "1909-01-09"); + List records = TestUtils.queryTable(TestUtils.defineTablePerson().getName()); + stepInput.setRecords(records); + + RunBackendStepOutput stepOutput = new RunBackendStepOutput(); + new BulkEditReceiveValuesStep().run(stepInput, stepOutput); + + String valuesBeingUpdated = stepOutput.getValueString(BulkEditReceiveValuesStep.FIELD_VALUES_BEING_UPDATED); + assertThat(valuesBeingUpdated).matches("(?s).*First Name.*Johnny.*"); + assertThat(valuesBeingUpdated).matches("(?s).*Email will be cleared.*"); + assertThat(valuesBeingUpdated).matches("(?s).*Birth Date.*1909-01-09.*"); + + int count = 0; + for(QRecord record : stepOutput.getRecords()) + { + assertEquals("Johnny", record.getValueString("firstName")); + assertNull(record.getValue("email")); + // todo value utils needed in getValueDate... assertEquals(LocalDate.of(1909, 1, 9), record.getValueDate("birthDate")); + count++; + } + assertEquals(records.size(), count); + } + +} \ No newline at end of file diff --git a/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/bulk/edit/BulkEditStoreRecordsStepTest.java b/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/bulk/edit/BulkEditStoreRecordsStepTest.java new file mode 100644 index 00000000..9b110db4 --- /dev/null +++ b/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/bulk/edit/BulkEditStoreRecordsStepTest.java @@ -0,0 +1,85 @@ +/* + * 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.processes.implementations.bulk.edit; + + +import java.util.List; +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.data.QRecord; +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.assertNull; + + +/******************************************************************************* + ** Unit test for BulkEditStoreRecordsStep + *******************************************************************************/ +class BulkEditStoreRecordsStepTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void test() throws QException + { + RunBackendStepInput stepInput = new RunBackendStepInput(TestUtils.defineInstance()); + stepInput.setSession(TestUtils.getMockSession()); + stepInput.setTableName(TestUtils.defineTablePerson().getName()); + stepInput.addValue(BulkEditReceiveValuesStep.FIELD_ENABLED_FIELDS, "firstName,email,birthDate"); + stepInput.addValue("firstName", "Johnny"); + stepInput.addValue("email", null); + stepInput.addValue("birthDate", "1909-01-09"); + List records = TestUtils.queryTable(TestUtils.defineTablePerson().getName()); + stepInput.setRecords(records); + + RunBackendStepOutput stepOutput = new RunBackendStepOutput(); + new BulkEditStoreRecordsStep().run(stepInput, stepOutput); + + assertRecordValues(stepOutput.getRecords()); + + // re-fetch the records, make sure they are updated. + // but since Mock backend doesn't actually update them, we can't do this.. + // todo - implement an in-memory backend, that would do this. + // List updatedRecords = TestUtils.queryTable(TestUtils.defineTablePerson().getName()); + // assertRecordValues(updatedRecords); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private void assertRecordValues(List records) + { + for(QRecord record : records) + { + assertEquals("Johnny", record.getValueString("firstName")); + assertNull(record.getValue("email")); + // todo value utils needed in getValueDate... assertEquals(LocalDate.of(1909, 1, 9), record.getValueDate("birthDate")); + } + } + +} \ No newline at end of file diff --git a/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/bulk/insert/BulkInsertReceiveFileStepTest.java b/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/bulk/insert/BulkInsertReceiveFileStepTest.java new file mode 100644 index 00000000..df53ac98 --- /dev/null +++ b/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/bulk/insert/BulkInsertReceiveFileStepTest.java @@ -0,0 +1,81 @@ +/* + * 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.processes.implementations.bulk.insert; + + +import java.util.List; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.processes.QUploadedFile; +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.data.QRecord; +import com.kingsrook.qqq.backend.core.state.StateType; +import com.kingsrook.qqq.backend.core.state.TempFileStateProvider; +import com.kingsrook.qqq.backend.core.state.UUIDAndTypeStateKey; +import com.kingsrook.qqq.backend.core.utils.TestUtils; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + + +/******************************************************************************* + ** Unit test for BulkInsertReceiveFileStep + *******************************************************************************/ +class BulkInsertReceiveFileStepTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void test() throws QException + { + //////////////////////////////////////////////////////////////// + // create an uploaded file, similar to how an http server may // + //////////////////////////////////////////////////////////////// + QUploadedFile qUploadedFile = new QUploadedFile(); + qUploadedFile.setBytes((TestUtils.getPersonCsvHeaderUsingLabels() + TestUtils.getPersonCsvRow1() + TestUtils.getPersonCsvRow2()).getBytes()); + qUploadedFile.setFilename("test.csv"); + UUIDAndTypeStateKey key = new UUIDAndTypeStateKey(StateType.UPLOADED_FILE); + TempFileStateProvider.getInstance().put(key, qUploadedFile); + + //////////////////////////// + // setup and run the step // + //////////////////////////// + RunBackendStepInput stepInput = new RunBackendStepInput(TestUtils.defineInstance()); + stepInput.setSession(TestUtils.getMockSession()); + stepInput.setTableName(TestUtils.defineTablePerson().getName()); + stepInput.addValue("uploadedFileKey", key); + + RunBackendStepOutput stepOutput = new RunBackendStepOutput(); + new BulkInsertReceiveFileStep().run(stepInput, stepOutput); + + List records = stepOutput.getRecords(); + assertEquals(2, records.size()); + assertEquals("John", records.get(0).getValueString("firstName")); + assertEquals("Jane", records.get(1).getValueString("firstName")); + assertNull(records.get(0).getValue("id")); + assertNull(records.get(1).getValue("id")); + + assertEquals(2, stepOutput.getValueInteger("noOfFileRows")); + } + +} \ No newline at end of file diff --git a/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/bulk/insert/BulkInsertStoreRecordsStepTest.java b/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/bulk/insert/BulkInsertStoreRecordsStepTest.java new file mode 100644 index 00000000..7e0b4860 --- /dev/null +++ b/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/bulk/insert/BulkInsertStoreRecordsStepTest.java @@ -0,0 +1,80 @@ +/* + * 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.processes.implementations.bulk.insert; + + +import java.util.List; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.processes.QUploadedFile; +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.data.QRecord; +import com.kingsrook.qqq.backend.core.state.StateType; +import com.kingsrook.qqq.backend.core.state.TempFileStateProvider; +import com.kingsrook.qqq.backend.core.state.UUIDAndTypeStateKey; +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 BulkInsertStoreRecordsStep + *******************************************************************************/ +class BulkInsertStoreRecordsStepTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void test() throws QException + { + //////////////////////////////////////////////////////////////// + // create an uploaded file, similar to how an http server may // + //////////////////////////////////////////////////////////////// + QUploadedFile qUploadedFile = new QUploadedFile(); + qUploadedFile.setBytes((TestUtils.getPersonCsvHeaderUsingLabels() + TestUtils.getPersonCsvRow1() + TestUtils.getPersonCsvRow2()).getBytes()); + qUploadedFile.setFilename("test.csv"); + UUIDAndTypeStateKey key = new UUIDAndTypeStateKey(StateType.UPLOADED_FILE); + TempFileStateProvider.getInstance().put(key, qUploadedFile); + + //////////////////////////// + // setup and run the step // + //////////////////////////// + RunBackendStepInput stepInput = new RunBackendStepInput(TestUtils.defineInstance()); + stepInput.setSession(TestUtils.getMockSession()); + stepInput.setTableName(TestUtils.defineTablePerson().getName()); + stepInput.addValue("uploadedFileKey", key); + + RunBackendStepOutput stepOutput = new RunBackendStepOutput(); + new BulkInsertStoreRecordsStep().run(stepInput, stepOutput); + + List records = stepOutput.getRecords(); + assertEquals(2, records.size()); + assertEquals("John", records.get(0).getValueString("firstName")); + assertEquals("Jane", records.get(1).getValueString("firstName")); + assertNotNull(records.get(0).getValue("id")); + assertNotNull(records.get(1).getValue("id")); + } + +} \ No newline at end of file diff --git a/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java b/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java index 02828726..76c4e30b 100644 --- a/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java +++ b/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java @@ -25,7 +25,12 @@ package com.kingsrook.qqq.backend.core.utils; import java.util.List; import com.kingsrook.qqq.backend.core.actions.processes.person.addtopeoplesage.AddAge; import com.kingsrook.qqq.backend.core.actions.processes.person.addtopeoplesage.GetAgeStatistics; +import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; import com.kingsrook.qqq.backend.core.adapters.QInstanceAdapter; +import com.kingsrook.qqq.backend.core.exceptions.QException; +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.processes.implementations.mock.MockBackendStep; import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationMetaData; import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData; @@ -56,8 +61,8 @@ import com.kingsrook.qqq.backend.core.processes.implementations.etl.basic.BasicE *******************************************************************************/ public class TestUtils { - public static String DEFAULT_BACKEND_NAME = "default"; - public static String PROCESS_NAME_GREET_PEOPLE = "greet"; + public static String DEFAULT_BACKEND_NAME = "default"; + public static String PROCESS_NAME_GREET_PEOPLE = "greet"; public static String PROCESS_NAME_GREET_PEOPLE_INTERACTIVE = "greetInteractive"; @@ -136,9 +141,9 @@ public class TestUtils .withLabel("Person") .withBackendName(DEFAULT_BACKEND_NAME) .withPrimaryKeyField("id") - .withField(new QFieldMetaData("id", QFieldType.INTEGER)) - .withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME)) - .withField(new QFieldMetaData("modifyDate", QFieldType.DATE_TIME)) + .withField(new QFieldMetaData("id", QFieldType.INTEGER).withIsEditable(false)) + .withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME).withIsEditable(false)) + .withField(new QFieldMetaData("modifyDate", QFieldType.DATE_TIME).withIsEditable(false)) .withField(new QFieldMetaData("firstName", QFieldType.STRING)) .withField(new QFieldMetaData("lastName", QFieldType.STRING)) .withField(new QFieldMetaData("birthDate", QFieldType.DATE)) @@ -306,4 +311,67 @@ public class TestUtils MockAuthenticationModule mockAuthenticationModule = new MockAuthenticationModule(); return (mockAuthenticationModule.createSession(null)); } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static List queryTable(String tableName) throws QException + { + QueryInput queryInput = new QueryInput(TestUtils.defineInstance()); + queryInput.setSession(TestUtils.getMockSession()); + queryInput.setTableName(tableName); + QueryOutput queryOutput = new QueryAction().execute(queryInput); + return (queryOutput.getRecords()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static String getPersonCsvHeader() + { + return (""" + "id","createDate","modifyDate","firstName","lastName","birthDate","email"\r + """); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static String getPersonCsvHeaderUsingLabels() + { + return (""" + "Id","Create Date","Modify Date","First Name","Last Name","Birth Date","Email"\r + """); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static String getPersonCsvRow1() + { + return (""" + "0","2021-10-26 14:39:37","2021-10-26 14:39:37","John","Doe","1980-01-01","john@doe.com"\r + """); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static String getPersonCsvRow2() + { + return (""" + "0","2021-10-26 14:39:37","2021-10-26 14:39:37","Jane","Doe","1981-01-01","john@doe.com"\r + """); + } + }