mirror of
https://github.com/Kingsrook/qqq.git
synced 2025-07-20 22:18:43 +00:00
Compare commits
34 Commits
snapshot-f
...
feature/we
Author | SHA1 | Date | |
---|---|---|---|
d7867b8d22 | |||
96217c839d | |||
5c02c1fd2e | |||
9b2c281431 | |||
5327424cec | |||
4fd68f9195 | |||
cb6101d0ed | |||
55e372a70f | |||
7b190d810a | |||
1fb509fea1 | |||
786f9ba8df | |||
55905d251d | |||
d23dbac0d9 | |||
962d09b120 | |||
4827669c0a | |||
6efc34b69e | |||
da52fccc86 | |||
efc69fee4b | |||
1808cea5c0 | |||
a7b5e00e27 | |||
685e747a91 | |||
3d6f05e4ea | |||
97883b3e43 | |||
e11a23ccc0 | |||
12383930b0 | |||
cc19268132 | |||
4883514f58 | |||
2ee26b14a9 | |||
e9e029d8e9 | |||
ff4a0b8849 | |||
7089ec92a6 | |||
60c5c11549 | |||
3c765e9e47 | |||
5db8cf9ca1 |
@ -0,0 +1,226 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.actions.customizers;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.QueryOrGetInputInterface;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.code.InitializableViaCodeReference;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReferenceWithProperties;
|
||||
import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Implementation of TableCustomizerInterface that runs multiple other customizers
|
||||
*******************************************************************************/
|
||||
public class MultiCustomizer implements InitializableViaCodeReference, TableCustomizerInterface
|
||||
{
|
||||
private static final String KEY_CODE_REFERENCES = "codeReferences";
|
||||
|
||||
private List<TableCustomizerInterface> customizers = new ArrayList<>();
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* Factory method that builds a {@link QCodeReferenceWithProperties} that will
|
||||
* allow this multi-customizer to be assigned to a table, and to track
|
||||
* in that code ref's properties, the "sub" QCodeReferences to be used.
|
||||
*
|
||||
* Added to a table as in:
|
||||
* <pre>
|
||||
* table.withCustomizer(TableCustomizers.POST_INSERT_RECORD,
|
||||
* MultiCustomizer.of(QCodeReference(x), QCodeReference(y)));
|
||||
* </pre>
|
||||
*
|
||||
* @param codeReferences
|
||||
* one or more {@link QCodeReference objects} to run when this customizer
|
||||
* runs. note that they will run in the order provided in this list.
|
||||
***************************************************************************/
|
||||
public static QCodeReferenceWithProperties of(QCodeReference... codeReferences)
|
||||
{
|
||||
ArrayList<QCodeReference> list = new ArrayList<>(Arrays.stream(codeReferences).toList());
|
||||
return (new QCodeReferenceWithProperties(MultiCustomizer.class, MapBuilder.of(KEY_CODE_REFERENCES, list)));
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* Add an additional table customizer code reference to an existing
|
||||
* codeReference, e.g., constructed by the `of` factory method.
|
||||
*
|
||||
* @see #of(QCodeReference...)
|
||||
***************************************************************************/
|
||||
public static void addTableCustomizer(QCodeReferenceWithProperties existingMultiCustomizerCodeReference, QCodeReference codeReference)
|
||||
{
|
||||
ArrayList<QCodeReference> list = (ArrayList<QCodeReference>) existingMultiCustomizerCodeReference.getProperties().computeIfAbsent(KEY_CODE_REFERENCES, key -> new ArrayList<>());
|
||||
list.add(codeReference);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* When this class is instantiated by the QCodeLoader, initialize the
|
||||
* sub-customizer objects.
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public void initialize(QCodeReference codeReference)
|
||||
{
|
||||
if(codeReference instanceof QCodeReferenceWithProperties codeReferenceWithProperties)
|
||||
{
|
||||
Serializable codeReferencesPropertyValue = codeReferenceWithProperties.getProperties().get(KEY_CODE_REFERENCES);
|
||||
if(codeReferencesPropertyValue instanceof List<?> list)
|
||||
{
|
||||
for(Object o : list)
|
||||
{
|
||||
if(o instanceof QCodeReference reference)
|
||||
{
|
||||
TableCustomizerInterface customizer = QCodeLoader.getAdHoc(TableCustomizerInterface.class, reference);
|
||||
customizers.add(customizer);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG.warn("Property KEY_CODE_REFERENCES [" + KEY_CODE_REFERENCES + "] must be a List<QCodeReference>.");
|
||||
}
|
||||
}
|
||||
|
||||
if(customizers.isEmpty())
|
||||
{
|
||||
LOG.info("No TableCustomizers were specified for MultiCustomizer.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* run postQuery method over all sub-customizers
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public List<QRecord> postQuery(QueryOrGetInputInterface queryInput, List<QRecord> records) throws QException
|
||||
{
|
||||
for(TableCustomizerInterface customizer : customizers)
|
||||
{
|
||||
records = customizer.postQuery(queryInput, records);
|
||||
}
|
||||
return records;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* run preInsert method over all sub-customizers
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public List<QRecord> preInsert(InsertInput insertInput, List<QRecord> records, boolean isPreview) throws QException
|
||||
{
|
||||
for(TableCustomizerInterface customizer : customizers)
|
||||
{
|
||||
records = customizer.preInsert(insertInput, records, isPreview);
|
||||
}
|
||||
return records;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* run postInsert method over all sub-customizers
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public List<QRecord> postInsert(InsertInput insertInput, List<QRecord> records) throws QException
|
||||
{
|
||||
for(TableCustomizerInterface customizer : customizers)
|
||||
{
|
||||
records = customizer.postInsert(insertInput, records);
|
||||
}
|
||||
return records;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* run preUpdate method over all sub-customizers
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public List<QRecord> preUpdate(UpdateInput updateInput, List<QRecord> records, boolean isPreview, Optional<List<QRecord>> oldRecordList) throws QException
|
||||
{
|
||||
for(TableCustomizerInterface customizer : customizers)
|
||||
{
|
||||
records = customizer.preUpdate(updateInput, records, isPreview, oldRecordList);
|
||||
}
|
||||
return records;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* run postUpdate method over all sub-customizers
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public List<QRecord> postUpdate(UpdateInput updateInput, List<QRecord> records, Optional<List<QRecord>> oldRecordList) throws QException
|
||||
{
|
||||
for(TableCustomizerInterface customizer : customizers)
|
||||
{
|
||||
records = customizer.postUpdate(updateInput, records, oldRecordList);
|
||||
}
|
||||
return records;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* run preDelete method over all sub-customizers
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public List<QRecord> preDelete(DeleteInput deleteInput, List<QRecord> records, boolean isPreview) throws QException
|
||||
{
|
||||
for(TableCustomizerInterface customizer : customizers)
|
||||
{
|
||||
records = customizer.preDelete(deleteInput, records, isPreview);
|
||||
}
|
||||
return records;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* run postDelete method over all sub-customizers
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public List<QRecord> postDelete(DeleteInput deleteInput, List<QRecord> records) throws QException
|
||||
{
|
||||
for(TableCustomizerInterface customizer : customizers)
|
||||
{
|
||||
records = customizer.postDelete(deleteInput, records);
|
||||
}
|
||||
return records;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,88 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.actions.customizers;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
|
||||
import com.kingsrook.qqq.backend.core.utils.collections.TypeTolerantKeyMap;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** utility class to help table customizers working with the oldRecordList.
|
||||
** Usage is just 2 lines:
|
||||
** outside of loop-over-records:
|
||||
** - OldRecordHelper oldRecordHelper = new OldRecordHelper(updateInput.getTableName(), oldRecordList);
|
||||
** then inside the record loop:
|
||||
** - Optional<QRecord> oldRecord = oldRecordHelper.getOldRecord(record);
|
||||
*******************************************************************************/
|
||||
public class OldRecordHelper
|
||||
{
|
||||
private String primaryKeyField;
|
||||
private QFieldType primaryKeyType;
|
||||
|
||||
private Optional<List<QRecord>> oldRecordList;
|
||||
private Map<Serializable, QRecord> oldRecordMap;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor
|
||||
**
|
||||
*******************************************************************************/
|
||||
public OldRecordHelper(String tableName, Optional<List<QRecord>> oldRecordList)
|
||||
{
|
||||
this.primaryKeyField = QContext.getQInstance().getTable(tableName).getPrimaryKeyField();
|
||||
this.primaryKeyType = QContext.getQInstance().getTable(tableName).getField(primaryKeyField).getType();
|
||||
|
||||
this.oldRecordList = oldRecordList;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
public Optional<QRecord> getOldRecord(QRecord record)
|
||||
{
|
||||
if(oldRecordMap == null)
|
||||
{
|
||||
if(oldRecordList.isPresent())
|
||||
{
|
||||
oldRecordMap = new TypeTolerantKeyMap<>(primaryKeyType);
|
||||
oldRecordList.get().forEach(r -> oldRecordMap.put(r.getValue(primaryKeyField), r));
|
||||
}
|
||||
else
|
||||
{
|
||||
oldRecordMap = Collections.emptyMap();
|
||||
}
|
||||
}
|
||||
|
||||
return (Optional.ofNullable(oldRecordMap.get(record.getValue(primaryKeyField))));
|
||||
}
|
||||
}
|
@ -57,7 +57,7 @@ public class BulkTableActionProcessPermissionChecker implements CustomPermission
|
||||
switch(bulkActionName)
|
||||
{
|
||||
case "bulkInsert" -> PermissionsHelper.checkTablePermissionThrowing(tableActionInput, TablePermissionSubType.INSERT);
|
||||
case "bulkEdit", "bulkEditWithFile" -> PermissionsHelper.checkTablePermissionThrowing(tableActionInput, TablePermissionSubType.EDIT);
|
||||
case "bulkEdit" -> PermissionsHelper.checkTablePermissionThrowing(tableActionInput, TablePermissionSubType.EDIT);
|
||||
case "bulkDelete" -> PermissionsHelper.checkTablePermissionThrowing(tableActionInput, TablePermissionSubType.DELETE);
|
||||
default -> LOG.warn("Unexpected bulk action name when checking permissions for process: " + processName);
|
||||
}
|
||||
|
@ -401,6 +401,7 @@ public class DeleteAction
|
||||
if(CollectionUtils.nullSafeHasContents(associatedKeys))
|
||||
{
|
||||
DeleteInput nextLevelDeleteInput = new DeleteInput();
|
||||
nextLevelDeleteInput.setFlags(deleteInput.getFlags());
|
||||
nextLevelDeleteInput.setTransaction(deleteInput.getTransaction());
|
||||
nextLevelDeleteInput.setTableName(association.getAssociatedTableName());
|
||||
nextLevelDeleteInput.setPrimaryKeys(associatedKeys);
|
||||
|
@ -34,7 +34,6 @@ import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import com.kingsrook.qqq.backend.core.actions.AbstractQActionFunction;
|
||||
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
|
||||
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
|
||||
import com.kingsrook.qqq.backend.core.actions.audits.DMLAuditAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.automation.AutomationStatus;
|
||||
import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationStatusUpdater;
|
||||
@ -54,6 +53,7 @@ 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;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
|
||||
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.joins.JoinOn;
|
||||
@ -157,7 +157,7 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
|
||||
//////////////////////////////////////////////////
|
||||
// insert any associations in the input records //
|
||||
//////////////////////////////////////////////////
|
||||
manageAssociations(table, insertOutput.getRecords(), insertInput.getTransaction());
|
||||
manageAssociations(table, insertOutput.getRecords(), insertInput);
|
||||
|
||||
//////////////////
|
||||
// do the audit //
|
||||
@ -174,9 +174,21 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
|
||||
.withRecordList(insertOutput.getRecords()));
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////
|
||||
// finally, run the post-insert customizer, if there is one //
|
||||
//////////////////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////
|
||||
// finally, run the post-insert customizers, if there are any //
|
||||
////////////////////////////////////////////////////////////////
|
||||
runPostInsertCustomizers(insertInput, table, insertOutput);
|
||||
|
||||
return insertOutput;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
private static void runPostInsertCustomizers(InsertInput insertInput, QTableMetaData table, InsertOutput insertOutput)
|
||||
{
|
||||
Optional<TableCustomizerInterface> postInsertCustomizer = QCodeLoader.getTableCustomizer(table, TableCustomizers.POST_INSERT_RECORD.getRole());
|
||||
if(postInsertCustomizer.isPresent())
|
||||
{
|
||||
@ -193,7 +205,25 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
|
||||
}
|
||||
}
|
||||
|
||||
return insertOutput;
|
||||
///////////////////////////////////////////////
|
||||
// run all of the instance-level customizers //
|
||||
///////////////////////////////////////////////
|
||||
List<QCodeReference> tableCustomizerCodes = QContext.getQInstance().getTableCustomizers(TableCustomizers.POST_INSERT_RECORD);
|
||||
for(QCodeReference tableCustomizerCode : tableCustomizerCodes)
|
||||
{
|
||||
try
|
||||
{
|
||||
TableCustomizerInterface tableCustomizer = QCodeLoader.getAdHoc(TableCustomizerInterface.class, tableCustomizerCode);
|
||||
insertOutput.setRecords(tableCustomizer.postInsert(insertInput, insertOutput.getRecords()));
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
for(QRecord record : insertOutput.getRecords())
|
||||
{
|
||||
record.addWarning(new QWarningMessage("An error occurred after the insert: " + e.getMessage()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -308,6 +338,19 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
|
||||
insertInput.setRecords(preInsertCustomizer.get().preInsert(insertInput, insertInput.getRecords(), isPreview));
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////
|
||||
// run all of the instance-level customizers //
|
||||
///////////////////////////////////////////////
|
||||
List<QCodeReference> tableCustomizerCodes = QContext.getQInstance().getTableCustomizers(TableCustomizers.PRE_INSERT_RECORD);
|
||||
for(QCodeReference tableCustomizerCode : tableCustomizerCodes)
|
||||
{
|
||||
TableCustomizerInterface tableCustomizer = QCodeLoader.getAdHoc(TableCustomizerInterface.class, tableCustomizerCode);
|
||||
if(whenToRun.equals(tableCustomizer.whenToRunPreInsert(insertInput, isPreview)))
|
||||
{
|
||||
insertInput.setRecords(tableCustomizer.preInsert(insertInput, insertInput.getRecords(), isPreview));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -342,7 +385,7 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void manageAssociations(QTableMetaData table, List<QRecord> insertedRecords, QBackendTransaction transaction) throws QException
|
||||
private void manageAssociations(QTableMetaData table, List<QRecord> insertedRecords, InsertInput insertInput) throws QException
|
||||
{
|
||||
for(Association association : CollectionUtils.nonNullList(table.getAssociations()))
|
||||
{
|
||||
@ -375,7 +418,8 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
|
||||
if(CollectionUtils.nullSafeHasContents(nextLevelInserts))
|
||||
{
|
||||
InsertInput nextLevelInsertInput = new InsertInput();
|
||||
nextLevelInsertInput.setTransaction(transaction);
|
||||
nextLevelInsertInput.withFlags(insertInput.getFlags());
|
||||
nextLevelInsertInput.setTransaction(insertInput.getTransaction());
|
||||
nextLevelInsertInput.setTableName(association.getAssociatedTableName());
|
||||
nextLevelInsertInput.setRecords(nextLevelInserts);
|
||||
InsertOutput nextLevelInsertOutput = new InsertAction().execute(nextLevelInsertInput);
|
||||
|
@ -126,6 +126,7 @@ public class ReplaceAction extends AbstractQActionFunction<ReplaceInput, Replace
|
||||
InsertInput insertInput = new InsertInput();
|
||||
insertInput.setTableName(table.getName());
|
||||
insertInput.setRecords(insertList);
|
||||
insertInput.withFlags(input.getFlags());
|
||||
insertInput.setTransaction(transaction);
|
||||
insertInput.setOmitDmlAudit(input.getOmitDmlAudit());
|
||||
InsertOutput insertOutput = new InsertAction().execute(insertInput);
|
||||
@ -135,6 +136,7 @@ public class ReplaceAction extends AbstractQActionFunction<ReplaceInput, Replace
|
||||
UpdateInput updateInput = new UpdateInput();
|
||||
updateInput.setTableName(table.getName());
|
||||
updateInput.setRecords(updateList);
|
||||
updateInput.withFlags(input.getFlags());
|
||||
updateInput.setTransaction(transaction);
|
||||
updateInput.setOmitDmlAudit(input.getOmitDmlAudit());
|
||||
UpdateOutput updateOutput = new UpdateAction().execute(updateInput);
|
||||
@ -151,6 +153,7 @@ public class ReplaceAction extends AbstractQActionFunction<ReplaceInput, Replace
|
||||
DeleteInput deleteInput = new DeleteInput();
|
||||
deleteInput.setTableName(table.getName());
|
||||
deleteInput.setQueryFilter(deleteFilter);
|
||||
deleteInput.withFlags(input.getFlags());
|
||||
deleteInput.setTransaction(transaction);
|
||||
deleteInput.setOmitDmlAudit(input.getOmitDmlAudit());
|
||||
DeleteOutput deleteOutput = new DeleteAction().execute(deleteInput);
|
||||
|
@ -57,6 +57,7 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.DynamicDefaultValueBehavior;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldBehavior;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
@ -199,6 +200,18 @@ public class UpdateAction
|
||||
//////////////////////////////////////////////////////////////
|
||||
// finally, run the post-update customizer, if there is one //
|
||||
//////////////////////////////////////////////////////////////
|
||||
runPostUpdateCustomizers(updateInput, table, updateOutput, oldRecordList);
|
||||
|
||||
return updateOutput;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
private static void runPostUpdateCustomizers(UpdateInput updateInput, QTableMetaData table, UpdateOutput updateOutput, Optional<List<QRecord>> oldRecordList)
|
||||
{
|
||||
Optional<TableCustomizerInterface> postUpdateCustomizer = QCodeLoader.getTableCustomizer(table, TableCustomizers.POST_UPDATE_RECORD.getRole());
|
||||
if(postUpdateCustomizer.isPresent())
|
||||
{
|
||||
@ -215,7 +228,49 @@ public class UpdateAction
|
||||
}
|
||||
}
|
||||
|
||||
return updateOutput;
|
||||
///////////////////////////////////////////////
|
||||
// run all of the instance-level customizers //
|
||||
///////////////////////////////////////////////
|
||||
List<QCodeReference> tableCustomizerCodes = QContext.getQInstance().getTableCustomizers(TableCustomizers.POST_UPDATE_RECORD);
|
||||
for(QCodeReference tableCustomizerCode : tableCustomizerCodes)
|
||||
{
|
||||
try
|
||||
{
|
||||
TableCustomizerInterface tableCustomizer = QCodeLoader.getAdHoc(TableCustomizerInterface.class, tableCustomizerCode);
|
||||
updateOutput.setRecords(tableCustomizer.postUpdate(updateInput, updateOutput.getRecords(), oldRecordList));
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
for(QRecord record : updateOutput.getRecords())
|
||||
{
|
||||
record.addWarning(new QWarningMessage("An error occurred after the update: " + e.getMessage()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
private static void runPreUpdateCustomizers(UpdateInput updateInput, QTableMetaData table, Optional<List<QRecord>> oldRecordList, boolean isPreview) throws QException
|
||||
{
|
||||
Optional<TableCustomizerInterface> preUpdateCustomizer = QCodeLoader.getTableCustomizer(table, TableCustomizers.PRE_UPDATE_RECORD.getRole());
|
||||
if(preUpdateCustomizer.isPresent())
|
||||
{
|
||||
updateInput.setRecords(preUpdateCustomizer.get().preUpdate(updateInput, updateInput.getRecords(), isPreview, oldRecordList));
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////
|
||||
// run all of the instance-level customizers //
|
||||
///////////////////////////////////////////////
|
||||
List<QCodeReference> tableCustomizerCodes = QContext.getQInstance().getTableCustomizers(TableCustomizers.PRE_UPDATE_RECORD);
|
||||
for(QCodeReference tableCustomizerCode : tableCustomizerCodes)
|
||||
{
|
||||
TableCustomizerInterface tableCustomizer = QCodeLoader.getAdHoc(TableCustomizerInterface.class, tableCustomizerCode);
|
||||
updateInput.setRecords(tableCustomizer.preUpdate(updateInput, updateInput.getRecords(), isPreview, oldRecordList));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -278,11 +333,7 @@ public class UpdateAction
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// after all validations, run the pre-update customizer, if there is one //
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
Optional<TableCustomizerInterface> preUpdateCustomizer = QCodeLoader.getTableCustomizer(table, TableCustomizers.PRE_UPDATE_RECORD.getRole());
|
||||
if(preUpdateCustomizer.isPresent())
|
||||
{
|
||||
updateInput.setRecords(preUpdateCustomizer.get().preUpdate(updateInput, updateInput.getRecords(), isPreview, oldRecordList));
|
||||
}
|
||||
runPreUpdateCustomizers(updateInput, table, oldRecordList, isPreview);
|
||||
}
|
||||
|
||||
|
||||
@ -405,7 +456,7 @@ public class UpdateAction
|
||||
QFieldType fieldType = table.getField(lock.getFieldName()).getType();
|
||||
Serializable lockValue = ValueUtils.getValueAsFieldType(fieldType, oldRecord.getValue(lock.getFieldName()));
|
||||
|
||||
List<QErrorMessage> errors = ValidateRecordSecurityLockHelper.validateRecordSecurityValue(table, lock, lockValue, fieldType, ValidateRecordSecurityLockHelper.Action.UPDATE, Collections.emptyMap());
|
||||
List<QErrorMessage> errors = ValidateRecordSecurityLockHelper.validateRecordSecurityValue(table, lock, lockValue, fieldType, ValidateRecordSecurityLockHelper.Action.UPDATE, Collections.emptyMap(), QContext.getQSession());
|
||||
if(CollectionUtils.nullSafeHasContents(errors))
|
||||
{
|
||||
errors.forEach(e -> record.addError(e));
|
||||
@ -554,6 +605,7 @@ public class UpdateAction
|
||||
{
|
||||
LOG.debug("Deleting associatedRecords", logPair("associatedTable", associatedTable.getName()), logPair("noOfRecords", queryOutput.getRecords().size()));
|
||||
DeleteInput deleteInput = new DeleteInput();
|
||||
deleteInput.setFlags(updateInput.getFlags());
|
||||
deleteInput.setTransaction(updateInput.getTransaction());
|
||||
deleteInput.setTableName(association.getAssociatedTableName());
|
||||
deleteInput.setPrimaryKeys(queryOutput.getRecords().stream().map(r -> r.getValue(associatedTable.getPrimaryKeyField())).collect(Collectors.toList()));
|
||||
@ -566,6 +618,7 @@ public class UpdateAction
|
||||
LOG.debug("Updating associatedRecords", logPair("associatedTable", associatedTable.getName()), logPair("noOfRecords", nextLevelUpdates.size()));
|
||||
UpdateInput nextLevelUpdateInput = new UpdateInput();
|
||||
nextLevelUpdateInput.setTransaction(updateInput.getTransaction());
|
||||
nextLevelUpdateInput.setFlags(updateInput.getFlags());
|
||||
nextLevelUpdateInput.setTableName(association.getAssociatedTableName());
|
||||
nextLevelUpdateInput.setRecords(nextLevelUpdates);
|
||||
UpdateOutput nextLevelUpdateOutput = new UpdateAction().execute(nextLevelUpdateInput);
|
||||
@ -576,6 +629,7 @@ public class UpdateAction
|
||||
LOG.debug("Inserting associatedRecords", logPair("associatedTable", associatedTable.getName()), logPair("noOfRecords", nextLevelUpdates.size()));
|
||||
InsertInput nextLevelInsertInput = new InsertInput();
|
||||
nextLevelInsertInput.setTransaction(updateInput.getTransaction());
|
||||
nextLevelInsertInput.setFlags(updateInput.getFlags());
|
||||
nextLevelInsertInput.setTableName(association.getAssociatedTableName());
|
||||
nextLevelInsertInput.setRecords(nextLevelInserts);
|
||||
InsertOutput nextLevelInsertOutput = new InsertAction().execute(nextLevelInsertInput);
|
||||
|
@ -50,6 +50,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.security.QSecurityKeyType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLockFilters;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.session.QSession;
|
||||
import com.kingsrook.qqq.backend.core.model.statusmessages.PermissionDeniedMessage;
|
||||
import com.kingsrook.qqq.backend.core.model.statusmessages.QErrorMessage;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
@ -102,7 +103,7 @@ public class ValidateRecordSecurityLockHelper
|
||||
// actually check lock values //
|
||||
////////////////////////////////
|
||||
Map<Serializable, RecordWithErrors> errorRecords = new HashMap<>();
|
||||
evaluateRecordLocks(table, records, action, locksToCheck, errorRecords, new ArrayList<>(), madeUpPrimaryKeys, transaction);
|
||||
evaluateRecordLocks(table, records, action, locksToCheck, errorRecords, new ArrayList<>(), madeUpPrimaryKeys, transaction, QContext.getQSession());
|
||||
|
||||
/////////////////////////////////
|
||||
// propagate errors to records //
|
||||
@ -124,6 +125,29 @@ public class ValidateRecordSecurityLockHelper
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** return boolean if given session can read given record
|
||||
***************************************************************************/
|
||||
public static boolean allowedToReadRecord(QTableMetaData table, QRecord record, QSession qSession, QBackendTransaction transaction) throws QException
|
||||
{
|
||||
MultiRecordSecurityLock locksToCheck = getRecordSecurityLocks(table, Action.SELECT);
|
||||
if(locksToCheck == null || CollectionUtils.nullSafeIsEmpty(locksToCheck.getLocks()))
|
||||
{
|
||||
return (true);
|
||||
}
|
||||
|
||||
Map<Serializable, RecordWithErrors> errorRecords = new HashMap<>();
|
||||
evaluateRecordLocks(table, List.of(record), Action.SELECT, locksToCheck, errorRecords, new ArrayList<>(), Collections.emptyMap(), transaction, qSession);
|
||||
if(errorRecords.containsKey(record.getValue(table.getPrimaryKeyField())))
|
||||
{
|
||||
return (false);
|
||||
}
|
||||
|
||||
return (true);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** For a list of `records` from a `table`, and a given `action`, evaluate a
|
||||
** `recordSecurityLock` (which may be a multi-lock) - populating the input map
|
||||
@ -142,7 +166,7 @@ public class ValidateRecordSecurityLockHelper
|
||||
** BUT - WRITE locks - in their case, we read the record no matter what, and in
|
||||
** here we need to verify we have a key that allows us to WRITE the record.
|
||||
*******************************************************************************/
|
||||
private static void evaluateRecordLocks(QTableMetaData table, List<QRecord> records, Action action, RecordSecurityLock recordSecurityLock, Map<Serializable, RecordWithErrors> errorRecords, List<Integer> treePosition, Map<Serializable, QRecord> madeUpPrimaryKeys, QBackendTransaction transaction) throws QException
|
||||
private static void evaluateRecordLocks(QTableMetaData table, List<QRecord> records, Action action, RecordSecurityLock recordSecurityLock, Map<Serializable, RecordWithErrors> errorRecords, List<Integer> treePosition, Map<Serializable, QRecord> madeUpPrimaryKeys, QBackendTransaction transaction, QSession qSession) throws QException
|
||||
{
|
||||
if(recordSecurityLock instanceof MultiRecordSecurityLock multiRecordSecurityLock)
|
||||
{
|
||||
@ -153,7 +177,7 @@ public class ValidateRecordSecurityLockHelper
|
||||
for(RecordSecurityLock childLock : CollectionUtils.nonNullList(multiRecordSecurityLock.getLocks()))
|
||||
{
|
||||
treePosition.add(i);
|
||||
evaluateRecordLocks(table, records, action, childLock, errorRecords, treePosition, madeUpPrimaryKeys, transaction);
|
||||
evaluateRecordLocks(table, records, action, childLock, errorRecords, treePosition, madeUpPrimaryKeys, transaction, qSession);
|
||||
treePosition.remove(treePosition.size() - 1);
|
||||
i++;
|
||||
}
|
||||
@ -165,7 +189,7 @@ public class ValidateRecordSecurityLockHelper
|
||||
// if this lock has an all-access key, and the user has that key, then there can't be any errors here, so return early //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
QSecurityKeyType securityKeyType = QContext.getQInstance().getSecurityKeyType(recordSecurityLock.getSecurityKeyType());
|
||||
if(StringUtils.hasContent(securityKeyType.getAllAccessKeyName()) && QContext.getQSession().hasSecurityKeyValue(securityKeyType.getAllAccessKeyName(), true, QFieldType.BOOLEAN))
|
||||
if(StringUtils.hasContent(securityKeyType.getAllAccessKeyName()) && qSession.hasSecurityKeyValue(securityKeyType.getAllAccessKeyName(), true, QFieldType.BOOLEAN))
|
||||
{
|
||||
return;
|
||||
}
|
||||
@ -193,7 +217,7 @@ public class ValidateRecordSecurityLockHelper
|
||||
}
|
||||
|
||||
Serializable recordSecurityValue = record.getValue(field.getName());
|
||||
List<QErrorMessage> recordErrors = validateRecordSecurityValue(table, recordSecurityLock, recordSecurityValue, field.getType(), action, madeUpPrimaryKeys);
|
||||
List<QErrorMessage> recordErrors = validateRecordSecurityValue(table, recordSecurityLock, recordSecurityValue, field.getType(), action, madeUpPrimaryKeys, qSession);
|
||||
if(CollectionUtils.nullSafeHasContents(recordErrors))
|
||||
{
|
||||
errorRecords.computeIfAbsent(record.getValue(primaryKeyField), (k) -> new RecordWithErrors(record)).addAll(recordErrors, treePosition);
|
||||
@ -339,7 +363,7 @@ public class ValidateRecordSecurityLockHelper
|
||||
|
||||
for(QRecord inputRecord : inputRecords)
|
||||
{
|
||||
List<QErrorMessage> recordErrors = validateRecordSecurityValue(table, recordSecurityLock, recordSecurityValue, field.getType(), action, madeUpPrimaryKeys);
|
||||
List<QErrorMessage> recordErrors = validateRecordSecurityValue(table, recordSecurityLock, recordSecurityValue, field.getType(), action, madeUpPrimaryKeys, qSession);
|
||||
if(CollectionUtils.nullSafeHasContents(recordErrors))
|
||||
{
|
||||
errorRecords.computeIfAbsent(inputRecord.getValue(primaryKeyField), (k) -> new RecordWithErrors(inputRecord)).addAll(recordErrors, treePosition);
|
||||
@ -446,7 +470,7 @@ public class ValidateRecordSecurityLockHelper
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static List<QErrorMessage> validateRecordSecurityValue(QTableMetaData table, RecordSecurityLock recordSecurityLock, Serializable recordSecurityValue, QFieldType fieldType, Action action, Map<Serializable, QRecord> madeUpPrimaryKeys)
|
||||
public static List<QErrorMessage> validateRecordSecurityValue(QTableMetaData table, RecordSecurityLock recordSecurityLock, Serializable recordSecurityValue, QFieldType fieldType, Action action, Map<Serializable, QRecord> madeUpPrimaryKeys, QSession qSession)
|
||||
{
|
||||
if(recordSecurityValue == null || (madeUpPrimaryKeys != null && madeUpPrimaryKeys.containsKey(recordSecurityValue)))
|
||||
{
|
||||
@ -461,7 +485,7 @@ public class ValidateRecordSecurityLockHelper
|
||||
}
|
||||
else
|
||||
{
|
||||
if(!QContext.getQSession().hasSecurityKeyValue(recordSecurityLock.getSecurityKeyType(), recordSecurityValue, fieldType))
|
||||
if(!qSession.hasSecurityKeyValue(recordSecurityLock.getSecurityKeyType(), recordSecurityValue, fieldType))
|
||||
{
|
||||
if(CollectionUtils.nullSafeHasContents(recordSecurityLock.getJoinNameChain()))
|
||||
{
|
||||
|
@ -47,12 +47,12 @@ public abstract class BasicCustomPossibleValueProvider<S, ID extends Serializabl
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
protected abstract S getSourceObject(Serializable id);
|
||||
protected abstract S getSourceObject(Serializable id) throws QException;
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
protected abstract List<S> getAllSourceObjects();
|
||||
protected abstract List<S> getAllSourceObjects() throws QException;
|
||||
|
||||
|
||||
|
||||
@ -60,7 +60,7 @@ public abstract class BasicCustomPossibleValueProvider<S, ID extends Serializabl
|
||||
**
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public QPossibleValue<ID> getPossibleValue(Serializable idValue)
|
||||
public QPossibleValue<ID> getPossibleValue(Serializable idValue) throws QException
|
||||
{
|
||||
S sourceObject = getSourceObject(idValue);
|
||||
if(sourceObject == null)
|
||||
|
@ -45,7 +45,7 @@ public interface QCustomPossibleValueProvider<T extends Serializable>
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
QPossibleValue<T> getPossibleValue(Serializable idValue);
|
||||
QPossibleValue<T> getPossibleValue(Serializable idValue) throws QException;
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
|
@ -45,9 +45,9 @@ import com.kingsrook.qqq.backend.core.actions.values.QCustomPossibleValueProvide
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.instances.enrichment.plugins.QInstanceEnricherPluginInterface;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.bulk.TableKeyFieldsPossibleValueSource;
|
||||
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.QSupplementalInstanceMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaDataInterface;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.AdornmentType;
|
||||
@ -211,6 +211,11 @@ public class QInstanceEnricher
|
||||
***************************************************************************/
|
||||
private void enrichInstance()
|
||||
{
|
||||
for(QSupplementalInstanceMetaData supplementalInstanceMetaData : qInstance.getSupplementalMetaData().values())
|
||||
{
|
||||
supplementalInstanceMetaData.enrich(qInstance);
|
||||
}
|
||||
|
||||
runPlugins(QInstance.class, qInstance, qInstance);
|
||||
}
|
||||
|
||||
@ -858,11 +863,6 @@ public class QInstanceEnricher
|
||||
*******************************************************************************/
|
||||
private void defineTableBulkProcesses(QInstance qInstance)
|
||||
{
|
||||
if(qInstance.getPossibleValueSource(TableKeyFieldsPossibleValueSource.NAME) == null)
|
||||
{
|
||||
qInstance.addPossibleValueSource(defineTableKeyFieldsPossibleValueSource());
|
||||
}
|
||||
|
||||
for(QTableMetaData table : qInstance.getTables().values())
|
||||
{
|
||||
if(table.getFields() == null)
|
||||
@ -886,12 +886,6 @@ public class QInstanceEnricher
|
||||
defineTableBulkEdit(qInstance, table, bulkEditProcessName);
|
||||
}
|
||||
|
||||
String bulkEditWithFileProcessName = table.getName() + ".bulkEditWithFile";
|
||||
if(qInstance.getProcess(bulkEditWithFileProcessName) == null)
|
||||
{
|
||||
defineTableBulkEditWithFile(qInstance, table, bulkEditWithFileProcessName);
|
||||
}
|
||||
|
||||
String bulkDeleteProcessName = table.getName() + ".bulkDelete";
|
||||
if(qInstance.getProcess(bulkDeleteProcessName) == null)
|
||||
{
|
||||
@ -996,16 +990,16 @@ public class QInstanceEnricher
|
||||
.withCode(new QCodeReference(BulkInsertReceiveValueMappingStep.class));
|
||||
|
||||
int i = 0;
|
||||
process.withStep(i++, prepareFileUploadStep);
|
||||
process.withStep(i++, uploadScreen);
|
||||
process.addStep(i++, prepareFileUploadStep);
|
||||
process.addStep(i++, uploadScreen);
|
||||
|
||||
process.withStep(i++, prepareFileMappingStep);
|
||||
process.withStep(i++, fileMappingScreen);
|
||||
process.withStep(i++, receiveFileMappingStep);
|
||||
process.addStep(i++, prepareFileMappingStep);
|
||||
process.addStep(i++, fileMappingScreen);
|
||||
process.addStep(i++, receiveFileMappingStep);
|
||||
|
||||
process.withStep(i++, prepareValueMappingStep);
|
||||
process.withStep(i++, valueMappingScreen);
|
||||
process.withStep(i++, receiveValueMappingStep);
|
||||
process.addStep(i++, prepareValueMappingStep);
|
||||
process.addStep(i++, valueMappingScreen);
|
||||
process.addStep(i++, receiveValueMappingStep);
|
||||
|
||||
process.getFrontendStep(StreamedETLWithFrontendProcess.STEP_NAME_REVIEW).setRecordListFields(editableFields);
|
||||
|
||||
@ -1072,122 +1066,6 @@ public class QInstanceEnricher
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void defineTableBulkEditWithFile(QInstance qInstance, QTableMetaData table, String processName)
|
||||
{
|
||||
Map<String, Serializable> values = new HashMap<>();
|
||||
values.put(StreamedETLWithFrontendProcess.FIELD_DESTINATION_TABLE, table.getName());
|
||||
values.put(StreamedETLWithFrontendProcess.FIELD_PREVIEW_MESSAGE, "This is a preview of the records that will be updated.");
|
||||
|
||||
QProcessMetaData process = StreamedETLWithFrontendProcess.defineProcessMetaData(
|
||||
BulkInsertExtractStep.class,
|
||||
BulkInsertTransformStep.class,
|
||||
BulkEditLoadStep.class,
|
||||
values
|
||||
)
|
||||
.withName(processName)
|
||||
.withLabel(table.getLabel() + " Bulk Edit With File")
|
||||
.withTableName(table.getName())
|
||||
.withIsHidden(true)
|
||||
.withPermissionRules(qInstance.getDefaultPermissionRules().clone()
|
||||
.withCustomPermissionChecker(new QCodeReference(BulkTableActionProcessPermissionChecker.class)));
|
||||
|
||||
List<QFieldMetaData> editableFields = table.getFields().values().stream()
|
||||
.filter(QFieldMetaData::getIsEditable)
|
||||
.filter(f -> !f.getType().equals(QFieldType.BLOB))
|
||||
.toList();
|
||||
|
||||
QBackendStepMetaData prepareFileUploadStep = new QBackendStepMetaData()
|
||||
.withName("prepareFileUpload")
|
||||
.withCode(new QCodeReference(BulkInsertPrepareFileUploadStep.class));
|
||||
|
||||
QFrontendStepMetaData uploadScreen = new QFrontendStepMetaData()
|
||||
.withName("upload")
|
||||
.withLabel("Upload File")
|
||||
.withFormField(new QFieldMetaData("theFile", QFieldType.BLOB)
|
||||
.withFieldAdornment(FileUploadAdornment.newFieldAdornment()
|
||||
.withValue(FileUploadAdornment.formatDragAndDrop())
|
||||
.withValue(FileUploadAdornment.widthFull()))
|
||||
.withLabel(table.getLabel() + " File")
|
||||
.withIsRequired(true))
|
||||
.withComponent(new QFrontendComponentMetaData().withType(QComponentType.HTML))
|
||||
.withComponent(new QFrontendComponentMetaData().withType(QComponentType.EDIT_FORM));
|
||||
|
||||
QBackendStepMetaData prepareFileMappingStep = new QBackendStepMetaData()
|
||||
.withName("prepareFileMapping")
|
||||
.withCode(new QCodeReference(BulkInsertPrepareFileMappingStep.class));
|
||||
|
||||
QFrontendStepMetaData fileMappingScreen = new QFrontendStepMetaData()
|
||||
.withName("fileMapping")
|
||||
.withLabel("File Mapping")
|
||||
.withBackStepName("prepareFileUpload")
|
||||
.withComponent(new QFrontendComponentMetaData().withType(QComponentType.BULK_LOAD_FILE_MAPPING_FORM))
|
||||
.withFormField(new QFieldMetaData("hasHeaderRow", QFieldType.BOOLEAN))
|
||||
.withFormField(new QFieldMetaData("layout", QFieldType.STRING)) // is actually PVS, but, this field is only added to help support helpContent, so :shrug:
|
||||
.withFormField(new QFieldMetaData("tableKeyFields", QFieldType.STRING).withPossibleValueSourceName(TableKeyFieldsPossibleValueSource.NAME));
|
||||
|
||||
QBackendStepMetaData receiveFileMappingStep = new QBackendStepMetaData()
|
||||
.withName("receiveFileMapping")
|
||||
.withCode(new QCodeReference(BulkInsertReceiveFileMappingStep.class));
|
||||
|
||||
QBackendStepMetaData prepareValueMappingStep = new QBackendStepMetaData()
|
||||
.withName("prepareValueMapping")
|
||||
.withCode(new QCodeReference(BulkInsertPrepareValueMappingStep.class));
|
||||
|
||||
QFrontendStepMetaData valueMappingScreen = new QFrontendStepMetaData()
|
||||
.withName("valueMapping")
|
||||
.withLabel("Value Mapping")
|
||||
.withBackStepName("prepareFileMapping")
|
||||
.withComponent(new QFrontendComponentMetaData().withType(QComponentType.BULK_LOAD_VALUE_MAPPING_FORM));
|
||||
|
||||
QBackendStepMetaData receiveValueMappingStep = new QBackendStepMetaData()
|
||||
.withName("receiveValueMapping")
|
||||
.withCode(new QCodeReference(BulkInsertReceiveValueMappingStep.class));
|
||||
|
||||
int i = 0;
|
||||
process.withStep(i++, prepareFileUploadStep);
|
||||
process.withStep(i++, uploadScreen);
|
||||
|
||||
process.withStep(i++, prepareFileMappingStep);
|
||||
process.withStep(i++, fileMappingScreen);
|
||||
process.withStep(i++, receiveFileMappingStep);
|
||||
|
||||
process.withStep(i++, prepareValueMappingStep);
|
||||
process.withStep(i++, valueMappingScreen);
|
||||
process.withStep(i++, receiveValueMappingStep);
|
||||
|
||||
process.getFrontendStep(StreamedETLWithFrontendProcess.STEP_NAME_REVIEW).setRecordListFields(editableFields);
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////
|
||||
// put the bulk-load profile form (e.g., for saving it) on the review & result screens) //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////
|
||||
process.getFrontendStep(StreamedETLWithFrontendProcess.STEP_NAME_REVIEW)
|
||||
.withBackStepName("prepareFileMapping")
|
||||
.getComponents().add(0, new QFrontendComponentMetaData().withType(QComponentType.BULK_LOAD_PROFILE_FORM));
|
||||
|
||||
process.getFrontendStep(StreamedETLWithFrontendProcess.STEP_NAME_RESULT)
|
||||
.getComponents().add(0, new QFrontendComponentMetaData().withType(QComponentType.BULK_LOAD_PROFILE_FORM));
|
||||
|
||||
qInstance.addProcess(process);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private QPossibleValueSource defineTableKeyFieldsPossibleValueSource()
|
||||
{
|
||||
return (new QPossibleValueSource()
|
||||
.withName(TableKeyFieldsPossibleValueSource.NAME)
|
||||
.withType(QPossibleValueSourceType.CUSTOM)
|
||||
.withCustomCodeReference(new QCodeReference(TableKeyFieldsPossibleValueSource.class)));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@ -1532,7 +1410,7 @@ public class QInstanceEnricher
|
||||
if(possibleValueSource.getIdType() == null)
|
||||
{
|
||||
QTableMetaData table = qInstance.getTable(possibleValueSource.getTableName());
|
||||
if(table != null)
|
||||
if(table != null && table.getFields() != null)
|
||||
{
|
||||
String primaryKeyField = table.getPrimaryKeyField();
|
||||
QFieldMetaData primaryKeyFieldMetaData = table.getFields().get(primaryKeyField);
|
||||
@ -1605,8 +1483,19 @@ public class QInstanceEnricher
|
||||
if(enrichMethod.isPresent())
|
||||
{
|
||||
Class<?> parameterType = enrichMethod.get().getParameterTypes()[0];
|
||||
|
||||
Set<String> existingPluginIdentifiers = enricherPlugins.getOrDefault(parameterType, Collections.emptyList())
|
||||
.stream().map(p -> p.getPluginIdentifier())
|
||||
.collect(Collectors.toSet());
|
||||
if(existingPluginIdentifiers.contains(plugin.getPluginIdentifier()))
|
||||
{
|
||||
LOG.debug("Enricher plugin is already registered - not re-adding it", logPair("pluginIdentifer", plugin.getPluginIdentifier()));
|
||||
}
|
||||
else
|
||||
{
|
||||
enricherPlugins.add(parameterType, plugin);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG.warn("Could not find enrich method on enricher plugin [" + plugin.getClass().getName() + "] (to infer type being enriched) - this plugin will not be used.");
|
||||
|
@ -42,6 +42,7 @@ import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationHandler;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizerInterface;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
|
||||
import com.kingsrook.qqq.backend.core.actions.dashboard.widgets.AbstractWidgetRenderer;
|
||||
import com.kingsrook.qqq.backend.core.actions.metadata.JoinGraph;
|
||||
@ -252,6 +253,17 @@ public class QInstanceValidator
|
||||
{
|
||||
validateSimpleCodeReference("Instance metaDataActionCustomizer ", qInstance.getMetaDataActionCustomizer(), MetaDataActionCustomizerInterface.class);
|
||||
}
|
||||
|
||||
if(qInstance.getTableCustomizers() != null)
|
||||
{
|
||||
for(Map.Entry<String, List<QCodeReference>> entry : qInstance.getTableCustomizers().entrySet())
|
||||
{
|
||||
for(QCodeReference codeReference : CollectionUtils.nonNullList(entry.getValue()))
|
||||
{
|
||||
validateSimpleCodeReference("Instance tableCustomizer of type " + entry.getKey() + ": ", codeReference, TableCustomizerInterface.class);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -283,8 +295,19 @@ public class QInstanceValidator
|
||||
if(validateMethod.isPresent())
|
||||
{
|
||||
Class<?> parameterType = validateMethod.get().getParameterTypes()[0];
|
||||
|
||||
Set<String> existingPluginIdentifiers = validatorPlugins.getOrDefault(parameterType, Collections.emptyList())
|
||||
.stream().map(p -> p.getPluginIdentifier())
|
||||
.collect(Collectors.toSet());
|
||||
if(existingPluginIdentifiers.contains(plugin.getPluginIdentifier()))
|
||||
{
|
||||
LOG.debug("Validator plugin is already registered - not re-adding it", logPair("pluginIdentifer", plugin.getPluginIdentifier()));
|
||||
}
|
||||
else
|
||||
{
|
||||
validatorPlugins.add(parameterType, plugin);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG.warn("Could not find validate method on validator plugin [" + plugin.getClass().getName() + "] (to infer type being validated) - this plugin will not be used.");
|
||||
@ -303,6 +326,17 @@ public class QInstanceValidator
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for validatorPlugins
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static ListingHash<Class<?>, QInstanceValidatorPluginInterface<?>> getValidatorPlugins()
|
||||
{
|
||||
return validatorPlugins;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@ -2238,8 +2272,7 @@ public class QInstanceValidator
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@SafeVarargs
|
||||
private void validateSimpleCodeReference(String prefix, QCodeReference codeReference, Class<?>... anyOfExpectedClasses)
|
||||
public void validateSimpleCodeReference(String prefix, QCodeReference codeReference, Class<?>... anyOfExpectedClasses)
|
||||
{
|
||||
if(!preAssertionsForCodeReference(codeReference, prefix))
|
||||
{
|
||||
|
@ -0,0 +1,37 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.instances.assessment;
|
||||
|
||||
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** marker for an object which can be processed by the QInstanceAssessor.
|
||||
*******************************************************************************/
|
||||
public interface Assessable
|
||||
{
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
void assess(QInstanceAssessor qInstanceAssessor, QInstance qInstance);
|
||||
}
|
@ -0,0 +1,219 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.instances.assessment;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** POC of a class that is meant to review meta-data for accuracy vs. real backends.
|
||||
*******************************************************************************/
|
||||
public class QInstanceAssessor
|
||||
{
|
||||
private static final QLogger LOG = QLogger.getLogger(QInstanceAssessor.class);
|
||||
|
||||
private final QInstance qInstance;
|
||||
|
||||
private List<String> errors = new ArrayList<>();
|
||||
private List<String> warnings = new ArrayList<>();
|
||||
private List<String> suggestions = new ArrayList<>();
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor
|
||||
**
|
||||
*******************************************************************************/
|
||||
public QInstanceAssessor(QInstance qInstance)
|
||||
{
|
||||
this.qInstance = qInstance;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void assess()
|
||||
{
|
||||
for(QBackendMetaData backend : qInstance.getBackends().values())
|
||||
{
|
||||
if(backend instanceof Assessable assessable)
|
||||
{
|
||||
assessable.assess(this, qInstance);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@SuppressWarnings("checkstyle:AvoidEscapedUnicodeCharacters")
|
||||
public String getSummary()
|
||||
{
|
||||
StringBuilder rs = new StringBuilder();
|
||||
|
||||
///////////////////////////
|
||||
// print header & errors //
|
||||
///////////////////////////
|
||||
if(CollectionUtils.nullSafeIsEmpty(errors))
|
||||
{
|
||||
rs.append("Assessment passed with no errors! \uD83D\uDE0E\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
rs.append("Assessment found the following ").append(StringUtils.plural(errors, "error", "errors")).append(": \uD83D\uDE32\n");
|
||||
|
||||
for(String error : errors)
|
||||
{
|
||||
rs.append(" - ").append(error).append("\n");
|
||||
}
|
||||
}
|
||||
|
||||
/////////////////////////////////////
|
||||
// print warnings if there are any //
|
||||
/////////////////////////////////////
|
||||
if(CollectionUtils.nullSafeHasContents(warnings))
|
||||
{
|
||||
rs.append("\nAssessment found the following ").append(StringUtils.plural(warnings, "warning", "warnings")).append(": \uD83E\uDD28\n");
|
||||
|
||||
for(String warning : warnings)
|
||||
{
|
||||
rs.append(" - ").append(warning).append("\n");
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////////////////////////
|
||||
// print suggestions, if there were any //
|
||||
//////////////////////////////////////////
|
||||
if(CollectionUtils.nullSafeHasContents(suggestions))
|
||||
{
|
||||
rs.append("\nThe following ").append(StringUtils.plural(suggestions, "fix is", "fixes are")).append(" suggested: \uD83E\uDD13\n");
|
||||
|
||||
for(String suggestion : suggestions)
|
||||
{
|
||||
rs.append("\n").append(suggestion).append("\n\n");
|
||||
}
|
||||
}
|
||||
|
||||
return (rs.toString());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for qInstance
|
||||
**
|
||||
*******************************************************************************/
|
||||
public QInstance getInstance()
|
||||
{
|
||||
return qInstance;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for errors
|
||||
**
|
||||
*******************************************************************************/
|
||||
public List<String> getErrors()
|
||||
{
|
||||
return errors;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for warnings
|
||||
**
|
||||
*******************************************************************************/
|
||||
public List<String> getWarnings()
|
||||
{
|
||||
return warnings;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void addError(String errorMessage)
|
||||
{
|
||||
errors.add(errorMessage);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void addWarning(String warningMessage)
|
||||
{
|
||||
warnings.add(warningMessage);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void addError(String errorMessage, Exception e)
|
||||
{
|
||||
addError(errorMessage + " : " + e.getMessage());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void addSuggestion(String message)
|
||||
{
|
||||
suggestions.add(message);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public int getExitCode()
|
||||
{
|
||||
if(CollectionUtils.nullSafeHasContents(errors))
|
||||
{
|
||||
return (1);
|
||||
}
|
||||
else
|
||||
{
|
||||
return (0);
|
||||
}
|
||||
}
|
||||
}
|
@ -37,4 +37,13 @@ public interface QInstanceEnricherPluginInterface<T>
|
||||
*******************************************************************************/
|
||||
void enrich(T object, QInstance qInstance);
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
default String getPluginIdentifier()
|
||||
{
|
||||
return getClass().getName();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -38,4 +38,13 @@ public interface QInstanceValidatorPluginInterface<T>
|
||||
*******************************************************************************/
|
||||
void validate(T object, QInstance qInstance, QInstanceValidator qInstanceValidator);
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
default String getPluginIdentifier()
|
||||
{
|
||||
return getClass().getName();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -23,7 +23,11 @@ package com.kingsrook.qqq.backend.core.model.actions.processes;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import com.kingsrook.qqq.backend.core.logging.LogPair;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -31,6 +35,45 @@ import com.kingsrook.qqq.backend.core.logging.LogPair;
|
||||
*******************************************************************************/
|
||||
public interface ProcessSummaryLineInterface extends Serializable
|
||||
{
|
||||
QLogger LOG = QLogger.getLogger(ProcessSummaryLineInterface.class);
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
static void log(String message, Serializable summaryLines, List<LogPair> additionalLogPairs)
|
||||
{
|
||||
try
|
||||
{
|
||||
if(summaryLines instanceof List)
|
||||
{
|
||||
List<ProcessSummaryLineInterface> list = (List<ProcessSummaryLineInterface>) summaryLines;
|
||||
|
||||
List<LogPair> logPairs = new ArrayList<>();
|
||||
for(ProcessSummaryLineInterface processSummaryLineInterface : list)
|
||||
{
|
||||
LogPair logPair = processSummaryLineInterface.toLogPair();
|
||||
logPair.setKey(logPair.getKey() + logPairs.size());
|
||||
logPairs.add(logPair);
|
||||
}
|
||||
|
||||
if(additionalLogPairs != null)
|
||||
{
|
||||
logPairs.addAll(0, additionalLogPairs);
|
||||
}
|
||||
logPairs.add(0, logPair("message", message));
|
||||
|
||||
LOG.info(logPairs);
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG.info("Unrecognized type for summaryLines (expected List)", logPair("processSummary", summaryLines));
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOG.info("Error logging a process summary", e, logPair("processSummary", summaryLines));
|
||||
}
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for status
|
||||
|
@ -0,0 +1,35 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.model.actions.tables;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** interface to mark enums (presumably classes too, but the original intent is
|
||||
** enums) that can be added to insert/update/delete action inputs to flag behaviors
|
||||
*******************************************************************************/
|
||||
public interface ActionFlag extends Serializable
|
||||
{
|
||||
|
||||
}
|
@ -24,9 +24,12 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.delete;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.ActionFlag;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.InputSource;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.QInputSource;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
@ -47,6 +50,8 @@ public class DeleteInput extends AbstractTableActionInput
|
||||
private boolean omitDmlAudit = false;
|
||||
private String auditContext = null;
|
||||
|
||||
private Set<ActionFlag> flags;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -295,4 +300,65 @@ public class DeleteInput extends AbstractTableActionInput
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for flags
|
||||
*******************************************************************************/
|
||||
public Set<ActionFlag> getFlags()
|
||||
{
|
||||
return (this.flags);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for flags
|
||||
*******************************************************************************/
|
||||
public void setFlags(Set<ActionFlag> flags)
|
||||
{
|
||||
this.flags = flags;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for flags
|
||||
*******************************************************************************/
|
||||
public DeleteInput withFlags(Set<ActionFlag> flags)
|
||||
{
|
||||
this.flags = flags;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
public DeleteInput withFlag(ActionFlag flag)
|
||||
{
|
||||
if(this.flags == null)
|
||||
{
|
||||
this.flags = new HashSet<>();
|
||||
}
|
||||
this.flags.add(flag);
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
public boolean hasFlag(ActionFlag flag)
|
||||
{
|
||||
if(this.flags == null)
|
||||
{
|
||||
return (false);
|
||||
}
|
||||
|
||||
return (this.flags.contains(flag));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -23,9 +23,12 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.insert;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.ActionFlag;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.InputSource;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.QInputSource;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
@ -48,6 +51,8 @@ public class InsertInput extends AbstractTableActionInput
|
||||
private boolean omitDmlAudit = false;
|
||||
private String auditContext = null;
|
||||
|
||||
private Set<ActionFlag> flags;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -316,4 +321,65 @@ public class InsertInput extends AbstractTableActionInput
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for flags
|
||||
*******************************************************************************/
|
||||
public Set<ActionFlag> getFlags()
|
||||
{
|
||||
return (this.flags);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for flags
|
||||
*******************************************************************************/
|
||||
public void setFlags(Set<ActionFlag> flags)
|
||||
{
|
||||
this.flags = flags;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for flags
|
||||
*******************************************************************************/
|
||||
public InsertInput withFlags(Set<ActionFlag> flags)
|
||||
{
|
||||
this.flags = flags;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
public InsertInput withFlag(ActionFlag flag)
|
||||
{
|
||||
if(this.flags == null)
|
||||
{
|
||||
this.flags = new HashSet<>();
|
||||
}
|
||||
this.flags.add(flag);
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
public boolean hasFlag(ActionFlag flag)
|
||||
{
|
||||
if(this.flags == null)
|
||||
{
|
||||
return (false);
|
||||
}
|
||||
|
||||
return (this.flags.contains(flag));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -22,9 +22,12 @@
|
||||
package com.kingsrook.qqq.backend.core.model.actions.tables.replace;
|
||||
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.ActionFlag;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey;
|
||||
@ -45,6 +48,8 @@ public class ReplaceInput extends AbstractTableActionInput
|
||||
|
||||
private boolean omitDmlAudit = false;
|
||||
|
||||
private Set<ActionFlag> flags;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -303,4 +308,65 @@ public class ReplaceInput extends AbstractTableActionInput
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for flags
|
||||
*******************************************************************************/
|
||||
public Set<ActionFlag> getFlags()
|
||||
{
|
||||
return (this.flags);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for flags
|
||||
*******************************************************************************/
|
||||
public void setFlags(Set<ActionFlag> flags)
|
||||
{
|
||||
this.flags = flags;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for flags
|
||||
*******************************************************************************/
|
||||
public ReplaceInput withFlags(Set<ActionFlag> flags)
|
||||
{
|
||||
this.flags = flags;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
public ReplaceInput withFlag(ActionFlag flag)
|
||||
{
|
||||
if(this.flags == null)
|
||||
{
|
||||
this.flags = new HashSet<>();
|
||||
}
|
||||
this.flags.add(flag);
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
public boolean hasFlag(ActionFlag flag)
|
||||
{
|
||||
if(this.flags == null)
|
||||
{
|
||||
return (false);
|
||||
}
|
||||
|
||||
return (this.flags.contains(flag));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -23,9 +23,12 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.update;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.ActionFlag;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.InputSource;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.QInputSource;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
@ -56,6 +59,8 @@ public class UpdateInput extends AbstractTableActionInput
|
||||
private boolean omitModifyDateUpdate = false;
|
||||
private String auditContext = null;
|
||||
|
||||
private Set<ActionFlag> flags;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -385,4 +390,65 @@ public class UpdateInput extends AbstractTableActionInput
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for flags
|
||||
*******************************************************************************/
|
||||
public Set<ActionFlag> getFlags()
|
||||
{
|
||||
return (this.flags);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for flags
|
||||
*******************************************************************************/
|
||||
public void setFlags(Set<ActionFlag> flags)
|
||||
{
|
||||
this.flags = flags;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for flags
|
||||
*******************************************************************************/
|
||||
public UpdateInput withFlags(Set<ActionFlag> flags)
|
||||
{
|
||||
this.flags = flags;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
public UpdateInput withFlag(ActionFlag flag)
|
||||
{
|
||||
if(this.flags == null)
|
||||
{
|
||||
this.flags = new HashSet<>();
|
||||
}
|
||||
this.flags.add(flag);
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
public boolean hasFlag(ActionFlag flag)
|
||||
{
|
||||
if(this.flags == null)
|
||||
{
|
||||
return (false);
|
||||
}
|
||||
|
||||
return (this.flags.contains(flag));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -25,7 +25,6 @@ package com.kingsrook.qqq.backend.core.model.actions.values;
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
|
||||
@ -40,8 +39,6 @@ public class SearchPossibleValueSourceInput extends AbstractActionInput implemen
|
||||
private String searchTerm;
|
||||
private List<Serializable> idList;
|
||||
private List<String> labelList;
|
||||
private Map<String, String> pathParamMap;
|
||||
private Map<String, List<String>> queryParamMap;
|
||||
|
||||
private Integer skip = 0;
|
||||
private Integer limit = 250;
|
||||
@ -287,7 +284,6 @@ public class SearchPossibleValueSourceInput extends AbstractActionInput implemen
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for labelList
|
||||
*******************************************************************************/
|
||||
@ -317,66 +313,4 @@ public class SearchPossibleValueSourceInput extends AbstractActionInput implemen
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for pathParamMap
|
||||
*******************************************************************************/
|
||||
public Map<String, String> getPathParamMap()
|
||||
{
|
||||
return (this.pathParamMap);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for pathParamMap
|
||||
*******************************************************************************/
|
||||
public void setPathParamMap(Map<String, String> pathParamMap)
|
||||
{
|
||||
this.pathParamMap = pathParamMap;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for pathParamMap
|
||||
*******************************************************************************/
|
||||
public SearchPossibleValueSourceInput withPathParamMap(Map<String, String> pathParamMap)
|
||||
{
|
||||
this.pathParamMap = pathParamMap;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for queryParamMap
|
||||
*******************************************************************************/
|
||||
public Map<String, List<String>> getQueryParamMap()
|
||||
{
|
||||
return (this.queryParamMap);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for queryParamMap
|
||||
*******************************************************************************/
|
||||
public void setQueryParamMap(Map<String, List<String>> queryParamMap)
|
||||
{
|
||||
this.queryParamMap = queryParamMap;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for queryParamMap
|
||||
*******************************************************************************/
|
||||
public SearchPossibleValueSourceInput withQueryParamMap(Map<String, List<String>> queryParamMap)
|
||||
{
|
||||
this.queryParamMap = queryParamMap;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,153 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2024. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.model.bulk;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import com.kingsrook.qqq.backend.core.actions.values.QCustomPossibleValueProvider;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.values.SearchPossibleValueSourceInput;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValue;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class TableKeyFieldsPossibleValueSource implements QCustomPossibleValueProvider<String>
|
||||
{
|
||||
public static final String NAME = "tableKeyFields";
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public QPossibleValue<String> getPossibleValue(Serializable tableAndKey)
|
||||
{
|
||||
QPossibleValue<String> possibleValue = null;
|
||||
|
||||
/////////////////////////////////////////////////////////////
|
||||
// keys are in the format <tableName>-<key1>|<key2>|<key3> //
|
||||
/////////////////////////////////////////////////////////////
|
||||
String[] keyParts = tableAndKey.toString().split("-");
|
||||
String tableName = keyParts[0];
|
||||
String key = keyParts[1];
|
||||
|
||||
QTableMetaData table = QContext.getQInstance().getTable(tableName);
|
||||
if(table.getPrimaryKeyField().equals(key))
|
||||
{
|
||||
String id = table.getPrimaryKeyField();
|
||||
String label = table.getField(table.getPrimaryKeyField()).getLabel();
|
||||
possibleValue = new QPossibleValue<>(id, label);
|
||||
}
|
||||
else
|
||||
{
|
||||
for(UniqueKey uniqueKey : table.getUniqueKeys())
|
||||
{
|
||||
String potentialMatch = getIdFromUniqueKey(uniqueKey);
|
||||
if(potentialMatch.equals(key))
|
||||
{
|
||||
String id = potentialMatch;
|
||||
String label = getLabelFromUniqueKey(table, uniqueKey);
|
||||
possibleValue = new QPossibleValue<>(id, label);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (possibleValue);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public List<QPossibleValue<String>> search(SearchPossibleValueSourceInput input) throws QException
|
||||
{
|
||||
List<QPossibleValue<String>> rs = new ArrayList<>();
|
||||
if(!CollectionUtils.nonNullMap(input.getPathParamMap()).containsKey("processName") || input.getPathParamMap().get("processName") == null || input.getPathParamMap().get("processName").isEmpty())
|
||||
{
|
||||
throw (new QException("Path Param of processName was not found."));
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////
|
||||
// process name will be like tnt.bulkEditWithFile //
|
||||
////////////////////////////////////////////////////
|
||||
String processName = input.getPathParamMap().get("processName");
|
||||
String tableName = processName.split("\\.")[0];
|
||||
|
||||
QTableMetaData table = QContext.getQInstance().getTable(tableName);
|
||||
for(UniqueKey uniqueKey : CollectionUtils.nonNullList(table.getUniqueKeys()))
|
||||
{
|
||||
String id = getIdFromUniqueKey(uniqueKey);
|
||||
String label = getLabelFromUniqueKey(table, uniqueKey);
|
||||
if(!StringUtils.hasContent(input.getSearchTerm()) || input.getSearchTerm().equals(id))
|
||||
{
|
||||
rs.add(new QPossibleValue<>(id, label));
|
||||
}
|
||||
}
|
||||
rs.sort(Comparator.comparing(QPossibleValue::getLabel));
|
||||
|
||||
///////////////////////////////
|
||||
// put the primary key first //
|
||||
///////////////////////////////
|
||||
if(!StringUtils.hasContent(input.getSearchTerm()) || input.getSearchTerm().equals(table.getPrimaryKeyField()))
|
||||
{
|
||||
rs.add(0, new QPossibleValue<>(table.getPrimaryKeyField(), table.getField(table.getPrimaryKeyField()).getLabel()));
|
||||
}
|
||||
|
||||
return rs;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private String getIdFromUniqueKey(UniqueKey uniqueKey)
|
||||
{
|
||||
return (StringUtils.join("|", uniqueKey.getFieldNames()));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private String getLabelFromUniqueKey(QTableMetaData tableMetaData, UniqueKey uniqueKey)
|
||||
{
|
||||
List<String> fieldLabels = new ArrayList<>(uniqueKey.getFieldNames().stream().map(f -> tableMetaData.getField(f).getLabel()).toList());
|
||||
fieldLabels.sort(Comparator.naturalOrder());
|
||||
return (StringUtils.joinWithCommasAndAnd(fieldLabels));
|
||||
}
|
||||
}
|
@ -177,6 +177,18 @@ public class MetaDataProducerHelper
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// sort them by sort order, then by the type that they return, as set up in the static map //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||
sortMetaDataProducers(producers);
|
||||
|
||||
return (producers);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
public static void sortMetaDataProducers(List<MetaDataProducerInterface<?>> producers)
|
||||
{
|
||||
producers.sort(Comparator
|
||||
.comparing((MetaDataProducerInterface<?> p) -> p.getSortOrder())
|
||||
.thenComparing((MetaDataProducerInterface<?> p) ->
|
||||
@ -191,11 +203,10 @@ public class MetaDataProducerHelper
|
||||
return (0);
|
||||
}
|
||||
}));
|
||||
|
||||
return (producers);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Recursively find all classes in the given package, that implement MetaDataProducerInterface
|
||||
** run them, and add their output to the given qInstance.
|
||||
@ -417,7 +428,7 @@ public class MetaDataProducerHelper
|
||||
return (null);
|
||||
}
|
||||
|
||||
ChildJoinFromRecordEntityGenericMetaDataProducer producer = new ChildJoinFromRecordEntityGenericMetaDataProducer(childTableName, parentTableName, possibleValueFieldName, childTable.childJoin().orderBy());
|
||||
ChildJoinFromRecordEntityGenericMetaDataProducer producer = new ChildJoinFromRecordEntityGenericMetaDataProducer(childTableName, parentTableName, possibleValueFieldName, childTable.childJoin().orderBy(), childTable.childJoin().isOneToOne());
|
||||
producer.setSourceClass(entityClass);
|
||||
return producer;
|
||||
}
|
||||
|
@ -86,7 +86,7 @@ public class MetaDataProducerMultiOutput implements MetaDataProducerOutput, Sour
|
||||
{
|
||||
List<T> rs = new ArrayList<>();
|
||||
|
||||
for(MetaDataProducerOutput content : contents)
|
||||
for(MetaDataProducerOutput content : CollectionUtils.nonNullList(contents))
|
||||
{
|
||||
if(content instanceof MetaDataProducerMultiOutput multiOutput)
|
||||
{
|
||||
|
@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.core.model.metadata;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
@ -30,6 +31,7 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
|
||||
import com.kingsrook.qqq.backend.core.actions.metadata.JoinGraph;
|
||||
import com.kingsrook.qqq.backend.core.actions.metadata.MetaDataAction;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
@ -65,6 +67,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.security.QSecurityKeyType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.scheduler.schedulable.SchedulableType;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.ListingHash;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
import io.github.cdimascio.dotenv.Dotenv;
|
||||
import io.github.cdimascio.dotenv.DotenvEntry;
|
||||
@ -116,6 +119,8 @@ public class QInstance
|
||||
private QPermissionRules defaultPermissionRules = QPermissionRules.defaultInstance();
|
||||
private QAuditRules defaultAuditRules = QAuditRules.defaultInstanceLevelNone();
|
||||
|
||||
private ListingHash<String, QCodeReference> tableCustomizers;
|
||||
|
||||
@Deprecated(since = "migrated to metaDataCustomizer")
|
||||
private QCodeReference metaDataFilter = null;
|
||||
|
||||
@ -1623,4 +1628,76 @@ public class QInstance
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for tableCustomizers
|
||||
*******************************************************************************/
|
||||
public ListingHash<String, QCodeReference> getTableCustomizers()
|
||||
{
|
||||
return (this.tableCustomizers);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for tableCustomizers
|
||||
*******************************************************************************/
|
||||
public void setTableCustomizers(ListingHash<String, QCodeReference> tableCustomizers)
|
||||
{
|
||||
this.tableCustomizers = tableCustomizers;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for tableCustomizers
|
||||
*******************************************************************************/
|
||||
public QInstance withTableCustomizers(ListingHash<String, QCodeReference> tableCustomizers)
|
||||
{
|
||||
this.tableCustomizers = tableCustomizers;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public QInstance withTableCustomizer(String role, QCodeReference customizer)
|
||||
{
|
||||
if(this.tableCustomizers == null)
|
||||
{
|
||||
this.tableCustomizers = new ListingHash<>();
|
||||
}
|
||||
|
||||
this.tableCustomizers.add(role, customizer);
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public QInstance withTableCustomizer(TableCustomizers tableCustomizer, QCodeReference customizer)
|
||||
{
|
||||
return (withTableCustomizer(tableCustomizer.getRole(), customizer));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for tableCustomizers
|
||||
*******************************************************************************/
|
||||
public List<QCodeReference> getTableCustomizers(TableCustomizers tableCustomizer)
|
||||
{
|
||||
if(this.tableCustomizers == null)
|
||||
{
|
||||
return (Collections.emptyList());
|
||||
}
|
||||
|
||||
return (this.tableCustomizers.getOrDefault(tableCustomizer.getRole(), Collections.emptyList()));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -24,7 +24,6 @@ package com.kingsrook.qqq.backend.core.model.metadata;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
import com.kingsrook.qqq.backend.core.instances.QInstanceValidator;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -37,7 +36,7 @@ public interface QSupplementalInstanceMetaData extends TopLevelMetaDataInterface
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
default void enrich(QTableMetaData table)
|
||||
default void enrich(QInstance qInstance)
|
||||
{
|
||||
////////////////////////
|
||||
// noop in base class //
|
||||
|
@ -26,12 +26,15 @@ import java.util.Objects;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerInterface;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerMultiOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.producers.annotations.ChildJoin;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.qbits.QBitProductionContext;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -62,6 +65,7 @@ public class ChildJoinFromRecordEntityGenericMetaDataProducer implements MetaDat
|
||||
private String childTableName; // e.g., lineItem
|
||||
private String parentTableName; // e.g., order
|
||||
private String foreignKeyFieldName; // e.g., orderId
|
||||
private boolean isOneToOne;
|
||||
|
||||
private ChildJoin.OrderBy[] orderBys;
|
||||
|
||||
@ -72,7 +76,7 @@ public class ChildJoinFromRecordEntityGenericMetaDataProducer implements MetaDat
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
public ChildJoinFromRecordEntityGenericMetaDataProducer(String childTableName, String parentTableName, String foreignKeyFieldName, ChildJoin.OrderBy[] orderBys)
|
||||
public ChildJoinFromRecordEntityGenericMetaDataProducer(String childTableName, String parentTableName, String foreignKeyFieldName, ChildJoin.OrderBy[] orderBys, boolean isOneToOne)
|
||||
{
|
||||
Objects.requireNonNull(childTableName, "childTableName cannot be null");
|
||||
Objects.requireNonNull(parentTableName, "parentTableName cannot be null");
|
||||
@ -82,6 +86,7 @@ public class ChildJoinFromRecordEntityGenericMetaDataProducer implements MetaDat
|
||||
this.parentTableName = parentTableName;
|
||||
this.foreignKeyFieldName = foreignKeyFieldName;
|
||||
this.orderBys = orderBys;
|
||||
this.isOneToOne = isOneToOne;
|
||||
}
|
||||
|
||||
|
||||
@ -92,23 +97,14 @@ public class ChildJoinFromRecordEntityGenericMetaDataProducer implements MetaDat
|
||||
@Override
|
||||
public QJoinMetaData produce(QInstance qInstance) throws QException
|
||||
{
|
||||
QTableMetaData parentTable = qInstance.getTable(parentTableName);
|
||||
if(parentTable == null)
|
||||
{
|
||||
throw (new QException("Could not find tableMetaData " + parentTableName));
|
||||
}
|
||||
|
||||
QTableMetaData childTable = qInstance.getTable(childTableName);
|
||||
if(childTable == null)
|
||||
{
|
||||
throw (new QException("Could not find tableMetaData " + childTable));
|
||||
}
|
||||
QTableMetaData parentTable = getTable(qInstance, parentTableName);
|
||||
QTableMetaData childTable = getTable(qInstance, childTableName);
|
||||
|
||||
QJoinMetaData join = new QJoinMetaData()
|
||||
.withLeftTable(parentTableName)
|
||||
.withRightTable(childTableName)
|
||||
.withInferredName()
|
||||
.withType(JoinType.ONE_TO_MANY)
|
||||
.withType(isOneToOne ? JoinType.ONE_TO_ONE : JoinType.ONE_TO_MANY)
|
||||
.withJoinOn(new JoinOn(parentTable.getPrimaryKeyField(), foreignKeyFieldName));
|
||||
|
||||
if(orderBys != null && orderBys.length > 0)
|
||||
@ -131,6 +127,41 @@ public class ChildJoinFromRecordEntityGenericMetaDataProducer implements MetaDat
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
*
|
||||
***************************************************************************/
|
||||
private QTableMetaData getTable(QInstance qInstance, String tableName) throws QException
|
||||
{
|
||||
QTableMetaData table = qInstance.getTable(tableName);
|
||||
if(table == null)
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// in case we're producing a QBit, and it's added a table to a multi-output, //
|
||||
// but not yet the instance, see if we can get table from there //
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
for(MetaDataProducerMultiOutput metaDataProducerMultiOutput : QBitProductionContext.getReadOnlyViewOfMetaDataProducerMultiOutputStack())
|
||||
{
|
||||
table = CollectionUtils.nonNullList(metaDataProducerMultiOutput.getEach(QTableMetaData.class)).stream()
|
||||
.filter(t -> t.getName().equals(tableName))
|
||||
.findFirst().orElse(null);
|
||||
|
||||
if(table != null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(table == null)
|
||||
{
|
||||
throw (new QException("Could not find tableMetaData: " + table));
|
||||
}
|
||||
|
||||
return table;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for sourceClass
|
||||
**
|
||||
|
@ -25,10 +25,13 @@ package com.kingsrook.qqq.backend.core.model.metadata.producers;
|
||||
import com.kingsrook.qqq.backend.core.actions.dashboard.widgets.ChildRecordListRenderer;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerInterface;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerMultiOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.producers.annotations.ChildRecordListWidget;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.qbits.QBitProductionContext;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
|
||||
|
||||
@ -89,6 +92,26 @@ public class ChildRecordListWidgetFromRecordEntityGenericMetaDataProducer implem
|
||||
String name = QJoinMetaData.makeInferredJoinName(parentTableName, childTableName);
|
||||
QJoinMetaData join = qInstance.getJoin(name);
|
||||
|
||||
if(join == null)
|
||||
{
|
||||
for(MetaDataProducerMultiOutput metaDataProducerMultiOutput : QBitProductionContext.getReadOnlyViewOfMetaDataProducerMultiOutputStack())
|
||||
{
|
||||
join = CollectionUtils.nonNullList(metaDataProducerMultiOutput.getEach(QJoinMetaData.class)).stream()
|
||||
.filter(t -> t.getName().equals(name))
|
||||
.findFirst().orElse(null);
|
||||
|
||||
if(join != null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(join == null)
|
||||
{
|
||||
throw (new QException("Could not find joinMetaData: " + name));
|
||||
}
|
||||
|
||||
QWidgetMetaData widget = ChildRecordListRenderer.widgetMetaDataBuilder(join)
|
||||
.withName(name)
|
||||
.withLabel(childRecordListWidget.label())
|
||||
|
@ -38,6 +38,8 @@ public @interface ChildJoin
|
||||
|
||||
OrderBy[] orderBy() default { };
|
||||
|
||||
boolean isOneToOne() default false;
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
|
@ -107,4 +107,14 @@ public interface QBitConfig extends Serializable
|
||||
{
|
||||
return (null);
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
*
|
||||
***************************************************************************/
|
||||
default String getDefaultBackendNameForTables()
|
||||
{
|
||||
return (null);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,192 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.model.metadata.qbits;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QRuntimeException;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerHelper;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerInterface;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerMultiOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** 2nd generation interface for top-level meta-data production classes that make
|
||||
** a qbit (evolution over QBitProducer).
|
||||
**
|
||||
*******************************************************************************/
|
||||
public interface QBitMetaDataProducer<C extends QBitConfig> extends MetaDataProducerInterface<MetaDataProducerMultiOutput>
|
||||
{
|
||||
QLogger LOG = QLogger.getLogger(QBitMetaDataProducer.class);
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
C getQBitConfig();
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
QBitMetaData getQBitMetaData();
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
default String getNamespace()
|
||||
{
|
||||
return (null);
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
default void postProduceActions(MetaDataProducerMultiOutput metaDataProducerMultiOutput, QInstance qinstance)
|
||||
{
|
||||
/////////////////////
|
||||
// noop by default //
|
||||
/////////////////////
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
default String getPackageNameForFindingMetaDataProducers()
|
||||
{
|
||||
Class<?> clazz = getClass();
|
||||
|
||||
////////////////////////////////////////////////////////////////
|
||||
// Walk up the hierarchy until we find the direct implementer //
|
||||
////////////////////////////////////////////////////////////////
|
||||
while(clazz != null)
|
||||
{
|
||||
Class<?>[] interfaces = clazz.getInterfaces();
|
||||
for(Class<?> interfaze : interfaces)
|
||||
{
|
||||
if(interfaze == QBitMetaDataProducer.class)
|
||||
{
|
||||
return clazz.getPackageName();
|
||||
}
|
||||
}
|
||||
clazz = clazz.getSuperclass();
|
||||
}
|
||||
|
||||
throw (new QRuntimeException("Unable to find packageName for QBitMetaDataProducer. You may need to implement getPackageName yourself..."));
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
@Override
|
||||
default MetaDataProducerMultiOutput produce(QInstance qInstance) throws QException
|
||||
{
|
||||
MetaDataProducerMultiOutput rs = new MetaDataProducerMultiOutput();
|
||||
|
||||
QBitMetaData qBitMetaData = getQBitMetaData();
|
||||
C qBitConfig = getQBitConfig();
|
||||
|
||||
qInstance.addQBit(qBitMetaData);
|
||||
|
||||
QBitProductionContext.pushQBitConfig(qBitConfig);
|
||||
QBitProductionContext.pushMetaDataProducerMultiOutput(rs);
|
||||
|
||||
try
|
||||
{
|
||||
qBitConfig.validate(qInstance);
|
||||
|
||||
List<MetaDataProducerInterface<?>> producers = MetaDataProducerHelper.findProducers(getPackageNameForFindingMetaDataProducers());
|
||||
MetaDataProducerHelper.sortMetaDataProducers(producers);
|
||||
for(MetaDataProducerInterface<?> producer : producers)
|
||||
{
|
||||
if(producer.getClass().equals(this.getClass()))
|
||||
{
|
||||
/////////////////////////////////////////////
|
||||
// avoid recursive processing of ourselves //
|
||||
/////////////////////////////////////////////
|
||||
continue;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// todo is this deprecated in favor of QBitProductionContext's stack... ? //
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
if(producer instanceof QBitComponentMetaDataProducer<?, ?>)
|
||||
{
|
||||
QBitComponentMetaDataProducer<?, C> qBitComponentMetaDataProducer = (QBitComponentMetaDataProducer<?, C>) producer;
|
||||
qBitComponentMetaDataProducer.setQBitConfig(qBitConfig);
|
||||
}
|
||||
|
||||
if(!producer.isEnabled())
|
||||
{
|
||||
LOG.debug("Not using producer which is not enabled", logPair("producer", producer.getClass().getSimpleName()));
|
||||
continue;
|
||||
}
|
||||
|
||||
MetaDataProducerOutput subProducerOutput = producer.produce(qInstance);
|
||||
|
||||
/////////////////////////////////////////////////
|
||||
// apply some things from the config to tables //
|
||||
/////////////////////////////////////////////////
|
||||
if(subProducerOutput instanceof QTableMetaData table)
|
||||
{
|
||||
if(qBitConfig.getTableMetaDataCustomizer() != null)
|
||||
{
|
||||
subProducerOutput = qBitConfig.getTableMetaDataCustomizer().customizeMetaData(qInstance, table);
|
||||
}
|
||||
|
||||
if(!StringUtils.hasContent(table.getBackendName()) && StringUtils.hasContent(qBitConfig.getDefaultBackendNameForTables()))
|
||||
{
|
||||
table.setBackendName(qBitConfig.getDefaultBackendNameForTables());
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////
|
||||
// set source qbit, if subProducerOutput is aware of such //
|
||||
////////////////////////////////////////////////////////////
|
||||
if(subProducerOutput instanceof SourceQBitAware sourceQBitAware)
|
||||
{
|
||||
sourceQBitAware.setSourceQBitName(qBitMetaData.getName());
|
||||
}
|
||||
|
||||
rs.add(subProducerOutput);
|
||||
}
|
||||
|
||||
postProduceActions(rs, qInstance);
|
||||
|
||||
return (rs);
|
||||
}
|
||||
finally
|
||||
{
|
||||
QBitProductionContext.popQBitConfig();
|
||||
QBitProductionContext.popMetaDataProducerMultiOutput();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -76,9 +76,6 @@ public interface QBitProducer
|
||||
{
|
||||
qBitConfig.validate(qInstance);
|
||||
|
||||
///////////////////////////////
|
||||
// todo - move to base class //
|
||||
///////////////////////////////
|
||||
for(MetaDataProducerInterface<?> producer : producers)
|
||||
{
|
||||
if(producer instanceof QBitComponentMetaDataProducer<?, ?>)
|
||||
|
@ -0,0 +1,136 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.model.metadata.qbits;
|
||||
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Stack;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerMultiOutput;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** While a qbit is being produced, track the context of the current config
|
||||
** and metaDataProducerMultiOutput that is being used. also, in case one
|
||||
** qbit produces another, push these contextual objects on a stack.
|
||||
*******************************************************************************/
|
||||
public class QBitProductionContext
|
||||
{
|
||||
private static final QLogger LOG = QLogger.getLogger(QBitProductionContext.class);
|
||||
|
||||
private static Stack<QBitConfig> qbitConfigStack = new Stack<>();
|
||||
private static Stack<MetaDataProducerMultiOutput> metaDataProducerMultiOutputStack = new Stack<>();
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
public static void pushQBitConfig(QBitConfig qBitConfig)
|
||||
{
|
||||
qbitConfigStack.push(qBitConfig);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
public static QBitConfig peekQBitConfig()
|
||||
{
|
||||
if(qbitConfigStack.isEmpty())
|
||||
{
|
||||
LOG.warn("Request to peek at empty QBitProductionContext configStack - returning null");
|
||||
return (null);
|
||||
}
|
||||
return qbitConfigStack.peek();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
public static void popQBitConfig()
|
||||
{
|
||||
if(qbitConfigStack.isEmpty())
|
||||
{
|
||||
LOG.warn("Request to pop empty QBitProductionContext configStack - returning with noop");
|
||||
return;
|
||||
}
|
||||
|
||||
qbitConfigStack.pop();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
public static void pushMetaDataProducerMultiOutput(MetaDataProducerMultiOutput metaDataProducerMultiOutput)
|
||||
{
|
||||
metaDataProducerMultiOutputStack.push(metaDataProducerMultiOutput);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
public static MetaDataProducerMultiOutput peekMetaDataProducerMultiOutput()
|
||||
{
|
||||
if(metaDataProducerMultiOutputStack.isEmpty())
|
||||
{
|
||||
LOG.warn("Request to peek at empty QBitProductionContext configStack - returning null");
|
||||
return (null);
|
||||
}
|
||||
return metaDataProducerMultiOutputStack.peek();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
public static List<MetaDataProducerMultiOutput> getReadOnlyViewOfMetaDataProducerMultiOutputStack()
|
||||
{
|
||||
return Collections.unmodifiableList(metaDataProducerMultiOutputStack);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
public static void popMetaDataProducerMultiOutput()
|
||||
{
|
||||
if(metaDataProducerMultiOutputStack.isEmpty())
|
||||
{
|
||||
LOG.warn("Request to pop empty QBitProductionContext metaDataProducerMultiOutput - returning with noop");
|
||||
return;
|
||||
}
|
||||
|
||||
metaDataProducerMultiOutputStack.pop();
|
||||
}
|
||||
|
||||
}
|
@ -44,7 +44,7 @@ public class MultiRecordSecurityLock extends RecordSecurityLock implements Clone
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected MultiRecordSecurityLock clone() throws CloneNotSupportedException
|
||||
public MultiRecordSecurityLock clone()
|
||||
{
|
||||
MultiRecordSecurityLock clone = (MultiRecordSecurityLock) super.clone();
|
||||
|
||||
|
@ -57,7 +57,9 @@ public class RecordSecurityLock implements Cloneable
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected RecordSecurityLock clone() throws CloneNotSupportedException
|
||||
public RecordSecurityLock clone()
|
||||
{
|
||||
try
|
||||
{
|
||||
RecordSecurityLock clone = (RecordSecurityLock) super.clone();
|
||||
|
||||
@ -72,6 +74,11 @@ public class RecordSecurityLock implements Cloneable
|
||||
|
||||
return (clone);
|
||||
}
|
||||
catch(CloneNotSupportedException e)
|
||||
{
|
||||
throw (new RuntimeException("Could not clone", e));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
@ -60,9 +60,6 @@ public class SavedBulkLoadProfile extends QRecordEntity
|
||||
@QField(label = "Mapping JSON")
|
||||
private String mappingJson;
|
||||
|
||||
@QField()
|
||||
private Boolean isBulkEdit;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -254,6 +251,7 @@ public class SavedBulkLoadProfile extends QRecordEntity
|
||||
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for mappingJson
|
||||
*******************************************************************************/
|
||||
@ -284,34 +282,4 @@ public class SavedBulkLoadProfile extends QRecordEntity
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for isBulkEdit
|
||||
*******************************************************************************/
|
||||
public Boolean getIsBulkEdit()
|
||||
{
|
||||
return (this.isBulkEdit);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for isBulkEdit
|
||||
*******************************************************************************/
|
||||
public void setIsBulkEdit(Boolean isBulkEdit)
|
||||
{
|
||||
this.isBulkEdit = isBulkEdit;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for isBulkEdit
|
||||
*******************************************************************************/
|
||||
public SavedBulkLoadProfile withIsBulkEdit(Boolean isBulkEdit)
|
||||
{
|
||||
this.isBulkEdit = isBulkEdit;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -113,7 +113,7 @@ public class SavedBulkLoadProfileMetaDataProvider
|
||||
.withFieldsFromEntity(SavedBulkLoadProfile.class)
|
||||
.withAuditRules(new QAuditRules().withAuditLevel(AuditLevel.FIELD))
|
||||
.withSection(new QFieldSection("identity", new QIcon().withName("badge"), Tier.T1, List.of("id", "label", "tableName")))
|
||||
.withSection(new QFieldSection("details", new QIcon().withName("text_snippet"), Tier.T2, List.of("userId", "mappingJson", "isBulkEdit")))
|
||||
.withSection(new QFieldSection("details", new QIcon().withName("text_snippet"), Tier.T2, List.of("userId", "mappingJson")))
|
||||
.withSection(new QFieldSection("dates", new QIcon().withName("calendar_month"), Tier.T3, List.of("createDate", "modifyDate")));
|
||||
|
||||
table.getField("mappingJson").withBehavior(SavedBulkLoadProfileJsonFieldDisplayValueFormatter.getInstance());
|
||||
|
@ -0,0 +1,123 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.model.tables;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import com.kingsrook.qqq.backend.core.actions.permissions.PermissionCheckResult;
|
||||
import com.kingsrook.qqq.backend.core.actions.permissions.PermissionsHelper;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.values.BasicCustomPossibleValueProvider;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
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.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValue;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** possible-value source provider for the `QQQ Table` PVS - a list of all tables
|
||||
** in an application/qInstance (that you have permission to see)
|
||||
*******************************************************************************/
|
||||
public class QQQTableCustomPossibleValueProvider extends BasicCustomPossibleValueProvider<QRecord, Integer>
|
||||
{
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
@Override
|
||||
protected QPossibleValue<Integer> makePossibleValue(QRecord sourceObject)
|
||||
{
|
||||
return (new QPossibleValue<>(sourceObject.getValueInteger("id"), sourceObject.getValueString("label")));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
@Override
|
||||
protected QRecord getSourceObject(Serializable id) throws QException
|
||||
{
|
||||
QRecord qqqTableRecord = GetAction.execute(QQQTable.TABLE_NAME, id);
|
||||
if(qqqTableRecord == null)
|
||||
{
|
||||
return (null);
|
||||
}
|
||||
|
||||
QTableMetaData table = QContext.getQInstance().getTable(qqqTableRecord.getValueString("name"));
|
||||
return isTableAllowed(table) ? qqqTableRecord : null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
@Override
|
||||
protected List<QRecord> getAllSourceObjects() throws QException
|
||||
{
|
||||
List<QRecord> records = QueryAction.execute(QQQTable.TABLE_NAME, null);
|
||||
ArrayList<QRecord> rs = new ArrayList<>();
|
||||
for(QRecord record : records)
|
||||
{
|
||||
QTableMetaData table = QContext.getQInstance().getTable(record.getValueString("name"));
|
||||
if(isTableAllowed(table))
|
||||
{
|
||||
rs.add(record);
|
||||
}
|
||||
}
|
||||
|
||||
return rs;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
private boolean isTableAllowed(QTableMetaData table)
|
||||
{
|
||||
if(table == null)
|
||||
{
|
||||
return (false);
|
||||
}
|
||||
|
||||
if(table.getIsHidden())
|
||||
{
|
||||
return (false);
|
||||
}
|
||||
|
||||
PermissionCheckResult permissionCheckResult = PermissionsHelper.getPermissionCheckResult(new QueryInput(table.getName()), table);
|
||||
if(!PermissionCheckResult.ALLOW.equals(permissionCheckResult))
|
||||
{
|
||||
return (false);
|
||||
}
|
||||
|
||||
return (true);
|
||||
}
|
||||
|
||||
}
|
@ -22,18 +22,29 @@
|
||||
package com.kingsrook.qqq.backend.core.model.tables;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.QueryOrGetInputInterface;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.AdornmentType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.processes.utils.GeneralProcessUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder;
|
||||
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||
|
||||
@ -89,4 +100,41 @@ public class QQQTableTableManager
|
||||
|
||||
return getOutput.getRecord().getValueInteger("id");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
public static List<QRecord> setRecordLinksToRecordsFromTableDynamicForPostQuery(QueryOrGetInputInterface queryInput, List<QRecord> records, String tableIdField, String recordIdField) throws QException
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
// note, this is a second copy of this logic (first being in standard process traces). //
|
||||
// let the rule of 3 apply if we find ourselves copying it again //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(queryInput.getShouldGenerateDisplayValues())
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// for records with a table id value - look up that table name, then set a display-value //
|
||||
// for the Link type adornment, to the inputRecordId record within that table. //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
Set<Serializable> tableIds = records.stream().map(r -> r.getValue(tableIdField)).filter(Objects::nonNull).collect(Collectors.toSet());
|
||||
if(!tableIds.isEmpty())
|
||||
{
|
||||
Map<Serializable, QRecord> tableMap = GeneralProcessUtils.loadTableToMap(QQQTable.TABLE_NAME, "id", new QQueryFilter(new QFilterCriteria("id", QCriteriaOperator.IN, tableIds)));
|
||||
|
||||
for(QRecord record : records)
|
||||
{
|
||||
QRecord qqqTableRecord = tableMap.get(record.getValue(tableIdField));
|
||||
if(qqqTableRecord != null && record.getValue(recordIdField) != null)
|
||||
{
|
||||
record.setDisplayValue(recordIdField + ":" + AdornmentType.LinkValues.TO_RECORD_FROM_TABLE_DYNAMIC, qqqTableRecord.getValueString("name"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (records);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -27,6 +27,9 @@ import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.audits.AuditLevel;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.audits.QAuditRules;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.PVSValueFormatAndFields;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSourceType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.Capability;
|
||||
@ -125,10 +128,11 @@ public class QQQTablesMetaDataProvider
|
||||
public QPossibleValueSource defineQQQTablePossibleValueSource()
|
||||
{
|
||||
return (new QPossibleValueSource()
|
||||
.withType(QPossibleValueSourceType.TABLE)
|
||||
.withName(QQQTable.TABLE_NAME)
|
||||
.withTableName(QQQTable.TABLE_NAME))
|
||||
.withOrderByField("label");
|
||||
.withType(QPossibleValueSourceType.CUSTOM)
|
||||
.withIdType(QFieldType.INTEGER)
|
||||
.withCustomCodeReference(new QCodeReference(QQQTableCustomPossibleValueProvider.class))
|
||||
.withValueFormatAndFields(PVSValueFormatAndFields.LABEL_ONLY));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -74,6 +74,7 @@ import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.ListingHash;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -364,6 +365,9 @@ public class MemoryRecordStore
|
||||
// differently from other backends, because of having the same record variable in the backend store and in the user-code. //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
QRecord recordToInsert = new QRecord(record);
|
||||
|
||||
makeValueTypesMatchFieldTypes(table, recordToInsert);
|
||||
|
||||
if(CollectionUtils.nullSafeHasContents(recordToInsert.getErrors()))
|
||||
{
|
||||
outputRecords.add(recordToInsert);
|
||||
@ -414,6 +418,30 @@ public class MemoryRecordStore
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
private static void makeValueTypesMatchFieldTypes(QTableMetaData table, QRecord recordToInsert)
|
||||
{
|
||||
for(QFieldMetaData field : table.getFields().values())
|
||||
{
|
||||
Serializable value = recordToInsert.getValue(field.getName());
|
||||
if(value != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
recordToInsert.setValue(field.getName(), ValueUtils.getValueAsFieldType(field.getType(), value));
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOG.info("Error converting value to field's type", e, logPair("fieldName", field.getName()), logPair("value", value));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@ -444,7 +472,19 @@ public class MemoryRecordStore
|
||||
QRecord recordToUpdate = tableData.get(primaryKeyValue);
|
||||
for(Map.Entry<String, Serializable> valueEntry : record.getValues().entrySet())
|
||||
{
|
||||
recordToUpdate.setValue(valueEntry.getKey(), valueEntry.getValue());
|
||||
String fieldName = valueEntry.getKey();
|
||||
try
|
||||
{
|
||||
///////////////////////////////////////////////
|
||||
// try to make field values match field type //
|
||||
///////////////////////////////////////////////
|
||||
recordToUpdate.setValue(fieldName, ValueUtils.getValueAsFieldType(table.getField(fieldName).getType(), valueEntry.getValue()));
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOG.info("Error converting value to field's type", e, logPair("fieldName", fieldName), logPair("value", valueEntry.getValue()));
|
||||
recordToUpdate.setValue(fieldName, valueEntry.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
if(returnUpdatedRecords)
|
||||
|
@ -36,12 +36,10 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.InputSource;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.QInputSource;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert.BulkInsertTransformStep;
|
||||
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.LoadViaUpdateStep;
|
||||
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.ProcessSummaryProviderInterface;
|
||||
import com.kingsrook.qqq.backend.core.processes.implementations.general.ProcessSummaryWarningsAndErrorsRollup;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import org.apache.commons.lang.BooleanUtils;
|
||||
import static com.kingsrook.qqq.backend.core.processes.implementations.bulk.edit.BulkEditTransformStep.buildInfoSummaryLines;
|
||||
|
||||
|
||||
@ -55,9 +53,6 @@ public class BulkEditLoadStep extends LoadViaUpdateStep implements ProcessSummar
|
||||
private ProcessSummaryLine okSummary = new ProcessSummaryLine(Status.OK);
|
||||
private List<ProcessSummaryLine> infoSummaries = new ArrayList<>();
|
||||
|
||||
private Serializable firstInsertedPrimaryKey = null;
|
||||
private Serializable lastInsertedPrimaryKey = null;
|
||||
|
||||
private ProcessSummaryWarningsAndErrorsRollup processSummaryWarningsAndErrorsRollup = ProcessSummaryWarningsAndErrorsRollup.build("edited");
|
||||
|
||||
private String tableLabel;
|
||||
@ -111,16 +106,8 @@ public class BulkEditLoadStep extends LoadViaUpdateStep implements ProcessSummar
|
||||
tableLabel = table.getLabel();
|
||||
}
|
||||
|
||||
boolean isBulkEdit = BooleanUtils.isTrue(runBackendStepInput.getValueBoolean("isBulkEdit"));
|
||||
if(isBulkEdit)
|
||||
{
|
||||
buildBulkUpdateWithFileInfoSummaryLines(runBackendStepOutput, table);
|
||||
}
|
||||
else
|
||||
{
|
||||
buildInfoSummaryLines(runBackendStepInput, table, infoSummaries, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -159,83 +146,4 @@ public class BulkEditLoadStep extends LoadViaUpdateStep implements ProcessSummar
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
private void buildBulkUpdateWithFileInfoSummaryLines(RunBackendStepOutput runBackendStepOutput, QTableMetaData table)
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// the transform step builds summary lines that it predicts will update successfully. //
|
||||
// but those lines don't have ids, which we'd like to have (e.g., for a process trace that //
|
||||
// might link to the built record). also, it's possible that there was a fail that only //
|
||||
// happened in the actual update, so, basically, re-do the summary here //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||
BulkInsertTransformStep transformStep = (BulkInsertTransformStep) getTransformStep();
|
||||
ProcessSummaryLine okSummary = transformStep.okSummary;
|
||||
okSummary.setCount(0);
|
||||
okSummary.setPrimaryKeys(new ArrayList<>());
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// but - since errors from the transform step don't even make it through to us here in the load step, //
|
||||
// do re-use the ProcessSummaryWarningsAndErrorsRollup from transform step as follows: //
|
||||
// clear out its warnings - we'll completely rebuild them here (with primary keys) //
|
||||
// and add new error lines, e.g., in case of errors that only happened past the validation if possible. //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
ProcessSummaryWarningsAndErrorsRollup processSummaryWarningsAndErrorsRollup = transformStep.processSummaryWarningsAndErrorsRollup;
|
||||
processSummaryWarningsAndErrorsRollup.resetWarnings();
|
||||
|
||||
List<QRecord> updatedRecords = runBackendStepOutput.getRecords();
|
||||
for(QRecord updatedRecord : updatedRecords)
|
||||
{
|
||||
Serializable primaryKey = updatedRecord.getValue(table.getPrimaryKeyField());
|
||||
if(CollectionUtils.nullSafeIsEmpty(updatedRecord.getErrors()) && primaryKey != null)
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////
|
||||
// if the record had no errors, and we have a primary key for it, then //
|
||||
// keep track of the range of primary keys (first and last) //
|
||||
/////////////////////////////////////////////////////////////////////////
|
||||
if(firstInsertedPrimaryKey == null)
|
||||
{
|
||||
firstInsertedPrimaryKey = primaryKey;
|
||||
}
|
||||
|
||||
lastInsertedPrimaryKey = primaryKey;
|
||||
|
||||
if(!CollectionUtils.nullSafeIsEmpty(updatedRecord.getWarnings()))
|
||||
{
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// if there were warnings on the updated record, put it in a warning line //
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
String message = updatedRecord.getWarnings().get(0).getMessage();
|
||||
processSummaryWarningsAndErrorsRollup.addWarning(message, primaryKey);
|
||||
}
|
||||
else
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
// if no warnings for the updated record, then put it in the OK line //
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
okSummary.incrementCountAndAddPrimaryKey(primaryKey);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// else if there were errors or no primary key, build an error line //
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
String message = "Failed to update";
|
||||
if(!CollectionUtils.nullSafeIsEmpty(updatedRecord.getErrors()))
|
||||
{
|
||||
//////////////////////////////////////////////////////////
|
||||
// use the error message from the record if we have one //
|
||||
//////////////////////////////////////////////////////////
|
||||
message = updatedRecord.getErrors().get(0).getMessage();
|
||||
}
|
||||
processSummaryWarningsAndErrorsRollup.addError(message, primaryKey);
|
||||
}
|
||||
}
|
||||
|
||||
okSummary.pickMessage(true);
|
||||
}
|
||||
}
|
||||
|
@ -48,7 +48,6 @@ import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||
import org.apache.commons.lang.BooleanUtils;
|
||||
import org.json.JSONObject;
|
||||
|
||||
|
||||
@ -66,11 +65,9 @@ public class BulkInsertPrepareFileMappingStep implements BackendStep
|
||||
{
|
||||
buildFileDetailsForMappingStep(runBackendStepInput, runBackendStepOutput);
|
||||
|
||||
boolean isBulkEdit = BooleanUtils.isTrue(runBackendStepInput.getValueBoolean("isBulkEdit"));
|
||||
String tableName = runBackendStepInput.getValueString("tableName");
|
||||
BulkLoadTableStructure tableStructure = BulkLoadTableStructureBuilder.buildTableStructure(tableName, isBulkEdit);
|
||||
BulkLoadTableStructure tableStructure = BulkLoadTableStructureBuilder.buildTableStructure(tableName);
|
||||
runBackendStepOutput.addValue("tableStructure", tableStructure);
|
||||
runBackendStepOutput.addValue("isBulkEdit", isBulkEdit);
|
||||
|
||||
boolean needSuggestedMapping = true;
|
||||
if(runBackendStepOutput.getProcessState().getIsStepBack())
|
||||
@ -84,7 +81,7 @@ public class BulkInsertPrepareFileMappingStep implements BackendStep
|
||||
{
|
||||
@SuppressWarnings("unchecked")
|
||||
List<String> headerValues = (List<String>) runBackendStepOutput.getValue("headerValues");
|
||||
buildSuggestedMapping(isBulkEdit, headerValues, getPrepopulatedValues(runBackendStepInput), tableStructure, runBackendStepOutput);
|
||||
buildSuggestedMapping(headerValues, getPrepopulatedValues(runBackendStepInput), tableStructure, runBackendStepOutput);
|
||||
}
|
||||
}
|
||||
|
||||
@ -115,10 +112,10 @@ public class BulkInsertPrepareFileMappingStep implements BackendStep
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
private void buildSuggestedMapping(boolean isBulkEdit, List<String> headerValues, Map<String, Serializable> prepopulatedValues, BulkLoadTableStructure tableStructure, RunBackendStepOutput runBackendStepOutput)
|
||||
private void buildSuggestedMapping(List<String> headerValues, Map<String, Serializable> prepopulatedValues, BulkLoadTableStructure tableStructure, RunBackendStepOutput runBackendStepOutput)
|
||||
{
|
||||
BulkLoadMappingSuggester bulkLoadMappingSuggester = new BulkLoadMappingSuggester();
|
||||
BulkLoadProfile bulkLoadProfile = bulkLoadMappingSuggester.suggestBulkLoadMappingProfile(tableStructure, headerValues, isBulkEdit);
|
||||
BulkLoadProfile bulkLoadProfile = bulkLoadMappingSuggester.suggestBulkLoadMappingProfile(tableStructure, headerValues);
|
||||
|
||||
if(CollectionUtils.nullSafeHasContents(prepopulatedValues))
|
||||
{
|
||||
|
@ -65,12 +65,10 @@ public class BulkInsertPrepareFileUploadStep implements BackendStep
|
||||
runBackendStepOutput.addValue("theFile", null);
|
||||
}
|
||||
|
||||
boolean isBulkEdit = runBackendStepInput.getProcessName().endsWith("EditWithFile");
|
||||
String tableName = runBackendStepInput.getValueString("tableName");
|
||||
QTableMetaData table = QContext.getQInstance().getTable(tableName);
|
||||
BulkLoadTableStructure tableStructure = BulkLoadTableStructureBuilder.buildTableStructure(tableName);
|
||||
runBackendStepOutput.addValue("tableStructure", tableStructure);
|
||||
runBackendStepOutput.addValue("isBulkEdit", isBulkEdit);
|
||||
|
||||
List<QFieldMetaData> requiredFields = new ArrayList<>();
|
||||
List<QFieldMetaData> additionalFields = new ArrayList<>();
|
||||
@ -86,14 +84,6 @@ public class BulkInsertPrepareFileUploadStep implements BackendStep
|
||||
}
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////
|
||||
// bulk edit allows primary key as a field //
|
||||
/////////////////////////////////////////////
|
||||
if(isBulkEdit)
|
||||
{
|
||||
requiredFields.add(0, table.getField(table.getPrimaryKeyField()));
|
||||
}
|
||||
|
||||
StringBuilder html;
|
||||
String childTableLabels = "";
|
||||
|
||||
@ -106,11 +96,11 @@ public class BulkInsertPrepareFileUploadStep implements BackendStep
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
boolean listFieldsInHelpText = false;
|
||||
|
||||
if(isBulkEdit || !CollectionUtils.nullSafeHasContents(tableStructure.getAssociations()))
|
||||
if(!CollectionUtils.nullSafeHasContents(tableStructure.getAssociations()))
|
||||
{
|
||||
html = new StringBuilder("""
|
||||
<p>Upload either a CSV or Excel (.xlsx) file, with one row for each record you want to
|
||||
${action} in the ${tableLabel} table.</p><br />
|
||||
insert in the ${tableLabel} table.</p><br />
|
||||
|
||||
<p>Your file can contain any number of columns. You will be prompted to map fields from
|
||||
the ${tableLabel} table to columns from your file or default values for all records that
|
||||
@ -214,7 +204,6 @@ public class BulkInsertPrepareFileUploadStep implements BackendStep
|
||||
finishCSV(flatCSV);
|
||||
|
||||
String htmlString = html.toString()
|
||||
.replace("${action}", (isBulkEdit ? "edit" : "insert"))
|
||||
.replace("${tableLabel}", table.getLabel())
|
||||
.replace("${childTableLabels}", childTableLabels)
|
||||
.replace("${flatCSV}", Base64.getEncoder().encodeToString(flatCSV.toString().getBytes(StandardCharsets.UTF_8)))
|
||||
|
@ -113,8 +113,6 @@ public class BulkInsertStepUtils
|
||||
{
|
||||
String layout = runBackendStepInput.getValueString("layout");
|
||||
Boolean hasHeaderRow = runBackendStepInput.getValueBoolean("hasHeaderRow");
|
||||
String keyFields = runBackendStepInput.getValueString("keyFields");
|
||||
Boolean isBulkEdit = runBackendStepInput.getValueBoolean("isBulkEdit");
|
||||
|
||||
ArrayList<BulkLoadProfileField> fieldList = new ArrayList<>();
|
||||
|
||||
@ -129,7 +127,6 @@ public class BulkInsertStepUtils
|
||||
bulkLoadProfileField.setColumnIndex(jsonObject.has("columnIndex") ? jsonObject.getInt("columnIndex") : null);
|
||||
bulkLoadProfileField.setDefaultValue((Serializable) jsonObject.opt("defaultValue"));
|
||||
bulkLoadProfileField.setDoValueMapping(jsonObject.optBoolean("doValueMapping"));
|
||||
bulkLoadProfileField.setClearIfEmpty(jsonObject.optBoolean("clearIfEmpty"));
|
||||
|
||||
if(BooleanUtils.isTrue(bulkLoadProfileField.getDoValueMapping()) && jsonObject.has("valueMappings"))
|
||||
{
|
||||
@ -143,8 +140,6 @@ public class BulkInsertStepUtils
|
||||
}
|
||||
|
||||
BulkLoadProfile bulkLoadProfile = new BulkLoadProfile()
|
||||
.withIsBulkEdit(isBulkEdit)
|
||||
.withKeyFields(keyFields)
|
||||
.withVersion(version)
|
||||
.withFieldList(fieldList)
|
||||
.withHasHeaderRow(hasHeaderRow)
|
||||
|
@ -24,7 +24,6 @@ package com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashSet;
|
||||
@ -33,13 +32,12 @@ import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.AbstractPreInsertCustomizer;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.AbstractPreInsertCustomizer.WhenToRun;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizerInterface;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.helpers.UniqueKeyHelper;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
@ -50,11 +48,6 @@ import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutp
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.Status;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.QInputSource;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
|
||||
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.WidgetType;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaDataInterface;
|
||||
@ -75,9 +68,6 @@ import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.ListingHash;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||
import org.apache.commons.lang.BooleanUtils;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -85,9 +75,9 @@ import org.json.JSONObject;
|
||||
*******************************************************************************/
|
||||
public class BulkInsertTransformStep extends AbstractTransformStep
|
||||
{
|
||||
public ProcessSummaryLine okSummary = new ProcessSummaryLine(Status.OK);
|
||||
ProcessSummaryLine okSummary = new ProcessSummaryLine(Status.OK);
|
||||
|
||||
public ProcessSummaryWarningsAndErrorsRollup processSummaryWarningsAndErrorsRollup = ProcessSummaryWarningsAndErrorsRollup.build("inserted")
|
||||
ProcessSummaryWarningsAndErrorsRollup processSummaryWarningsAndErrorsRollup = ProcessSummaryWarningsAndErrorsRollup.build("inserted")
|
||||
.withDoReplaceSingletonCountLinesWithSuffixOnly(false);
|
||||
|
||||
private ListingHash<String, RowValue> errorToExampleRowValueMap = new ListingHash<>();
|
||||
@ -200,252 +190,6 @@ public class BulkInsertTransformStep extends AbstractTransformStep
|
||||
QTableMetaData table = QContext.getQInstance().getTable(runBackendStepInput.getTableName());
|
||||
List<QRecord> records = runBackendStepInput.getRecords();
|
||||
|
||||
if(BooleanUtils.isTrue(runBackendStepInput.getValueBoolean("isBulkEdit")))
|
||||
{
|
||||
handleBulkEdit(runBackendStepInput, runBackendStepOutput, records, table);
|
||||
runBackendStepOutput.addValue("isBulkEdit", true);
|
||||
}
|
||||
else
|
||||
{
|
||||
handleBulkLoad(runBackendStepInput, runBackendStepOutput, records, table);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void handleBulkEdit(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput, List<QRecord> records, QTableMetaData table) throws QException
|
||||
{
|
||||
///////////////////////////////////////////
|
||||
// get the key fields for this bulk edit //
|
||||
///////////////////////////////////////////
|
||||
String keyFieldsString = runBackendStepInput.getValueString("keyFields");
|
||||
List<String> keyFields = Arrays.asList(keyFieldsString.split("\\|"));
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// if the key field is the primary key, then just look up those records //
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
List<QRecord> nonMatchingRecords = new ArrayList<>();
|
||||
List<QRecord> oldRecords = new ArrayList<>();
|
||||
List<QRecord> recordsToUpdate = new ArrayList<>();
|
||||
if(keyFields.size() == 1 && table.getPrimaryKeyField().equals(keyFields.get(0)))
|
||||
{
|
||||
recordsToUpdate = records;
|
||||
String primaryKeyName = table.getPrimaryKeyField();
|
||||
List<Serializable> primaryKeys = records.stream().map(record -> record.getValue(primaryKeyName)).toList();
|
||||
oldRecords = new QueryAction().execute(new QueryInput(table.getName()).withFilter(new QQueryFilter(new QFilterCriteria(table.getPrimaryKeyField(), QCriteriaOperator.IN, primaryKeys)))).getRecords();
|
||||
|
||||
///////////////////////////////////////////
|
||||
// get a set of old records primary keys //
|
||||
///////////////////////////////////////////
|
||||
Set<Serializable> matchedPrimaryKeys = oldRecords.stream()
|
||||
.map(r -> r.getValue(table.getPrimaryKeyField()))
|
||||
.collect(java.util.stream.Collectors.toSet());
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// iterate over file records and if primary keys dont match, add to the non matching records list //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
for(QRecord record : records)
|
||||
{
|
||||
Serializable recordKey = record.getValue(table.getPrimaryKeyField());
|
||||
if(!matchedPrimaryKeys.contains(recordKey))
|
||||
{
|
||||
nonMatchingRecords.add(record);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Set<Serializable> uniqueIds = new HashSet<>();
|
||||
List<QRecord> potentialRecords = new ArrayList<>();
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if not using the primary key, then we will look up all records for each part of the unique key //
|
||||
// and for each found, if all unique parts match we will add to our list of database records //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
for(String uniqueKeyPart : keyFields)
|
||||
{
|
||||
List<Serializable> values = records.stream().map(record -> record.getValue(uniqueKeyPart)).toList();
|
||||
for(QRecord databaseRecord : new QueryAction().execute(new QueryInput(table.getName()).withFilter(new QQueryFilter(new QFilterCriteria(uniqueKeyPart, QCriteriaOperator.IN, values)))).getRecords())
|
||||
{
|
||||
if(!uniqueIds.contains(databaseRecord.getValue(table.getPrimaryKeyField())))
|
||||
{
|
||||
potentialRecords.add(databaseRecord);
|
||||
uniqueIds.add(databaseRecord.getValue(table.getPrimaryKeyField()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// now iterate over all of the potential records checking each unique fields //
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
fileRecordLoop:
|
||||
for(QRecord fileRecord : records)
|
||||
{
|
||||
for(QRecord databaseRecord : potentialRecords)
|
||||
{
|
||||
boolean allMatch = true;
|
||||
|
||||
for(String uniqueKeyPart : keyFields)
|
||||
{
|
||||
if(!Objects.equals(fileRecord.getValue(uniqueKeyPart), databaseRecord.getValue(uniqueKeyPart)))
|
||||
{
|
||||
allMatch = false;
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
// if we get here with all matching, update the record from the file's primary key, //
|
||||
// add it to the list to update, and continue looping over file records //
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
if(allMatch)
|
||||
{
|
||||
oldRecords.add(databaseRecord);
|
||||
fileRecord.setValue(table.getPrimaryKeyField(), databaseRecord.getValue(table.getPrimaryKeyField()));
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// iterate over the fields in the bulk load profile, if the value for that field is empty and the value //
|
||||
// of 'clear if empty' is set to true, then update the record to update with the old record's value //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
JSONArray array = new JSONArray(runBackendStepInput.getValueString("fieldListJSON"));
|
||||
for(int i = 0; i < array.length(); i++)
|
||||
{
|
||||
JSONObject jsonObject = array.getJSONObject(i);
|
||||
String fieldName = jsonObject.optString("fieldName");
|
||||
boolean clearIfEmpty = jsonObject.optBoolean("clearIfEmpty");
|
||||
|
||||
if(fileRecord.getValue(fieldName) == null)
|
||||
{
|
||||
if(clearIfEmpty)
|
||||
{
|
||||
fileRecord.setValue(fieldName, null);
|
||||
}
|
||||
else
|
||||
{
|
||||
fileRecord.setValue(fieldName, databaseRecord.getValue(fieldName));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
recordsToUpdate.add(fileRecord);
|
||||
continue fileRecordLoop;
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////
|
||||
// if we make it here, that means the record was not found, keep for logging warning //
|
||||
///////////////////////////////////////////////////////////////////////////////////////
|
||||
nonMatchingRecords.add(fileRecord);
|
||||
}
|
||||
}
|
||||
|
||||
for(QRecord missingRecord : CollectionUtils.nonNullList(nonMatchingRecords))
|
||||
{
|
||||
String message = "Did not have a matching existing record.";
|
||||
processSummaryWarningsAndErrorsRollup.addError(message, null);
|
||||
addToErrorToExampleRowMap(message, missingRecord);
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// set up an insert-input, which will be used as input to the pre-customizer as well as for additional validations //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
UpdateInput updateInput = new UpdateInput(table.getName());
|
||||
updateInput.setInputSource(QInputSource.USER);
|
||||
updateInput.setRecords(recordsToUpdate);
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// load the pre-insert customizer and set it up, if there is one //
|
||||
// then we'll run it based on its WhenToRun value //
|
||||
// we do this, in case it needs to, for example, adjust values that //
|
||||
// are part of a unique key //
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
boolean didAlreadyRunCustomizer = false;
|
||||
Optional<TableCustomizerInterface> preUpdateCustomizer = QCodeLoader.getTableCustomizer(table, TableCustomizers.PRE_UPDATE_RECORD.getRole());
|
||||
if(preUpdateCustomizer.isPresent())
|
||||
{
|
||||
List<QRecord> recordsAfterCustomizer = preUpdateCustomizer.get().preUpdate(updateInput, records, true, Optional.of(oldRecords));
|
||||
runBackendStepInput.setRecords(recordsAfterCustomizer);
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// so we used to have a comment here asking "do we care if the customizer runs both now, and in the validation below?" //
|
||||
// when implementing Bulk Load V2, we were seeing that some customizers were adding errors to records, both now, and //
|
||||
// when they ran below. so, at that time, we added this boolean, to track and avoid the double-run... //
|
||||
// we could also imagine this being a setting on the pre-insert customizer, similar to its whenToRun attribute... //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
didAlreadyRunCustomizer = true;
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
// run all validation from the insert action - in Preview mode (boolean param) //
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
updateInput.setRecords(recordsToUpdate);
|
||||
UpdateAction updateAction = new UpdateAction();
|
||||
updateAction.performValidations(updateInput, Optional.of(recordsToUpdate), didAlreadyRunCustomizer);
|
||||
List<QRecord> validationResultRecords = updateInput.getRecords();
|
||||
|
||||
/////////////////////////////////////////////////////////////////
|
||||
// look at validation results to build process summary results //
|
||||
/////////////////////////////////////////////////////////////////
|
||||
List<QRecord> outputRecords = new ArrayList<>();
|
||||
for(QRecord record : validationResultRecords)
|
||||
{
|
||||
List<QErrorMessage> errorsFromAssociations = getErrorsFromAssociations(record);
|
||||
if(CollectionUtils.nullSafeHasContents(errorsFromAssociations))
|
||||
{
|
||||
List<QErrorMessage> recordErrors = Objects.requireNonNullElseGet(record.getErrors(), () -> new ArrayList<>());
|
||||
recordErrors.addAll(errorsFromAssociations);
|
||||
record.setErrors(recordErrors);
|
||||
}
|
||||
|
||||
if(CollectionUtils.nullSafeHasContents(record.getErrors()))
|
||||
{
|
||||
for(QErrorMessage error : record.getErrors())
|
||||
{
|
||||
if(error instanceof AbstractBulkLoadRollableValueError rollableValueError)
|
||||
{
|
||||
processSummaryWarningsAndErrorsRollup.addError(rollableValueError.getMessageToUseAsProcessSummaryRollupKey(), null);
|
||||
addToErrorToExampleRowValueMap(rollableValueError, record);
|
||||
}
|
||||
else
|
||||
{
|
||||
processSummaryWarningsAndErrorsRollup.addError(error.getMessage(), null);
|
||||
addToErrorToExampleRowMap(error.getMessage(), record);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if(CollectionUtils.nullSafeHasContents(record.getWarnings()))
|
||||
{
|
||||
String message = record.getWarnings().get(0).getMessage();
|
||||
processSummaryWarningsAndErrorsRollup.addWarning(message, null);
|
||||
outputRecords.add(record);
|
||||
}
|
||||
else
|
||||
{
|
||||
okSummary.incrementCountAndAddPrimaryKey(null);
|
||||
outputRecords.add(record);
|
||||
|
||||
for(Map.Entry<String, List<QRecord>> entry : CollectionUtils.nonNullMap(record.getAssociatedRecords()).entrySet())
|
||||
{
|
||||
String associationName = entry.getKey();
|
||||
ProcessSummaryLine associationToInsertLine = associationsToInsertSummaries.computeIfAbsent(associationName, x -> new ProcessSummaryLine(Status.OK));
|
||||
associationToInsertLine.incrementCount(CollectionUtils.nonNullList(entry.getValue()).size());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
runBackendStepOutput.setRecords(outputRecords);
|
||||
this.rowsProcessed += records.size();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void handleBulkLoad(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput, List<QRecord> records, QTableMetaData table) throws QException
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// set up an insert-input, which will be used as input to the pre-customizer as well as for additional validations //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
@ -465,7 +209,7 @@ public class BulkInsertTransformStep extends AbstractTransformStep
|
||||
Optional<TableCustomizerInterface> preInsertCustomizer = QCodeLoader.getTableCustomizer(table, TableCustomizers.PRE_INSERT_RECORD.getRole());
|
||||
if(preInsertCustomizer.isPresent())
|
||||
{
|
||||
WhenToRun whenToRun = preInsertCustomizer.get().whenToRunPreInsert(insertInput, true);
|
||||
AbstractPreInsertCustomizer.WhenToRun whenToRun = preInsertCustomizer.get().whenToRunPreInsert(insertInput, true);
|
||||
if(WhenToRun.BEFORE_ALL_VALIDATIONS.equals(whenToRun) || WhenToRun.BEFORE_UNIQUE_KEY_CHECKS.equals(whenToRun))
|
||||
{
|
||||
List<QRecord> recordsAfterCustomizer = preInsertCustomizer.get().preInsert(insertInput, records, true);
|
||||
@ -741,13 +485,11 @@ public class BulkInsertTransformStep extends AbstractTransformStep
|
||||
recordsProcessedLine.withPluralFutureMessage("records were");
|
||||
recordsProcessedLine.withPluralPastMessage("records were");
|
||||
|
||||
boolean isBulkEdit = BooleanUtils.isTrue(runBackendStepOutput.getValueBoolean("isBulkEdit"));
|
||||
String action = isBulkEdit ? "updated" : "inserted";
|
||||
String noWarningsSuffix = processSummaryWarningsAndErrorsRollup.countWarnings() == 0 ? "" : " with no warnings";
|
||||
okSummary.setSingularFutureMessage(tableLabel + " record will be " + action + noWarningsSuffix + ".");
|
||||
okSummary.setPluralFutureMessage(tableLabel + " records will be " + action + noWarningsSuffix + ".");
|
||||
okSummary.setSingularPastMessage(tableLabel + " record was " + action + noWarningsSuffix + ".");
|
||||
okSummary.setPluralPastMessage(tableLabel + " records were " + action + noWarningsSuffix + ".");
|
||||
okSummary.setSingularFutureMessage(tableLabel + " record will be inserted" + noWarningsSuffix + ".");
|
||||
okSummary.setPluralFutureMessage(tableLabel + " records will be inserted" + noWarningsSuffix + ".");
|
||||
okSummary.setSingularPastMessage(tableLabel + " record was inserted" + noWarningsSuffix + ".");
|
||||
okSummary.setPluralPastMessage(tableLabel + " records were inserted" + noWarningsSuffix + ".");
|
||||
okSummary.pickMessage(isForResultScreen);
|
||||
okSummary.addSelfToListIfAnyCount(rs);
|
||||
|
||||
@ -760,10 +502,10 @@ public class BulkInsertTransformStep extends AbstractTransformStep
|
||||
String associationLabel = associationTable.getLabel();
|
||||
|
||||
ProcessSummaryLine line = entry.getValue();
|
||||
line.setSingularFutureMessage(associationLabel + " record will be " + action + ".");
|
||||
line.setPluralFutureMessage(associationLabel + " records will be " + action + ".");
|
||||
line.setSingularPastMessage(associationLabel + " record was " + action + ".");
|
||||
line.setPluralPastMessage(associationLabel + " records were " + action + ".");
|
||||
line.setSingularFutureMessage(associationLabel + " record will be inserted.");
|
||||
line.setPluralFutureMessage(associationLabel + " records will be inserted.");
|
||||
line.setSingularPastMessage(associationLabel + " record was inserted.");
|
||||
line.setPluralPastMessage(associationLabel + " records were inserted.");
|
||||
line.pickMessage(isForResultScreen);
|
||||
line.addSelfToListIfAnyCount(rs);
|
||||
}
|
||||
|
@ -54,7 +54,7 @@ public class BulkLoadMappingSuggester
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
public BulkLoadProfile suggestBulkLoadMappingProfile(BulkLoadTableStructure tableStructure, List<String> headerRow, boolean isBulkEdit)
|
||||
public BulkLoadProfile suggestBulkLoadMappingProfile(BulkLoadTableStructure tableStructure, List<String> headerRow)
|
||||
{
|
||||
massagedHeadersWithoutNumbersToIndexMap = new LinkedHashMap<>();
|
||||
for(int i = 0; i < headerRow.size(); i++)
|
||||
@ -90,7 +90,6 @@ public class BulkLoadMappingSuggester
|
||||
.withVersion("v1")
|
||||
.withLayout(layout)
|
||||
.withHasHeaderRow(true)
|
||||
.withIsBulkEdit(isBulkEdit)
|
||||
.withFieldList(fieldList);
|
||||
|
||||
return (bulkLoadProfile);
|
||||
|
@ -25,19 +25,12 @@ package com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert.map
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.values.SearchPossibleValueSourceInput;
|
||||
import com.kingsrook.qqq.backend.core.model.bulk.TableKeyFieldsPossibleValueSource;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValue;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.Association;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert.model.BulkLoadTableStructure;
|
||||
@ -51,16 +44,12 @@ import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
*******************************************************************************/
|
||||
public class BulkLoadTableStructureBuilder
|
||||
{
|
||||
private static final QLogger LOG = QLogger.getLogger(BulkLoadTableStructureBuilder.class);
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
public static BulkLoadTableStructure buildTableStructure(String tableName)
|
||||
{
|
||||
return (buildTableStructure(tableName, null, null, false));
|
||||
return (buildTableStructure(tableName, null, null));
|
||||
}
|
||||
|
||||
|
||||
@ -68,24 +57,13 @@ public class BulkLoadTableStructureBuilder
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
public static BulkLoadTableStructure buildTableStructure(String tableName, Boolean isBulkEdit)
|
||||
{
|
||||
return (buildTableStructure(tableName, null, null, isBulkEdit));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
private static BulkLoadTableStructure buildTableStructure(String tableName, Association association, String parentAssociationPath, Boolean isBulkEdit)
|
||||
private static BulkLoadTableStructure buildTableStructure(String tableName, Association association, String parentAssociationPath)
|
||||
{
|
||||
QTableMetaData table = QContext.getQInstance().getTable(tableName);
|
||||
|
||||
BulkLoadTableStructure tableStructure = new BulkLoadTableStructure();
|
||||
tableStructure.setTableName(tableName);
|
||||
tableStructure.setLabel(table.getLabel());
|
||||
tableStructure.setIsBulkEdit(isBulkEdit);
|
||||
|
||||
Set<String> associationJoinFieldNamesToExclude = new HashSet<>();
|
||||
|
||||
@ -141,30 +119,6 @@ public class BulkLoadTableStructureBuilder
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////
|
||||
// for bulk edit, users can use the primary key field //
|
||||
////////////////////////////////////////////////////////
|
||||
if(isBulkEdit)
|
||||
{
|
||||
fields.add(table.getField(table.getPrimaryKeyField()));
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// also make available what key fields are available for this table //
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
try
|
||||
{
|
||||
SearchPossibleValueSourceInput input = new SearchPossibleValueSourceInput()
|
||||
.withPossibleValueSourceName("tableKeyFields")
|
||||
.withPathParamMap(Map.of("processName", tableName + ".bulkEditWithFile"));
|
||||
List<QPossibleValue<String>> search = new TableKeyFieldsPossibleValueSource().search(input);
|
||||
tableStructure.setPossibleKeyFields(new ArrayList<>(search.stream().map(QPossibleValue::getId).toList()));
|
||||
}
|
||||
catch(QException qe)
|
||||
{
|
||||
LOG.warn("Unable to retrieve possible key fields for table [" + tableName + "]", qe);
|
||||
}
|
||||
}
|
||||
|
||||
fields.sort(Comparator.comparing(f -> ObjectUtils.requireNonNullElse(f.getLabel(), f.getName(), "")));
|
||||
|
||||
for(Association childAssociation : CollectionUtils.nonNullList(table.getAssociations()))
|
||||
@ -178,7 +132,7 @@ public class BulkLoadTableStructureBuilder
|
||||
String nextLevelPath =
|
||||
(StringUtils.hasContent(parentAssociationPath) ? parentAssociationPath + "." : "")
|
||||
+ (association != null ? association.getName() : "");
|
||||
BulkLoadTableStructure associatedStructure = buildTableStructure(childAssociation.getAssociatedTableName(), childAssociation, nextLevelPath, isBulkEdit);
|
||||
BulkLoadTableStructure associatedStructure = buildTableStructure(childAssociation.getAssociatedTableName(), childAssociation, nextLevelPath);
|
||||
tableStructure.addAssociation(associatedStructure);
|
||||
}
|
||||
}
|
||||
|
@ -37,8 +37,6 @@ public class BulkLoadProfile implements Serializable
|
||||
private Boolean hasHeaderRow;
|
||||
private String layout;
|
||||
private String version;
|
||||
private Boolean isBulkEdit;
|
||||
private String keyFields;
|
||||
|
||||
|
||||
|
||||
@ -134,7 +132,6 @@ public class BulkLoadProfile implements Serializable
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for version
|
||||
*******************************************************************************/
|
||||
@ -165,65 +162,4 @@ public class BulkLoadProfile implements Serializable
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for isBulkEdit
|
||||
*******************************************************************************/
|
||||
public Boolean getIsBulkEdit()
|
||||
{
|
||||
return (this.isBulkEdit);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for isBulkEdit
|
||||
*******************************************************************************/
|
||||
public void setIsBulkEdit(Boolean isBulkEdit)
|
||||
{
|
||||
this.isBulkEdit = isBulkEdit;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for isBulkEdit
|
||||
*******************************************************************************/
|
||||
public BulkLoadProfile withIsBulkEdit(Boolean isBulkEdit)
|
||||
{
|
||||
this.isBulkEdit = isBulkEdit;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for keyFields
|
||||
*******************************************************************************/
|
||||
public String getKeyFields()
|
||||
{
|
||||
return (this.keyFields);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for keyFields
|
||||
*******************************************************************************/
|
||||
public void setKeyFields(String keyFields)
|
||||
{
|
||||
this.keyFields = keyFields;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for keyFields
|
||||
*******************************************************************************/
|
||||
public BulkLoadProfile withKeyFields(String keyFields)
|
||||
{
|
||||
this.keyFields = keyFields;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -35,7 +35,6 @@ public class BulkLoadProfileField
|
||||
private Integer columnIndex;
|
||||
private String headerName;
|
||||
private Serializable defaultValue;
|
||||
private Boolean clearIfEmpty;
|
||||
private Boolean doValueMapping;
|
||||
private Map<String, Serializable> valueMappings;
|
||||
|
||||
@ -195,7 +194,6 @@ public class BulkLoadProfileField
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for headerName
|
||||
*******************************************************************************/
|
||||
@ -226,34 +224,4 @@ public class BulkLoadProfileField
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for clearIfEmpty
|
||||
*******************************************************************************/
|
||||
public Boolean getClearIfEmpty()
|
||||
{
|
||||
return (this.clearIfEmpty);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for clearIfEmpty
|
||||
*******************************************************************************/
|
||||
public void setClearIfEmpty(Boolean clearIfEmpty)
|
||||
{
|
||||
this.clearIfEmpty = clearIfEmpty;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for clearIfEmpty
|
||||
*******************************************************************************/
|
||||
public BulkLoadProfileField withClearIfEmpty(Boolean clearIfEmpty)
|
||||
{
|
||||
this.clearIfEmpty = clearIfEmpty;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -38,9 +38,6 @@ public class BulkLoadTableStructure implements Serializable
|
||||
private String tableName;
|
||||
private String label;
|
||||
private String associationPath; // null/empty for main table, then associationName for a child, associationName.associationName for a grandchild
|
||||
private Boolean isBulkEdit;
|
||||
private String keyFields;
|
||||
private ArrayList<String> possibleKeyFields;
|
||||
|
||||
private ArrayList<QFieldMetaData> fields; // mmm, not marked as serializable (at this time) - is okay?
|
||||
private ArrayList<BulkLoadTableStructure> associations;
|
||||
@ -275,98 +272,4 @@ public class BulkLoadTableStructure implements Serializable
|
||||
}
|
||||
this.associations.add(association);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for isBulkEdit
|
||||
*******************************************************************************/
|
||||
public Boolean getIsBulkEdit()
|
||||
{
|
||||
return (this.isBulkEdit);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for isBulkEdit
|
||||
*******************************************************************************/
|
||||
public void setIsBulkEdit(Boolean isBulkEdit)
|
||||
{
|
||||
this.isBulkEdit = isBulkEdit;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for isBulkEdit
|
||||
*******************************************************************************/
|
||||
public BulkLoadTableStructure withIsBulkEdit(Boolean isBulkEdit)
|
||||
{
|
||||
this.isBulkEdit = isBulkEdit;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for keyFields
|
||||
*******************************************************************************/
|
||||
public String getKeyFields()
|
||||
{
|
||||
return (this.keyFields);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for keyFields
|
||||
*******************************************************************************/
|
||||
public void setKeyFields(String keyFields)
|
||||
{
|
||||
this.keyFields = keyFields;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for keyFields
|
||||
*******************************************************************************/
|
||||
public BulkLoadTableStructure withKeyFields(String keyFields)
|
||||
{
|
||||
this.keyFields = keyFields;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for possibleKeyFields
|
||||
*******************************************************************************/
|
||||
public ArrayList<String> getPossibleKeyFields()
|
||||
{
|
||||
return (this.possibleKeyFields);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for possibleKeyFields
|
||||
*******************************************************************************/
|
||||
public void setPossibleKeyFields(ArrayList<String> possibleKeyFields)
|
||||
{
|
||||
this.possibleKeyFields = possibleKeyFields;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for possibleKeyFields
|
||||
*******************************************************************************/
|
||||
public BulkLoadTableStructure withPossibleKeyFields(ArrayList<String> possibleKeyFields)
|
||||
{
|
||||
this.possibleKeyFields = possibleKeyFields;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -45,7 +45,6 @@ import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.savedbulkloadprofiles.SavedBulkLoadProfile;
|
||||
import org.apache.commons.lang.BooleanUtils;
|
||||
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||
|
||||
|
||||
@ -104,28 +103,12 @@ public class QuerySavedBulkLoadProfileProcess implements BackendStep
|
||||
else
|
||||
{
|
||||
String tableName = runBackendStepInput.getValueString("tableName");
|
||||
boolean isBulkEdit = BooleanUtils.isTrue(runBackendStepInput.getValueBoolean("isBulkEdit"));
|
||||
|
||||
QueryInput input = new QueryInput();
|
||||
input.setTableName(SavedBulkLoadProfile.TABLE_NAME);
|
||||
|
||||
QQueryFilter filter = new QQueryFilter()
|
||||
input.setFilter(new QQueryFilter()
|
||||
.withCriteria(new QFilterCriteria("tableName", QCriteriaOperator.EQUALS, tableName))
|
||||
.withOrderBy(new QFilterOrderBy("label"));
|
||||
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
// account for nulls here, so if is bulk edit, only look for true, //
|
||||
// otherwise look for nulls or not equal to true //
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
if(isBulkEdit)
|
||||
{
|
||||
filter.withCriteria(new QFilterCriteria("isBulkEdit", QCriteriaOperator.EQUALS, true));
|
||||
}
|
||||
else
|
||||
{
|
||||
filter.withCriteria(new QFilterCriteria("isBulkEdit", QCriteriaOperator.NOT_EQUALS_OR_IS_NULL, true));
|
||||
}
|
||||
input.setFilter(filter);
|
||||
.withOrderBy(new QFilterOrderBy("label")));
|
||||
|
||||
QueryOutput output = new QueryAction().execute(input);
|
||||
runBackendStepOutput.setRecords(output.getRecords());
|
||||
|
@ -50,7 +50,6 @@ import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaD
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.savedbulkloadprofiles.SavedBulkLoadProfile;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import org.apache.commons.lang.BooleanUtils;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -91,7 +90,6 @@ public class StoreSavedBulkLoadProfileProcess implements BackendStep
|
||||
String userId = QContext.getQSession().getUser().getIdReference();
|
||||
String tableName = runBackendStepInput.getValueString("tableName");
|
||||
String label = runBackendStepInput.getValueString("label");
|
||||
Boolean isBulkEdit = BooleanUtils.isTrue(runBackendStepInput.getValueBoolean("isBulkEdit"));
|
||||
|
||||
String mappingJson = processMappingJson(runBackendStepInput.getValueString("mappingJson"));
|
||||
|
||||
@ -100,7 +98,6 @@ public class StoreSavedBulkLoadProfileProcess implements BackendStep
|
||||
.withValue("mappingJson", mappingJson)
|
||||
.withValue("label", label)
|
||||
.withValue("tableName", tableName)
|
||||
.withValue("isBulkEdit", isBulkEdit)
|
||||
.withValue("userId", userId);
|
||||
|
||||
List<QRecord> savedBulkLoadProfileList;
|
||||
|
@ -0,0 +1,276 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.scheduler.processes;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.ActionFlag;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerInterface;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.scheduleing.QSchedulerMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.scheduledjobs.ScheduledJob;
|
||||
import com.kingsrook.qqq.backend.core.model.scheduledjobs.ScheduledJobParameter;
|
||||
import com.kingsrook.qqq.backend.core.model.scheduledjobs.ScheduledJobType;
|
||||
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.StreamedETLWithFrontendProcess;
|
||||
import com.kingsrook.qqq.backend.core.processes.implementations.tablesync.AbstractTableSyncTransformStep;
|
||||
import com.kingsrook.qqq.backend.core.processes.implementations.tablesync.TableSyncProcess;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.collections.ListBuilder;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
* Base class to manage creating scheduled jobs based on records in another table
|
||||
*
|
||||
* Expected to be used via BaseSyncToScheduledJobTableCustomizer - see its javadoc.
|
||||
* @see BaseSyncToScheduledJobTableCustomizer
|
||||
*******************************************************************************/
|
||||
public abstract class AbstractRecordSyncToScheduledJobProcess extends AbstractTableSyncTransformStep implements MetaDataProducerInterface<QProcessMetaData>
|
||||
{
|
||||
private static final QLogger LOG = QLogger.getLogger(AbstractRecordSyncToScheduledJobProcess.class);
|
||||
|
||||
public static final String SCHEDULER_NAME_FIELD_NAME = "schedulerName";
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* action flags that can be put in an insert/update/delete input to control
|
||||
* behavior of this process.
|
||||
***************************************************************************/
|
||||
public enum ActionFlags implements ActionFlag
|
||||
{
|
||||
/***************************************************************************
|
||||
* tell this process not to run upon such an action taken on the source table.
|
||||
***************************************************************************/
|
||||
DO_NOT_SYNC
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public QProcessMetaData produce(QInstance qInstance) throws QException
|
||||
{
|
||||
QProcessMetaData processMetaData = TableSyncProcess.processMetaDataBuilder(false)
|
||||
.withName(getClass().getSimpleName())
|
||||
.withSyncTransformStepClass(getClass())
|
||||
.withReviewStepRecordFields(List.of(
|
||||
new QFieldMetaData(getRecordForeignKeyFieldName(), QFieldType.INTEGER).withPossibleValueSourceName(getRecordForeignKeyPossibleValueSourceName()),
|
||||
new QFieldMetaData("cronExpression", QFieldType.STRING),
|
||||
new QFieldMetaData("isActive", QFieldType.BOOLEAN)
|
||||
))
|
||||
.getProcessMetaData();
|
||||
|
||||
processMetaData.getBackendStep(StreamedETLWithFrontendProcess.STEP_NAME_PREVIEW).getInputMetaData()
|
||||
.withField(new QFieldMetaData(SCHEDULER_NAME_FIELD_NAME, QFieldType.STRING));
|
||||
|
||||
return (processMetaData);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public QRecord populateRecordToStore(RunBackendStepInput runBackendStepInput, QRecord destinationRecord, QRecord sourceRecord) throws QException
|
||||
{
|
||||
ScheduledJob scheduledJob;
|
||||
if(destinationRecord == null || destinationRecord.getValue("id") == null)
|
||||
{
|
||||
QInstance qInstance = QContext.getQInstance();
|
||||
|
||||
////////////////////////////////////////////////////////////////
|
||||
// this is the table at which the scheduled job will point to //
|
||||
////////////////////////////////////////////////////////////////
|
||||
QTableMetaData sourceTableMetaData = qInstance.getTable(getSourceTableName());
|
||||
String sourceTableId = String.valueOf(sourceRecord.getValueString(sourceTableMetaData.getPrimaryKeyField()));
|
||||
String sourceTableJobKey = getSourceTableName() + "Id";
|
||||
|
||||
///////////////////////////////////////////////////////////
|
||||
// this is the table that the scheduled record points to //
|
||||
///////////////////////////////////////////////////////////
|
||||
QTableMetaData recordForeignTableMetaData = qInstance.getTable(getRecordForeignKeyPossibleValueSourceName());
|
||||
String sourceRecordForeignKeyId = sourceRecord.getValueString(getRecordForeignKeyFieldName());
|
||||
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
// need to do an insert - set lots of key values in the scheduled job //
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
scheduledJob = new ScheduledJob();
|
||||
scheduledJob.setSchedulerName(runBackendStepInput.getValueString(SCHEDULER_NAME_FIELD_NAME));
|
||||
scheduledJob.setType(ScheduledJobType.PROCESS.name());
|
||||
scheduledJob.setForeignKeyType(getSourceTableName());
|
||||
scheduledJob.setForeignKeyValue(sourceTableId);
|
||||
scheduledJob.setJobParameters(ListBuilder.of(
|
||||
new ScheduledJobParameter().withKey("isScheduledJob").withValue("true"),
|
||||
new ScheduledJobParameter().withKey("processName").withValue(getProcessNameScheduledJobParameter()),
|
||||
new ScheduledJobParameter().withKey(sourceTableJobKey).withValue(sourceTableId),
|
||||
new ScheduledJobParameter().withKey("recordId").withValue(ValueUtils.getValueAsString(sourceRecordForeignKeyId))
|
||||
));
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// make a call to allow subclasses to customize parts of the job record //
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
scheduledJob.setLabel(recordForeignTableMetaData.getLabel() + " " + sourceRecordForeignKeyId);
|
||||
scheduledJob.setDescription("Job to run " + sourceTableMetaData.getLabel() + " Id " + sourceTableId
|
||||
+ " (which runs for " + recordForeignTableMetaData.getLabel() + " Id " + sourceRecordForeignKeyId + ")");
|
||||
}
|
||||
else
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////////////////
|
||||
// else doing an update - populate scheduled job entity from destination record //
|
||||
//////////////////////////////////////////////////////////////////////////////////
|
||||
scheduledJob = new ScheduledJob(destinationRecord);
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////
|
||||
// these fields sync on insert and update //
|
||||
// todo - if no diffs, should we return null (to avoid changing quartz at all?) //
|
||||
//////////////////////////////////////////////////////////////////////////////////
|
||||
scheduledJob.setCronExpression(sourceRecord.getValueString("cronExpression"));
|
||||
scheduledJob.setCronTimeZoneId(sourceRecord.getValueString("cronTimeZoneId"));
|
||||
scheduledJob.setIsActive(true);
|
||||
|
||||
scheduledJob = customizeScheduledJob(scheduledJob, sourceRecord);
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
// try to make sure scheduler name is set (and fail if it isn't!) //
|
||||
////////////////////////////////////////////////////////////////////
|
||||
makeSureSchedulerNameIsSet(scheduledJob);
|
||||
|
||||
return scheduledJob.toQRecord();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
protected void makeSureSchedulerNameIsSet(ScheduledJob scheduledJob) throws QException
|
||||
{
|
||||
if(!StringUtils.hasContent(scheduledJob.getSchedulerName()))
|
||||
{
|
||||
Map<String, QSchedulerMetaData> schedulers = QContext.getQInstance().getSchedulers();
|
||||
if(schedulers.size() == 1)
|
||||
{
|
||||
scheduledJob.setSchedulerName(schedulers.keySet().iterator().next());
|
||||
}
|
||||
}
|
||||
|
||||
if(!StringUtils.hasContent(scheduledJob.getSchedulerName()))
|
||||
{
|
||||
String message = "Could not determine scheduler name for webhook scheduled job.";
|
||||
LOG.warn(message);
|
||||
throw (new QException(message));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
protected ScheduledJob customizeScheduledJob(ScheduledJob scheduledJob, QRecord sourceRecord) throws QException
|
||||
{
|
||||
return (scheduledJob);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected QQueryFilter getExistingRecordQueryFilter(RunBackendStepInput runBackendStepInput, List<Serializable> sourceKeyList)
|
||||
{
|
||||
return super.getExistingRecordQueryFilter(runBackendStepInput, sourceKeyList)
|
||||
.withCriteria(new QFilterCriteria("foreignKeyType", QCriteriaOperator.EQUALS, getScheduledJobForeignKeyType()));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected SyncProcessConfig getSyncProcessConfig()
|
||||
{
|
||||
return new SyncProcessConfig(getSourceTableName(), getSourceTableKeyField(), ScheduledJob.TABLE_NAME, "foreignKeyValue", true, true);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
protected abstract String getScheduledJobForeignKeyType();
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
protected abstract String getRecordForeignKeyFieldName();
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
protected abstract String getRecordForeignKeyPossibleValueSourceName();
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
protected abstract String getSourceTableName();
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
protected abstract String getProcessNameScheduledJobParameter();
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
protected String getSourceTableKeyField()
|
||||
{
|
||||
return ("id");
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,387 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.scheduler.processes;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizerInterface;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
|
||||
import com.kingsrook.qqq.backend.core.actions.processes.QProcessCallbackFactory;
|
||||
import com.kingsrook.qqq.backend.core.actions.processes.RunProcessAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.DeleteAction;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryLineInterface;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.code.InitializableViaCodeReference;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReferenceWithProperties;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.scheduledjobs.ScheduledJob;
|
||||
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.StreamedETLWithFrontendProcess;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** an implementation of a TableCustomizer that runs a subclass of
|
||||
** AbstractRecordSyncToScheduledJobProcess - to manage scheduledJob records that
|
||||
** correspond to records in another table (e.g., a job for each Client)
|
||||
**
|
||||
** Easiest way to use is:
|
||||
** - BaseSyncToScheduledJobTableCustomizer.setTableCustomizers(tableMetaData, new YourSyncScheduledJobProcessSubclass());
|
||||
** which adds post-insert, -update, and -delete customizers to your table.
|
||||
**
|
||||
** If you need additional table customizer code in those slots, I suppose you could
|
||||
** simply make your customizer create an instance of this class, set its
|
||||
** properties, and run its appropriate postInsertOrUpdate/postDelete methods.
|
||||
*******************************************************************************/
|
||||
public class BaseSyncToScheduledJobTableCustomizer implements TableCustomizerInterface, InitializableViaCodeReference
|
||||
{
|
||||
private static final QLogger LOG = QLogger.getLogger(BaseSyncToScheduledJobTableCustomizer.class);
|
||||
|
||||
public static final String KEY_TABLE_NAME = "tableName";
|
||||
public static final String KEY_SYNC_PROCESS_NAME = "syncProcessName";
|
||||
public static final String KEY_SCHEDULED_JOB_FOREIGN_KEY_TYPE = "scheduledJobForeignKeyType";
|
||||
|
||||
private String tableName;
|
||||
private String syncProcessName;
|
||||
private String scheduledJobForeignKeyType;
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* Create a {@link QCodeReferenceWithProperties} that can be used to add this
|
||||
* class to a table.
|
||||
*
|
||||
* If this is the only customizer for the post insert/update/delete events
|
||||
* on your table, you can instead call setTableCustomizers. But if you want,
|
||||
* for example, a sync-scheduled-job (what this customizer does) plus some other
|
||||
* customizers, then you can call this method to get a code reference that you
|
||||
* can add, for example, to {@link com.kingsrook.qqq.backend.core.actions.customizers.MultiCustomizer}
|
||||
*
|
||||
* @param tableMetaData the table that the customizer will be used on.
|
||||
* @param syncProcess instance of the subclass of AbstractRecordSyncToScheduledJobProcess
|
||||
* that should run in the table's post insert/update/delete
|
||||
* events.
|
||||
* @see #setTableCustomizers(QTableMetaData, AbstractRecordSyncToScheduledJobProcess)
|
||||
***************************************************************************/
|
||||
public static QCodeReferenceWithProperties makeCodeReference(QTableMetaData tableMetaData, AbstractRecordSyncToScheduledJobProcess syncProcess)
|
||||
{
|
||||
return new QCodeReferenceWithProperties(BaseSyncToScheduledJobTableCustomizer.class, Map.of(
|
||||
KEY_TABLE_NAME, tableMetaData.getName(),
|
||||
KEY_SYNC_PROCESS_NAME, syncProcess.getClass().getSimpleName(),
|
||||
KEY_SCHEDULED_JOB_FOREIGN_KEY_TYPE, syncProcess.getScheduledJobForeignKeyType()
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* Add post insert/update/delete customizers to a table, that will run a
|
||||
* sync-scheduled-job process.
|
||||
*
|
||||
* @param tableMetaData the table that the customizer will be used on.
|
||||
* @param syncProcess instance of the subclass of AbstractRecordSyncToScheduledJobProcess
|
||||
* that should run in the table's post insert/update/delete
|
||||
* events.
|
||||
***************************************************************************/
|
||||
public static void setTableCustomizers(QTableMetaData tableMetaData, AbstractRecordSyncToScheduledJobProcess syncProcess)
|
||||
{
|
||||
QCodeReference codeReference = makeCodeReference(tableMetaData, syncProcess);
|
||||
tableMetaData.withCustomizer(TableCustomizers.POST_INSERT_RECORD, codeReference);
|
||||
tableMetaData.withCustomizer(TableCustomizers.POST_UPDATE_RECORD, codeReference);
|
||||
tableMetaData.withCustomizer(TableCustomizers.POST_DELETE_RECORD, codeReference);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public void initialize(QCodeReference codeReference)
|
||||
{
|
||||
if(codeReference instanceof QCodeReferenceWithProperties codeReferenceWithProperties)
|
||||
{
|
||||
tableName = ValueUtils.getValueAsString(codeReferenceWithProperties.getProperties().get(KEY_TABLE_NAME));
|
||||
syncProcessName = ValueUtils.getValueAsString(codeReferenceWithProperties.getProperties().get(KEY_SYNC_PROCESS_NAME));
|
||||
scheduledJobForeignKeyType = ValueUtils.getValueAsString(codeReferenceWithProperties.getProperties().get(KEY_SCHEDULED_JOB_FOREIGN_KEY_TYPE));
|
||||
|
||||
if(!StringUtils.hasContent(tableName))
|
||||
{
|
||||
LOG.warn("Missing property under KEY_TABLE_NAME [" + KEY_TABLE_NAME + "] in codeReference for BaseSyncToScheduledJobTableCustomizer");
|
||||
}
|
||||
|
||||
if(!StringUtils.hasContent(syncProcessName))
|
||||
{
|
||||
LOG.warn("Missing property under KEY_SYNC_PROCESS_NAME [" + KEY_SYNC_PROCESS_NAME + "] in codeReference for BaseSyncToScheduledJobTableCustomizer");
|
||||
}
|
||||
|
||||
if(!StringUtils.hasContent(scheduledJobForeignKeyType))
|
||||
{
|
||||
LOG.warn("Missing property under KEY_SCHEDULED_JOB_FOREIGN_KEY_TYPE [" + KEY_SCHEDULED_JOB_FOREIGN_KEY_TYPE + "] in codeReference for BaseSyncToScheduledJobTableCustomizer");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public List<QRecord> postInsertOrUpdate(AbstractActionInput input, List<QRecord> records, Optional<List<QRecord>> oldRecordList) throws QException
|
||||
{
|
||||
if(input instanceof UpdateInput updateInput && updateInput.hasFlag(AbstractRecordSyncToScheduledJobProcess.ActionFlags.DO_NOT_SYNC))
|
||||
{
|
||||
return records;
|
||||
}
|
||||
|
||||
if(input instanceof InsertInput insertInput && insertInput.hasFlag(AbstractRecordSyncToScheduledJobProcess.ActionFlags.DO_NOT_SYNC))
|
||||
{
|
||||
return records;
|
||||
}
|
||||
|
||||
runSyncProcessForRecordList(records, syncProcessName);
|
||||
return records;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public List<QRecord> postDelete(DeleteInput deleteInput, List<QRecord> records) throws QException
|
||||
{
|
||||
deleteScheduledJobsForRecordList(records);
|
||||
return records;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* Run the named process over a set of records (e.g., that were inserted or
|
||||
* updated).
|
||||
*
|
||||
* This method is normally called from within this class, in postInsertOrUpdate.
|
||||
*
|
||||
* Note that if the {@link ScheduledJob} table isn't defined in the QInstance,
|
||||
* that the process will not be called.
|
||||
*
|
||||
* @param records list of records to use as source records in the table-sync
|
||||
* to the scheduledJob table.
|
||||
* @param processName name of the sync-process to run.
|
||||
***************************************************************************/
|
||||
public void runSyncProcessForRecordList(List<QRecord> records, String processName)
|
||||
{
|
||||
if(QContext.getQInstance().getTable(ScheduledJob.TABLE_NAME) == null)
|
||||
{
|
||||
LOG.info("ScheduledJob table not found, skipping scheduled job sync.");
|
||||
return;
|
||||
}
|
||||
|
||||
String primaryKeyField = QContext.getQInstance().getTable(tableName).getPrimaryKeyField();
|
||||
|
||||
List<Serializable> sourceRecordIds = records.stream()
|
||||
.filter(r -> CollectionUtils.nullSafeIsEmpty(r.getErrors()))
|
||||
.map(r -> r.getValue(primaryKeyField))
|
||||
.filter(Objects::nonNull).toList();
|
||||
|
||||
if(CollectionUtils.nullSafeIsEmpty(sourceRecordIds))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
RunProcessInput runProcessInput = new RunProcessInput();
|
||||
runProcessInput.setProcessName(processName);
|
||||
runProcessInput.setCallback(QProcessCallbackFactory.forPrimaryKeys("id", sourceRecordIds));
|
||||
runProcessInput.setFrontendStepBehavior(RunProcessInput.FrontendStepBehavior.SKIP);
|
||||
RunProcessOutput runProcessOutput = new RunProcessAction().execute(runProcessInput);
|
||||
|
||||
Serializable processSummary = runProcessOutput.getValue(StreamedETLWithFrontendProcess.FIELD_PROCESS_SUMMARY);
|
||||
ProcessSummaryLineInterface.log("Sync to ScheduledJob Process Summary", processSummary, List.of(logPair("sourceTable", tableName)));
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOG.warn("Error syncing records to scheduled jobs", e, logPair("sourceTable", tableName), logPair("sourceRecordIds", sourceRecordIds));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* Delete scheduled job records for source-table records that have been deleted.
|
||||
*
|
||||
* This method is normally called from within this class, in postDelete.
|
||||
*
|
||||
* Note that if the {@link ScheduledJob} table isn't defined in the QInstance,
|
||||
* that the process will not be called.
|
||||
*
|
||||
* @param records list of records to use as foreign-key sources to identify
|
||||
* scheduledJob records to delete
|
||||
***************************************************************************/
|
||||
public void deleteScheduledJobsForRecordList(List<QRecord> records)
|
||||
{
|
||||
if(QContext.getQInstance().getTable(ScheduledJob.TABLE_NAME) == null)
|
||||
{
|
||||
LOG.info("ScheduledJob table not found, skipping scheduled job delete.");
|
||||
return;
|
||||
}
|
||||
|
||||
List<String> sourceRecordIds = records.stream()
|
||||
.filter(r -> CollectionUtils.nullSafeIsEmpty(r.getErrors()))
|
||||
.map(r -> r.getValueString("id")).toList();
|
||||
|
||||
if(sourceRecordIds.isEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////
|
||||
// delete any corresponding scheduledJob records //
|
||||
///////////////////////////////////////////////////
|
||||
try
|
||||
{
|
||||
new DeleteAction().execute(new DeleteInput(ScheduledJob.TABLE_NAME).withQueryFilter(new QQueryFilter()
|
||||
.withCriteria(new QFilterCriteria("foreignKeyType", QCriteriaOperator.EQUALS, getScheduledJobForeignKeyType()))
|
||||
.withCriteria(new QFilterCriteria("foreignKeyValue", QCriteriaOperator.IN, sourceRecordIds))));
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOG.warn("Error deleting scheduled jobs for scheduled records", e, logPair("sourceTable", tableName), logPair("sourceRecordIds", sourceRecordIds));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for tableName
|
||||
*******************************************************************************/
|
||||
public String getTableName()
|
||||
{
|
||||
return (this.tableName);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for tableName
|
||||
*******************************************************************************/
|
||||
public void setTableName(String tableName)
|
||||
{
|
||||
this.tableName = tableName;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for tableName
|
||||
*******************************************************************************/
|
||||
public BaseSyncToScheduledJobTableCustomizer withTableName(String tableName)
|
||||
{
|
||||
this.tableName = tableName;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for syncProcessName
|
||||
*******************************************************************************/
|
||||
public String getSyncProcessName()
|
||||
{
|
||||
return (this.syncProcessName);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for syncProcessName
|
||||
*******************************************************************************/
|
||||
public void setSyncProcessName(String syncProcessName)
|
||||
{
|
||||
this.syncProcessName = syncProcessName;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for syncProcessName
|
||||
*******************************************************************************/
|
||||
public BaseSyncToScheduledJobTableCustomizer withSyncProcessName(String syncProcessName)
|
||||
{
|
||||
this.syncProcessName = syncProcessName;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for scheduledJobForeignKeyType
|
||||
*******************************************************************************/
|
||||
public String getScheduledJobForeignKeyType()
|
||||
{
|
||||
return (this.scheduledJobForeignKeyType);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for scheduledJobForeignKeyType
|
||||
*******************************************************************************/
|
||||
public void setScheduledJobForeignKeyType(String scheduledJobForeignKeyType)
|
||||
{
|
||||
this.scheduledJobForeignKeyType = scheduledJobForeignKeyType;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for scheduledJobForeignKeyType
|
||||
*******************************************************************************/
|
||||
public BaseSyncToScheduledJobTableCustomizer withScheduledJobForeignKeyType(String scheduledJobForeignKeyType)
|
||||
{
|
||||
this.scheduledJobForeignKeyType = scheduledJobForeignKeyType;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,76 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2024. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.utils.collections;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Map;
|
||||
import java.util.function.Supplier;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
|
||||
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Version of map where string keys are handled case-insensitively. e.g.,
|
||||
** map.put("One", 1); map.get("ONE") == 1.
|
||||
*******************************************************************************/
|
||||
public class TypeTolerantKeyMap<V> extends TransformedKeyMap<Serializable, Serializable, V>
|
||||
{
|
||||
|
||||
/***************************************************************************
|
||||
*
|
||||
***************************************************************************/
|
||||
public TypeTolerantKeyMap(QFieldType qFieldType)
|
||||
{
|
||||
super(key -> ValueUtils.getValueAsFieldType(qFieldType, key));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
*
|
||||
***************************************************************************/
|
||||
public TypeTolerantKeyMap(QFieldType qFieldType, Supplier<Map<Serializable, V>> supplier)
|
||||
{
|
||||
super(key -> ValueUtils.getValueAsFieldType(qFieldType, key), supplier);
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
*
|
||||
***************************************************************************/
|
||||
public TypeTolerantKeyMap(Class<? extends Serializable> c)
|
||||
{
|
||||
super(key -> ValueUtils.getValueAsType(c, key));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
*
|
||||
***************************************************************************/
|
||||
public TypeTolerantKeyMap(Class<? extends Serializable> c, Supplier<Map<Serializable, V>> supplier)
|
||||
{
|
||||
super(key -> ValueUtils.getValueAsType(c, key), supplier);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,141 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.actions.customizers;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import com.kingsrook.qqq.backend.core.BaseTest;
|
||||
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.tables.insert.InsertInput;
|
||||
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.code.QCodeReference;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReferenceWithProperties;
|
||||
import com.kingsrook.qqq.backend.core.utils.TestUtils;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Unit test for MultiCustomizer
|
||||
*******************************************************************************/
|
||||
class MultiCustomizerTest extends BaseTest
|
||||
{
|
||||
private static List<String> events = new ArrayList<>();
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@BeforeEach
|
||||
@AfterEach
|
||||
void beforeAndAfterEach()
|
||||
{
|
||||
events.clear();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void test() throws QException
|
||||
{
|
||||
QInstance qInstance = TestUtils.defineInstance();
|
||||
qInstance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY)
|
||||
.withCustomizer(TableCustomizers.PRE_INSERT_RECORD, MultiCustomizer.of(
|
||||
new QCodeReference(CustomizerA.class),
|
||||
new QCodeReference(CustomizerB.class)
|
||||
));
|
||||
reInitInstanceInContext(qInstance);
|
||||
|
||||
new InsertAction().execute(new InsertInput(TestUtils.TABLE_NAME_PERSON_MEMORY).withRecord(new QRecord()));
|
||||
assertThat(events).hasSize(2)
|
||||
.contains("CustomizerA.preInsert")
|
||||
.contains("CustomizerB.preInsert");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testAddingMore() throws QException
|
||||
{
|
||||
QInstance qInstance = TestUtils.defineInstance();
|
||||
|
||||
QCodeReferenceWithProperties multiCustomizer = MultiCustomizer.of(new QCodeReference(CustomizerA.class));
|
||||
MultiCustomizer.addTableCustomizer(multiCustomizer, new QCodeReference(CustomizerB.class));
|
||||
|
||||
qInstance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY).withCustomizer(TableCustomizers.PRE_INSERT_RECORD, multiCustomizer);
|
||||
reInitInstanceInContext(qInstance);
|
||||
|
||||
new InsertAction().execute(new InsertInput(TestUtils.TABLE_NAME_PERSON_MEMORY).withRecord(new QRecord()));
|
||||
assertThat(events).hasSize(2)
|
||||
.contains("CustomizerA.preInsert")
|
||||
.contains("CustomizerB.preInsert");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
public static class CustomizerA implements TableCustomizerInterface
|
||||
{
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public List<QRecord> preInsert(InsertInput insertInput, List<QRecord> records, boolean isPreview) throws QException
|
||||
{
|
||||
events.add("CustomizerA.preInsert");
|
||||
return (records);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
public static class CustomizerB implements TableCustomizerInterface
|
||||
{
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public List<QRecord> preInsert(InsertInput insertInput, List<QRecord> records, boolean isPreview) throws QException
|
||||
{
|
||||
events.add("CustomizerB.preInsert");
|
||||
return (records);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,70 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.actions.customizers;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import com.kingsrook.qqq.backend.core.BaseTest;
|
||||
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.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Unit test for OldRecordHelper
|
||||
*******************************************************************************/
|
||||
class OldRecordHelperTest extends BaseTest
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void test()
|
||||
{
|
||||
OldRecordHelper oldRecordHelper = new OldRecordHelper(TestUtils.TABLE_NAME_PERSON_MEMORY, Optional.of(List.of(
|
||||
new QRecord().withValue("id", 1)
|
||||
)));
|
||||
|
||||
assertTrue(oldRecordHelper.getOldRecord(new QRecord().withValue("id", 1)).isPresent());
|
||||
assertTrue(oldRecordHelper.getOldRecord(new QRecord().withValue("id", "1")).isPresent());
|
||||
assertFalse(oldRecordHelper.getOldRecord(new QRecord().withValue("id", 2)).isPresent());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testEmptyOldRecords()
|
||||
{
|
||||
OldRecordHelper oldRecordHelper = new OldRecordHelper(TestUtils.TABLE_NAME_PERSON_MEMORY, Optional.empty());
|
||||
assertFalse(oldRecordHelper.getOldRecord(new QRecord().withValue("id", 1)).isPresent());
|
||||
assertFalse(oldRecordHelper.getOldRecord(new QRecord().withValue("id", "1")).isPresent());
|
||||
assertFalse(oldRecordHelper.getOldRecord(new QRecord().withValue("id", 2)).isPresent());
|
||||
}
|
||||
|
||||
}
|
@ -252,7 +252,7 @@ class MetaDataActionTest extends BaseTest
|
||||
// with several permissions set, we should see some things, and they should have permissions turned on //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
assertEquals(Set.of("person"), result.getTables().keySet());
|
||||
assertEquals(Set.of("increaseBirthdate", "runShapesPersonReport", "person.bulkInsert", "person.bulkEdit", "person.bulkEditWithFile", "person.bulkDelete"), result.getProcesses().keySet());
|
||||
assertEquals(Set.of("increaseBirthdate", "runShapesPersonReport", "person.bulkInsert", "person.bulkEdit", "person.bulkDelete"), result.getProcesses().keySet());
|
||||
assertEquals(Set.of("shapesPersonReport", "personJoinShapeReport", "simplePersonReport"), result.getReports().keySet());
|
||||
assertEquals(Set.of("PersonsByCreateDateBarChart"), result.getWidgets().keySet());
|
||||
|
||||
@ -288,7 +288,7 @@ class MetaDataActionTest extends BaseTest
|
||||
|
||||
assertEquals(Set.of("person", "personFile", "personMemory"), result.getTables().keySet());
|
||||
|
||||
assertEquals(Set.of("increaseBirthdate", "personFile.bulkInsert", "personFile.bulkEdit", "personFile.bulkEditWithFile", "personFile.bulkDelete", "personMemory.bulkInsert", "personMemory.bulkEdit", "personMemory.bulkEditWithFile", "personMemory.bulkDelete"), result.getProcesses().keySet());
|
||||
assertEquals(Set.of("increaseBirthdate", "personFile.bulkInsert", "personFile.bulkEdit", "personFile.bulkDelete", "personMemory.bulkInsert", "personMemory.bulkEdit", "personMemory.bulkDelete"), result.getProcesses().keySet());
|
||||
assertEquals(Set.of(), result.getReports().keySet());
|
||||
assertEquals(Set.of(), result.getWidgets().keySet());
|
||||
|
||||
@ -335,7 +335,7 @@ class MetaDataActionTest extends BaseTest
|
||||
MetaDataOutput result = new MetaDataAction().execute(new MetaDataInput());
|
||||
|
||||
assertEquals(Set.of("person", "personFile", "personMemory"), result.getTables().keySet());
|
||||
assertEquals(Set.of("increaseBirthdate", "personFile.bulkInsert", "personFile.bulkEdit", "personFile.bulkEditWithFile", "personMemory.bulkDelete"), result.getProcesses().keySet());
|
||||
assertEquals(Set.of("increaseBirthdate", "personFile.bulkInsert", "personFile.bulkEdit", "personMemory.bulkDelete"), result.getProcesses().keySet());
|
||||
assertEquals(Set.of(), result.getReports().keySet());
|
||||
assertEquals(Set.of(), result.getWidgets().keySet());
|
||||
|
||||
|
@ -0,0 +1,150 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.actions.tables;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import com.kingsrook.qqq.backend.core.BaseTest;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizerInterface;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
|
||||
import com.kingsrook.qqq.backend.core.model.statusmessages.SystemErrorStatusMessage;
|
||||
import com.kingsrook.qqq.backend.core.utils.ListingHash;
|
||||
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.assertNull;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class InsertActionInstanceLevelTableCustomizersTest extends BaseTest
|
||||
{
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testInstanceLevelCustomizers() throws QException
|
||||
{
|
||||
QContext.getQInstance().withTableCustomizer(TableCustomizers.PRE_INSERT_RECORD, new QCodeReference(BreaksEverythingCustomizer.class));
|
||||
QRecord record = new InsertAction().execute(new InsertInput(TestUtils.TABLE_NAME_SHAPE).withRecord(new QRecord().withValue("name", "octogon"))).getRecords().get(0);
|
||||
assertEquals("Everything is broken", record.getErrorsAsString());
|
||||
assertNull(record.getValueInteger("id"));
|
||||
|
||||
QContext.getQInstance().setTableCustomizers(new ListingHash<>());
|
||||
QContext.getQInstance().withTableCustomizer(TableCustomizers.PRE_INSERT_RECORD, new QCodeReference(SetsFirstName.class));
|
||||
QContext.getQInstance().withTableCustomizer(TableCustomizers.PRE_INSERT_RECORD, new QCodeReference(SetsLastName.class));
|
||||
QContext.getQInstance().withTableCustomizer(TableCustomizers.POST_INSERT_RECORD, new QCodeReference(DoesNothing.class));
|
||||
DoesNothing.callCount = 0;
|
||||
record = new InsertAction().execute(new InsertInput(TestUtils.TABLE_NAME_SHAPE).withRecord(new QRecord().withValue("name", "octogon"))).getRecords().get(0);
|
||||
assertEquals("Jeff", record.getValueString("firstName"));
|
||||
assertEquals("Smith", record.getValueString("lastName"));
|
||||
assertNotNull(record.getValueInteger("id"));
|
||||
assertEquals(1, DoesNothing.callCount);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static class BreaksEverythingCustomizer implements TableCustomizerInterface
|
||||
{
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public List<QRecord> preInsertOrUpdate(AbstractActionInput input, List<QRecord> records, boolean isPreview, Optional<List<QRecord>> oldRecordList) throws QException
|
||||
{
|
||||
records.forEach(r -> r.addError(new SystemErrorStatusMessage("Everything is broken")));
|
||||
return records;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static class SetsFirstName implements TableCustomizerInterface
|
||||
{
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public List<QRecord> preInsertOrUpdate(AbstractActionInput input, List<QRecord> records, boolean isPreview, Optional<List<QRecord>> oldRecordList) throws QException
|
||||
{
|
||||
records.forEach(r -> r.setValue("firstName", "Jeff"));
|
||||
return records;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static class SetsLastName implements TableCustomizerInterface
|
||||
{
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public List<QRecord> preInsertOrUpdate(AbstractActionInput input, List<QRecord> records, boolean isPreview, Optional<List<QRecord>> oldRecordList) throws QException
|
||||
{
|
||||
records.forEach(r -> r.setValue("lastName", "Smith"));
|
||||
return records;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static class DoesNothing implements TableCustomizerInterface
|
||||
{
|
||||
static int callCount = 0;
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
*
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public List<QRecord> postInsertOrUpdate(AbstractActionInput input, List<QRecord> records, Optional<List<QRecord>> oldRecordList) throws QException
|
||||
{
|
||||
callCount++;
|
||||
return records;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,70 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.actions.tables;
|
||||
|
||||
|
||||
import com.kingsrook.qqq.backend.core.BaseTest;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
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.update.UpdateInput;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
|
||||
import com.kingsrook.qqq.backend.core.utils.ListingHash;
|
||||
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;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class UpdateActionInstanceLevelTableCustomizersTest extends BaseTest
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testInstanceLevelCustomizers() throws QException
|
||||
{
|
||||
QRecord record = new InsertAction().execute(new InsertInput(TestUtils.TABLE_NAME_SHAPE).withRecord(new QRecord().withValue("name", "octogon"))).getRecords().get(0);
|
||||
|
||||
QContext.getQInstance().withTableCustomizer(TableCustomizers.PRE_UPDATE_RECORD, new QCodeReference(InsertActionInstanceLevelTableCustomizersTest.BreaksEverythingCustomizer.class));
|
||||
record = new UpdateAction().execute(new UpdateInput(TestUtils.TABLE_NAME_SHAPE).withRecord(new QRecord().withValue("id", record.getValue("id")).withValue("name", "octogon"))).getRecords().get(0);
|
||||
assertEquals("Everything is broken", record.getErrorsAsString());
|
||||
|
||||
QContext.getQInstance().setTableCustomizers(new ListingHash<>());
|
||||
QContext.getQInstance().withTableCustomizer(TableCustomizers.PRE_UPDATE_RECORD, new QCodeReference(InsertActionInstanceLevelTableCustomizersTest.SetsFirstName.class));
|
||||
QContext.getQInstance().withTableCustomizer(TableCustomizers.PRE_UPDATE_RECORD, new QCodeReference(InsertActionInstanceLevelTableCustomizersTest.SetsLastName.class));
|
||||
QContext.getQInstance().withTableCustomizer(TableCustomizers.POST_UPDATE_RECORD, new QCodeReference(InsertActionInstanceLevelTableCustomizersTest.DoesNothing.class));
|
||||
InsertActionInstanceLevelTableCustomizersTest.DoesNothing.callCount = 0;
|
||||
record = new UpdateAction().execute(new UpdateInput(TestUtils.TABLE_NAME_SHAPE).withRecord(new QRecord().withValue("id", record.getValue("id")).withValue("name", "octogon"))).getRecords().get(0);
|
||||
assertEquals("Jeff", record.getValueString("firstName"));
|
||||
assertEquals("Smith", record.getValueString("lastName"));
|
||||
assertNotNull(record.getValueInteger("id"));
|
||||
assertEquals(1, InsertActionInstanceLevelTableCustomizersTest.DoesNothing.callCount);
|
||||
}
|
||||
|
||||
}
|
@ -23,14 +23,22 @@ package com.kingsrook.qqq.backend.core.actions.tables.helpers;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qqq.backend.core.BaseTest;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.helpers.ValidateRecordSecurityLockHelper.RecordWithErrors;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.security.MultiRecordSecurityLock;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.session.QSession;
|
||||
import com.kingsrook.qqq.backend.core.model.statusmessages.BadInputStatusMessage;
|
||||
import com.kingsrook.qqq.backend.core.utils.TestUtils;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static com.kingsrook.qqq.backend.core.model.metadata.security.MultiRecordSecurityLock.BooleanOperator.AND;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -106,4 +114,29 @@ class ValidateRecordSecurityLockHelperTest extends BaseTest
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testAllowedToReadRecord() throws QException
|
||||
{
|
||||
QTableMetaData table = QContext.getQInstance().getTables().get(TestUtils.TABLE_NAME_ORDER);
|
||||
|
||||
QSession sessionWithStore1 = new QSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_TYPE_STORE, 1);
|
||||
QSession sessionWithStore2 = new QSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_TYPE_STORE, 2);
|
||||
QSession sessionWithStore1and2 = new QSession().withSecurityKeyValues(Map.of(TestUtils.SECURITY_KEY_TYPE_STORE, List.of(1, 2)));
|
||||
QSession sessionWithStoresAllAccess = new QSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_TYPE_STORE_ALL_ACCESS, true);
|
||||
QSession sessionWithNoStores = new QSession();
|
||||
|
||||
QRecord recordStore1 = new QRecord().withValue("storeId", 1);
|
||||
|
||||
assertTrue(ValidateRecordSecurityLockHelper.allowedToReadRecord(table, recordStore1, sessionWithStore1, null));
|
||||
assertFalse(ValidateRecordSecurityLockHelper.allowedToReadRecord(table, recordStore1, sessionWithStore2, null));
|
||||
assertTrue(ValidateRecordSecurityLockHelper.allowedToReadRecord(table, recordStore1, sessionWithStore1and2, null));
|
||||
assertTrue(ValidateRecordSecurityLockHelper.allowedToReadRecord(table, recordStore1, sessionWithStoresAllAccess, null));
|
||||
assertFalse(ValidateRecordSecurityLockHelper.allowedToReadRecord(table, recordStore1, sessionWithNoStores, null));
|
||||
}
|
||||
|
||||
}
|
@ -176,6 +176,30 @@ public class QInstanceValidatorTest extends BaseTest
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testInstanceLevelTableCustomizers()
|
||||
{
|
||||
assertValidationFailureReasons((qInstance) -> qInstance.withTableCustomizer(TableCustomizers.PRE_INSERT_RECORD, new QCodeReference(QInstanceValidator.class)),
|
||||
"Instance tableCustomizer of type preInsertRecord: CodeReference is not of the expected type");
|
||||
|
||||
assertValidationFailureReasons((qInstance) ->
|
||||
{
|
||||
qInstance.withTableCustomizer(TableCustomizers.POST_UPDATE_RECORD, new QCodeReference(QInstanceValidator.class));
|
||||
qInstance.withTableCustomizer(TableCustomizers.POST_UPDATE_RECORD, new QCodeReference(QInstanceValidator.class));
|
||||
qInstance.withTableCustomizer(TableCustomizers.PRE_DELETE_RECORD, new QCodeReference(QInstanceValidator.class));
|
||||
},
|
||||
"Instance tableCustomizer of type postUpdateRecord: CodeReference is not of the expected type",
|
||||
"Instance tableCustomizer of type postUpdateRecord: CodeReference is not of the expected type",
|
||||
"Instance tableCustomizer of type preDeleteRecord: CodeReference is not of the expected type");
|
||||
|
||||
assertValidationSuccess((qInstance) -> qInstance.withTableCustomizer(TableCustomizers.POST_UPDATE_RECORD, new QCodeReference(CustomizerValid.class)));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Test an instance with null backends - should throw.
|
||||
**
|
||||
|
@ -80,7 +80,7 @@ class TablesCustomPossibleValueProviderTest extends BaseTest
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testGetPossibleValue()
|
||||
void testGetPossibleValue() throws QException
|
||||
{
|
||||
TablesCustomPossibleValueProvider provider = new TablesCustomPossibleValueProvider();
|
||||
|
||||
|
@ -0,0 +1,174 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.model.tables;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
import com.kingsrook.qqq.backend.core.BaseTest;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.values.SearchPossibleValueSourceInput;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
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.permissions.PermissionLevel;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.permissions.QPermissionRules;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValue;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.utils.TestUtils;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
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.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Unit test for QQQTableCustomPossibleValueProvider
|
||||
*******************************************************************************/
|
||||
class QQQTableCustomPossibleValueProviderTest extends BaseTest
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@BeforeEach
|
||||
void beforeEach() throws QException
|
||||
{
|
||||
QInstance qInstance = TestUtils.defineInstance();
|
||||
|
||||
qInstance.addTable(new QTableMetaData()
|
||||
.withName("hidden")
|
||||
.withIsHidden(true)
|
||||
.withBackendName(TestUtils.MEMORY_BACKEND_NAME)
|
||||
.withPrimaryKeyField("id")
|
||||
.withField(new QFieldMetaData("id", QFieldType.INTEGER)));
|
||||
|
||||
qInstance.addTable(new QTableMetaData()
|
||||
.withName("restricted")
|
||||
.withPermissionRules(new QPermissionRules().withLevel(PermissionLevel.HAS_ACCESS_PERMISSION))
|
||||
.withBackendName(TestUtils.MEMORY_BACKEND_NAME)
|
||||
.withPrimaryKeyField("id")
|
||||
.withField(new QFieldMetaData("id", QFieldType.INTEGER)));
|
||||
|
||||
new QQQTablesMetaDataProvider().defineAll(qInstance, TestUtils.MEMORY_BACKEND_NAME, TestUtils.MEMORY_BACKEND_NAME, null);
|
||||
|
||||
QContext.init(qInstance, newSession());
|
||||
|
||||
for(String tableName : qInstance.getTables().keySet())
|
||||
{
|
||||
QQQTableTableManager.getQQQTableId(qInstance, tableName);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testGetPossibleValue() throws QException
|
||||
{
|
||||
Integer personTableId = QQQTableTableManager.getQQQTableId(QContext.getQInstance(), TestUtils.TABLE_NAME_PERSON);
|
||||
QQQTableCustomPossibleValueProvider provider = new QQQTableCustomPossibleValueProvider();
|
||||
|
||||
QPossibleValue<Integer> possibleValue = provider.getPossibleValue(personTableId);
|
||||
assertEquals(personTableId, possibleValue.getId());
|
||||
assertEquals("Person", possibleValue.getLabel());
|
||||
|
||||
assertNull(provider.getPossibleValue(-1));
|
||||
|
||||
Integer hiddenTableId = QQQTableTableManager.getQQQTableId(QContext.getQInstance(), "hidden");
|
||||
assertNull(provider.getPossibleValue(hiddenTableId));
|
||||
|
||||
Integer restrictedTableId = QQQTableTableManager.getQQQTableId(QContext.getQInstance(), "restricted");
|
||||
assertNull(provider.getPossibleValue(restrictedTableId));
|
||||
|
||||
QContext.getQSession().withPermission("restricted.hasAccess");
|
||||
assertNotNull(provider.getPossibleValue(restrictedTableId));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testSearchPossibleValue() throws QException
|
||||
{
|
||||
Integer personTableId = QQQTableTableManager.getQQQTableId(QContext.getQInstance(), TestUtils.TABLE_NAME_PERSON);
|
||||
Integer shapeTableId = QQQTableTableManager.getQQQTableId(QContext.getQInstance(), TestUtils.TABLE_NAME_SHAPE);
|
||||
Integer hiddenTableId = QQQTableTableManager.getQQQTableId(QContext.getQInstance(), "hidden");
|
||||
Integer restrictedTableId = QQQTableTableManager.getQQQTableId(QContext.getQInstance(), "restricted");
|
||||
|
||||
QQQTableCustomPossibleValueProvider provider = new QQQTableCustomPossibleValueProvider();
|
||||
|
||||
List<QPossibleValue<Integer>> list = provider.search(new SearchPossibleValueSourceInput()
|
||||
.withPossibleValueSourceName(QQQTable.TABLE_NAME));
|
||||
assertThat(list).anyMatch(p -> p.getId().equals(personTableId));
|
||||
assertThat(list).noneMatch(p -> p.getId().equals(-1));
|
||||
assertThat(list).noneMatch(p -> p.getId().equals(hiddenTableId));
|
||||
assertThat(list).noneMatch(p -> p.getId().equals(restrictedTableId));
|
||||
assertNull(provider.getPossibleValue("restricted"));
|
||||
|
||||
list = provider.search(new SearchPossibleValueSourceInput()
|
||||
.withPossibleValueSourceName(QQQTable.TABLE_NAME)
|
||||
.withIdList(List.of(personTableId, shapeTableId, hiddenTableId)));
|
||||
assertEquals(2, list.size());
|
||||
assertThat(list).anyMatch(p -> p.getId().equals(personTableId));
|
||||
assertThat(list).anyMatch(p -> p.getId().equals(shapeTableId));
|
||||
assertThat(list).noneMatch(p -> p.getId().equals(hiddenTableId));
|
||||
|
||||
list = provider.search(new SearchPossibleValueSourceInput()
|
||||
.withPossibleValueSourceName(QQQTable.TABLE_NAME)
|
||||
.withLabelList(List.of("Person", "Shape", "Restricted")));
|
||||
assertEquals(2, list.size());
|
||||
assertThat(list).anyMatch(p -> p.getId().equals(personTableId));
|
||||
assertThat(list).anyMatch(p -> p.getId().equals(shapeTableId));
|
||||
assertThat(list).noneMatch(p -> p.getId().equals(restrictedTableId));
|
||||
|
||||
list = provider.search(new SearchPossibleValueSourceInput()
|
||||
.withPossibleValueSourceName(QQQTable.TABLE_NAME)
|
||||
.withSearchTerm("restricted"));
|
||||
assertEquals(0, list.size());
|
||||
|
||||
/////////////////////////////////////////
|
||||
// add permission for restricted table //
|
||||
/////////////////////////////////////////
|
||||
QContext.getQSession().withPermission("restricted.hasAccess");
|
||||
list = provider.search(new SearchPossibleValueSourceInput()
|
||||
.withPossibleValueSourceName(QQQTable.TABLE_NAME)
|
||||
.withSearchTerm("restricted"));
|
||||
assertEquals(1, list.size());
|
||||
|
||||
list = provider.search(new SearchPossibleValueSourceInput()
|
||||
.withPossibleValueSourceName(QQQTable.TABLE_NAME)
|
||||
.withLabelList(List.of("Person", "Shape", "Restricted")));
|
||||
assertEquals(3, list.size());
|
||||
assertThat(list).anyMatch(p -> p.getId().equals(personTableId));
|
||||
assertThat(list).anyMatch(p -> p.getId().equals(shapeTableId));
|
||||
assertThat(list).anyMatch(p -> p.getId().equals(restrictedTableId));
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -1,519 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2022. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.processes.implementations.bulk.editwithfile;
|
||||
|
||||
|
||||
import java.io.OutputStream;
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import com.kingsrook.qqq.backend.core.BaseTest;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.AbstractPreInsertCustomizer;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizerInterface;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
|
||||
import com.kingsrook.qqq.backend.core.actions.processes.RunProcessAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.StorageAction;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryAssert;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryLine;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryLineInterface;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.Status;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.storage.StorageInput;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.statusmessages.BadInputStatusMessage;
|
||||
import com.kingsrook.qqq.backend.core.model.statusmessages.QWarningMessage;
|
||||
import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryRecordStore;
|
||||
import com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert.BulkInsertFullProcessTest;
|
||||
import com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert.model.BulkLoadProfile;
|
||||
import com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert.model.BulkLoadProfileField;
|
||||
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.StreamedETLWithFrontendProcess;
|
||||
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.TestUtils;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
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.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Unit test for full bulk insert process
|
||||
*******************************************************************************/
|
||||
class BulkEditWithFileFullProcessTest extends BaseTest
|
||||
{
|
||||
private static final String defaultEmail = "noone@kingsrook.com";
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@BeforeEach
|
||||
@AfterEach
|
||||
void beforeAndAfterEach()
|
||||
{
|
||||
MemoryRecordStore.getInstance().reset();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static String getPersonCsvRow1()
|
||||
{
|
||||
return ("""
|
||||
"1","2021-10-26 14:39:37","2021-10-26 14:39:37","Jehn","Doe","1980-01-01","john@doe.com","Missouri",24
|
||||
""");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static String getPersonCsvRow2()
|
||||
{
|
||||
return ("""
|
||||
"2","2021-10-26 14:39:37","2021-10-26 14:39:37","Jyne","Doe","1981-01-01","john@doe.com","Illinois",
|
||||
""");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static String getPersonCsvHeaderUsingLabels()
|
||||
{
|
||||
return ("""
|
||||
"Id","Create Date","Modify Date","First Name","Last Name","Birth Date","Email","Home State",noOfShoes
|
||||
""");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
public void test() throws Exception
|
||||
{
|
||||
/////////////////////////////////////////////
|
||||
// use the bulk insert test to insert data //
|
||||
/////////////////////////////////////////////
|
||||
new BulkInsertFullProcessTest().test();
|
||||
assertThat(TestUtils.queryTable(TestUtils.TABLE_NAME_PERSON_MEMORY)).isNotEmpty();
|
||||
|
||||
/////////////////////////////////////////////////////////
|
||||
// start the process - expect to go to the upload step //
|
||||
/////////////////////////////////////////////////////////
|
||||
RunProcessInput runProcessInput = new RunProcessInput();
|
||||
runProcessInput.addValue("keyFields", "id");
|
||||
runProcessInput.addValue("isBulkEdit", "true");
|
||||
RunProcessOutput runProcessOutput = startProcess(runProcessInput);
|
||||
String processUUID = runProcessOutput.getProcessUUID();
|
||||
assertThat(runProcessOutput.getProcessState().getNextStepName()).isPresent().get().isEqualTo("upload");
|
||||
|
||||
//////////////////////////
|
||||
// continue post-upload //
|
||||
//////////////////////////
|
||||
runProcessOutput = continueProcessPostUpload(runProcessInput, processUUID, simulateFileUpload(2));
|
||||
assertEquals(List.of("Id", "Create Date", "Modify Date", "First Name", "Last Name", "Birth Date", "Email", "Home State", "noOfShoes"), runProcessOutput.getValue("headerValues"));
|
||||
assertEquals(List.of("A", "B", "C", "D", "E", "F", "G", "H", "I"), runProcessOutput.getValue("headerLetters"));
|
||||
|
||||
//////////////////////////////////////////////////////
|
||||
// assert about the suggested mapping that was done //
|
||||
//////////////////////////////////////////////////////
|
||||
Serializable bulkLoadProfile = runProcessOutput.getValue("bulkLoadProfile");
|
||||
assertThat(bulkLoadProfile).isInstanceOf(BulkLoadProfile.class);
|
||||
assertThat(((BulkLoadProfile) bulkLoadProfile).getFieldList()).hasSizeGreaterThan(5);
|
||||
assertEquals("id", ((BulkLoadProfile) bulkLoadProfile).getFieldList().get(0).getFieldName());
|
||||
assertEquals(0, ((BulkLoadProfile) bulkLoadProfile).getFieldList().get(0).getColumnIndex());
|
||||
assertEquals("firstName", ((BulkLoadProfile) bulkLoadProfile).getFieldList().get(1).getFieldName());
|
||||
assertEquals(3, ((BulkLoadProfile) bulkLoadProfile).getFieldList().get(1).getColumnIndex());
|
||||
assertEquals("lastName", ((BulkLoadProfile) bulkLoadProfile).getFieldList().get(2).getFieldName());
|
||||
assertEquals(4, ((BulkLoadProfile) bulkLoadProfile).getFieldList().get(2).getColumnIndex());
|
||||
assertEquals("birthDate", ((BulkLoadProfile) bulkLoadProfile).getFieldList().get(3).getFieldName());
|
||||
assertEquals(5, ((BulkLoadProfile) bulkLoadProfile).getFieldList().get(3).getColumnIndex());
|
||||
|
||||
assertThat(runProcessOutput.getProcessState().getNextStepName()).isPresent().get().isEqualTo("fileMapping");
|
||||
|
||||
////////////////////////////////
|
||||
// continue post file-mapping //
|
||||
////////////////////////////////
|
||||
runProcessOutput = continueProcessPostFileMapping(runProcessInput);
|
||||
Serializable valueMappingField = runProcessOutput.getValue("valueMappingField");
|
||||
assertThat(valueMappingField).isInstanceOf(QFrontendFieldMetaData.class);
|
||||
assertEquals("homeStateId", ((QFrontendFieldMetaData) valueMappingField).getName());
|
||||
assertEquals(List.of("Missouri", "Illinois"), runProcessOutput.getValue("fileValues"));
|
||||
assertEquals(List.of("homeStateId"), runProcessOutput.getValue("fieldNamesToDoValueMapping"));
|
||||
assertEquals(Map.of(1, "IL"), runProcessOutput.getValue("mappedValueLabels"));
|
||||
assertEquals(0, runProcessOutput.getValue("valueMappingFieldIndex"));
|
||||
assertThat(runProcessOutput.getProcessState().getNextStepName()).isPresent().get().isEqualTo("valueMapping");
|
||||
|
||||
/////////////////////////////////
|
||||
// continue post value-mapping //
|
||||
/////////////////////////////////
|
||||
runProcessOutput = continueProcessPostValueMapping(runProcessInput);
|
||||
assertThat(runProcessOutput.getProcessState().getNextStepName()).isPresent().get().isEqualTo("review");
|
||||
|
||||
/////////////////////////////////
|
||||
// continue post review screen //
|
||||
/////////////////////////////////
|
||||
runProcessOutput = continueProcessPostReviewScreen(runProcessInput);
|
||||
assertThat(runProcessOutput.getRecords()).hasSize(2);
|
||||
assertThat(runProcessOutput.getProcessState().getNextStepName()).isPresent().get().isEqualTo("result");
|
||||
assertThat(runProcessOutput.getValues().get(StreamedETLWithFrontendProcess.FIELD_PROCESS_SUMMARY)).isNotNull().isInstanceOf(List.class);
|
||||
assertThat(runProcessOutput.getException()).isEmpty();
|
||||
|
||||
ProcessSummaryLineInterface okLine = ProcessSummaryAssert.assertThat(runProcessOutput)
|
||||
.hasLineWithMessageContaining("Person Memory records were edited")
|
||||
.hasStatus(Status.OK)
|
||||
.hasCount(2)
|
||||
.getLine();
|
||||
assertEquals(List.of(1, 2), ((ProcessSummaryLine) okLine).getPrimaryKeys());
|
||||
|
||||
////////////////////////////////////
|
||||
// query for the inserted records //
|
||||
////////////////////////////////////
|
||||
List<QRecord> records = TestUtils.queryTable(TestUtils.TABLE_NAME_PERSON_MEMORY);
|
||||
|
||||
assertEquals("Jehn", records.get(0).getValueString("firstName"));
|
||||
assertEquals("Jyne", records.get(1).getValueString("firstName"));
|
||||
|
||||
assertNotNull(records.get(0).getValue("id"));
|
||||
assertNotNull(records.get(1).getValue("id"));
|
||||
assertEquals(1, records.get(0).getValue("id"));
|
||||
assertEquals(2, records.get(1).getValue("id"));
|
||||
|
||||
assertEquals(2, records.get(0).getValueInteger("homeStateId"));
|
||||
assertEquals(1, records.get(1).getValueInteger("homeStateId"));
|
||||
|
||||
assertEquals(defaultEmail, records.get(0).getValueString("email"));
|
||||
assertEquals(defaultEmail, records.get(1).getValueString("email"));
|
||||
|
||||
assertEquals(24, records.get(0).getValueInteger("noOfShoes"));
|
||||
assertNull(records.get(1).getValue("noOfShoes"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testSummaryLinePrimaryKeys() throws Exception
|
||||
{
|
||||
/////////////////////////////////////////////
|
||||
// use the bulk insert test to insert data //
|
||||
/////////////////////////////////////////////
|
||||
new BulkInsertFullProcessTest().testSummaryLinePrimaryKeys();
|
||||
assertThat(TestUtils.queryTable(TestUtils.TABLE_NAME_PERSON_MEMORY)).isNotEmpty();
|
||||
|
||||
QContext.getQInstance().getTable(TestUtils.TABLE_NAME_PERSON_MEMORY)
|
||||
.withCustomizer(TableCustomizers.PRE_UPDATE_RECORD, new QCodeReference(PersonWarnOrErrorCustomizer.class));
|
||||
|
||||
/////////////////////////////////////////////////////////
|
||||
// start the process - expect to go to the upload step //
|
||||
/////////////////////////////////////////////////////////
|
||||
RunProcessInput runProcessInput = new RunProcessInput();
|
||||
RunProcessOutput runProcessOutput = startProcess(runProcessInput);
|
||||
String processUUID = runProcessOutput.getProcessUUID();
|
||||
|
||||
continueProcessPostUpload(runProcessInput, processUUID, simulateFileUploadForWarningCase());
|
||||
continueProcessPostFileMapping(runProcessInput);
|
||||
continueProcessPostValueMapping(runProcessInput);
|
||||
runProcessOutput = continueProcessPostReviewScreen(runProcessInput);
|
||||
|
||||
ProcessSummaryLineInterface okLine = ProcessSummaryAssert.assertThat(runProcessOutput)
|
||||
.hasLineWithMessageContaining("Person Memory records were edited")
|
||||
.hasStatus(Status.OK)
|
||||
.hasCount(4)
|
||||
.getLine();
|
||||
assertEquals(List.of(1, 2, 3, 4), ((ProcessSummaryLine) okLine).getPrimaryKeys());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testSummaryLineErrors() throws Exception
|
||||
{
|
||||
/////////////////////////////////////////////
|
||||
// use the bulk insert test to insert data //
|
||||
/////////////////////////////////////////////
|
||||
new BulkInsertFullProcessTest().testSummaryLineErrors();
|
||||
assertThat(TestUtils.queryTable(TestUtils.TABLE_NAME_PERSON_MEMORY)).isNotEmpty();
|
||||
|
||||
QContext.getQInstance().getTable(TestUtils.TABLE_NAME_PERSON_MEMORY)
|
||||
.withCustomizer(TableCustomizers.PRE_UPDATE_RECORD, new QCodeReference(PersonWarnOrErrorCustomizer.class));
|
||||
|
||||
/////////////////////////////////////////////////////////
|
||||
// start the process - expect to go to the upload step //
|
||||
/////////////////////////////////////////////////////////
|
||||
RunProcessInput runProcessInput = new RunProcessInput();
|
||||
RunProcessOutput runProcessOutput = startProcess(runProcessInput);
|
||||
String processUUID = runProcessOutput.getProcessUUID();
|
||||
|
||||
continueProcessPostUpload(runProcessInput, processUUID, simulateFileUploadForErrorCase());
|
||||
continueProcessPostFileMapping(runProcessInput);
|
||||
continueProcessPostValueMapping(runProcessInput);
|
||||
runProcessOutput = continueProcessPostReviewScreen(runProcessInput);
|
||||
|
||||
ProcessSummaryAssert.assertThat(runProcessOutput).hasLineWithMessageContaining("Person Memory record was edited.").hasStatus(Status.OK).hasCount(1);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testOneRow() throws Exception
|
||||
{
|
||||
/////////////////////////////////////////////
|
||||
// use the bulk insert test to insert data //
|
||||
/////////////////////////////////////////////
|
||||
new BulkInsertFullProcessTest().testSummaryLineErrors();
|
||||
assertThat(TestUtils.queryTable(TestUtils.TABLE_NAME_PERSON_MEMORY)).isNotEmpty();
|
||||
|
||||
RunProcessInput runProcessInput = new RunProcessInput();
|
||||
RunProcessOutput runProcessOutput = startProcess(runProcessInput);
|
||||
String processUUID = runProcessOutput.getProcessUUID();
|
||||
|
||||
continueProcessPostUpload(runProcessInput, processUUID, simulateFileUpload(1));
|
||||
continueProcessPostFileMapping(runProcessInput);
|
||||
continueProcessPostValueMapping(runProcessInput);
|
||||
runProcessOutput = continueProcessPostReviewScreen(runProcessInput);
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// all that just so we can make sure this message is right (because it was wrong when we first wrote it, lol) //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
ProcessSummaryAssert.assertThat(runProcessOutput).hasLineWithMessageContaining("Person Memory record was edited.").hasStatus(Status.OK).hasCount(1);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
private static RunProcessOutput continueProcessPostReviewScreen(RunProcessInput runProcessInput) throws QException
|
||||
{
|
||||
RunProcessOutput runProcessOutput;
|
||||
runProcessInput.setStartAfterStep("review");
|
||||
addProfileToRunProcessInput(runProcessInput);
|
||||
runProcessOutput = new RunProcessAction().execute(runProcessInput);
|
||||
return runProcessOutput;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
private static RunProcessOutput continueProcessPostValueMapping(RunProcessInput runProcessInput) throws QException
|
||||
{
|
||||
runProcessInput.setStartAfterStep("valueMapping");
|
||||
runProcessInput.addValue("mappedValuesJSON", JsonUtils.toJson(Map.of("Illinois", 1, "Missouri", 2)));
|
||||
addProfileToRunProcessInput(runProcessInput);
|
||||
RunProcessOutput runProcessOutput = new RunProcessAction().execute(runProcessInput);
|
||||
return (runProcessOutput);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
private static RunProcessOutput continueProcessPostFileMapping(RunProcessInput runProcessInput) throws QException
|
||||
{
|
||||
RunProcessOutput runProcessOutput;
|
||||
runProcessInput.setStartAfterStep("fileMapping");
|
||||
addProfileToRunProcessInput(runProcessInput);
|
||||
runProcessOutput = new RunProcessAction().execute(runProcessInput);
|
||||
return runProcessOutput;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
private static RunProcessOutput continueProcessPostUpload(RunProcessInput runProcessInput, String processUUID, StorageInput storageInput) throws QException
|
||||
{
|
||||
runProcessInput.setProcessUUID(processUUID);
|
||||
runProcessInput.setStartAfterStep("upload");
|
||||
runProcessInput.addValue("theFile", new ArrayList<>(List.of(storageInput)));
|
||||
RunProcessOutput runProcessOutput = new RunProcessAction().execute(runProcessInput);
|
||||
return (runProcessOutput);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
private static StorageInput simulateFileUpload(int noOfRows) throws Exception
|
||||
{
|
||||
String storageReference = UUID.randomUUID() + ".csv";
|
||||
StorageInput storageInput = new StorageInput(TestUtils.TABLE_NAME_MEMORY_STORAGE).withReference(storageReference);
|
||||
try(OutputStream outputStream = new StorageAction().createOutputStream(storageInput))
|
||||
{
|
||||
outputStream.write((getPersonCsvHeaderUsingLabels() + getPersonCsvRow1() + (noOfRows == 2 ? getPersonCsvRow2() : "")).getBytes());
|
||||
}
|
||||
return storageInput;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
private static StorageInput simulateFileUploadForWarningCase() throws Exception
|
||||
{
|
||||
String storageReference = UUID.randomUUID() + ".csv";
|
||||
StorageInput storageInput = new StorageInput(TestUtils.TABLE_NAME_MEMORY_STORAGE).withReference(storageReference);
|
||||
try(OutputStream outputStream = new StorageAction().createOutputStream(storageInput))
|
||||
{
|
||||
outputStream.write((getPersonCsvHeaderUsingLabels() + """
|
||||
"1","2021-10-26 14:39:37","2021-10-26 14:39:37","John","Doe","1980-01-01","john@doe.com","Missouri",42
|
||||
"2","2021-10-26 14:39:37","2021-10-26 14:39:37","Tornado warning","Doe","1980-01-01","john@doe.com","Missouri",42
|
||||
"3","2021-10-26 14:39:37","2021-10-26 14:39:37","Tornado warning","Doey","1980-01-01","john@doe.com","Missouri",42
|
||||
"4","2021-10-26 14:39:37","2021-10-26 14:39:37","Hurricane warning","Doe","1980-01-01","john@doe.com","Missouri",42
|
||||
""").getBytes());
|
||||
}
|
||||
return storageInput;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
private static StorageInput simulateFileUploadForErrorCase() throws Exception
|
||||
{
|
||||
String storageReference = UUID.randomUUID() + ".csv";
|
||||
StorageInput storageInput = new StorageInput(TestUtils.TABLE_NAME_MEMORY_STORAGE).withReference(storageReference);
|
||||
try(OutputStream outputStream = new StorageAction().createOutputStream(storageInput))
|
||||
{
|
||||
outputStream.write((getPersonCsvHeaderUsingLabels() + """
|
||||
"1","2021-10-26 14:39:37","2021-10-26 14:39:37","John","Doe","1980-01-01","john@doe.com","Missouri",42
|
||||
"2","2021-10-26 14:39:37","2021-10-26 14:39:37","not-pre-Error plane","Doe","1980-01-01","john@doe.com","Missouri",42
|
||||
"3","2021-10-26 14:39:37","2021-10-26 14:39:37","Error purifier","Doe","1980-01-01","john@doe.com","Missouri",42
|
||||
""").getBytes());
|
||||
}
|
||||
return storageInput;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
private static RunProcessOutput startProcess(RunProcessInput runProcessInput) throws QException
|
||||
{
|
||||
runProcessInput.setProcessName(TestUtils.TABLE_NAME_PERSON_MEMORY + ".bulkEditWithFile");
|
||||
runProcessInput.addValue("tableName", TestUtils.TABLE_NAME_PERSON_MEMORY);
|
||||
runProcessInput.addValue("isBulkEdit", "true");
|
||||
RunProcessOutput runProcessOutput = new RunProcessAction().execute(runProcessInput);
|
||||
return runProcessOutput;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
private static void addProfileToRunProcessInput(RunProcessInput input)
|
||||
{
|
||||
input.addValue("version", "v1");
|
||||
input.addValue("layout", "FLAT");
|
||||
input.addValue("isBulkEdit", "true");
|
||||
input.addValue("keyFields", "id");
|
||||
input.addValue("hasHeaderRow", "true");
|
||||
input.addValue("fieldListJSON", JsonUtils.toJson(List.of(
|
||||
new BulkLoadProfileField().withFieldName("id").withColumnIndex(0),
|
||||
new BulkLoadProfileField().withFieldName("firstName").withColumnIndex(3),
|
||||
new BulkLoadProfileField().withFieldName("lastName").withColumnIndex(4),
|
||||
new BulkLoadProfileField().withFieldName("email").withDefaultValue(defaultEmail),
|
||||
new BulkLoadProfileField().withFieldName("homeStateId").withColumnIndex(7).withDoValueMapping(true).withValueMappings(Map.of("Illinois", 1)),
|
||||
new BulkLoadProfileField().withFieldName("noOfShoes").withColumnIndex(8)
|
||||
)));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
public static class PersonWarnOrErrorCustomizer implements TableCustomizerInterface
|
||||
{
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public AbstractPreInsertCustomizer.WhenToRun whenToRunPreInsert(InsertInput insertInput, boolean isPreview)
|
||||
{
|
||||
return AbstractPreInsertCustomizer.WhenToRun.BEFORE_ALL_VALIDATIONS;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public List<QRecord> preInsert(InsertInput insertInput, List<QRecord> records, boolean isPreview) throws QException
|
||||
{
|
||||
for(QRecord record : records)
|
||||
{
|
||||
if(record.getValueString("firstName").toLowerCase().contains("warn"))
|
||||
{
|
||||
record.addWarning(new QWarningMessage(record.getValueString("firstName")));
|
||||
}
|
||||
else if(record.getValueString("firstName").toLowerCase().contains("error"))
|
||||
{
|
||||
if(isPreview && record.getValueString("firstName").toLowerCase().contains("not-pre-error"))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
record.addError(new BadInputStatusMessage(record.getValueString("firstName")));
|
||||
}
|
||||
}
|
||||
return records;
|
||||
}
|
||||
}
|
||||
}
|
@ -67,7 +67,7 @@ import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
/*******************************************************************************
|
||||
** Unit test for full bulk insert process
|
||||
*******************************************************************************/
|
||||
public class BulkInsertFullProcessTest extends BaseTest
|
||||
class BulkInsertFullProcessTest extends BaseTest
|
||||
{
|
||||
private static final String defaultEmail = "noone@kingsrook.com";
|
||||
|
||||
@ -125,7 +125,7 @@ public class BulkInsertFullProcessTest extends BaseTest
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
public void test() throws Exception
|
||||
void test() throws Exception
|
||||
{
|
||||
assertThat(TestUtils.queryTable(TestUtils.TABLE_NAME_PERSON_MEMORY)).isEmpty();
|
||||
|
||||
@ -224,7 +224,7 @@ public class BulkInsertFullProcessTest extends BaseTest
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
public void testSummaryLinePrimaryKeys() throws Exception
|
||||
void testSummaryLinePrimaryKeys() throws Exception
|
||||
{
|
||||
assertThat(TestUtils.queryTable(TestUtils.TABLE_NAME_PERSON_MEMORY)).isEmpty();
|
||||
QContext.getQInstance().getTable(TestUtils.TABLE_NAME_PERSON_MEMORY)
|
||||
@ -267,7 +267,7 @@ public class BulkInsertFullProcessTest extends BaseTest
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
public void testSummaryLineErrors() throws Exception
|
||||
void testSummaryLineErrors() throws Exception
|
||||
{
|
||||
assertThat(TestUtils.queryTable(TestUtils.TABLE_NAME_PERSON_MEMORY)).isEmpty();
|
||||
QContext.getQInstance().getTable(TestUtils.TABLE_NAME_PERSON_MEMORY)
|
||||
@ -304,7 +304,7 @@ public class BulkInsertFullProcessTest extends BaseTest
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
public void testOneRow() throws Exception
|
||||
void testOneRow() throws Exception
|
||||
{
|
||||
///////////////////////////////////////
|
||||
// make sure table is empty to start //
|
||||
|
@ -52,7 +52,7 @@ class BulkLoadMappingSuggesterTest extends BaseTest
|
||||
BulkLoadTableStructure tableStructure = BulkLoadTableStructureBuilder.buildTableStructure(TestUtils.TABLE_NAME_PERSON_MEMORY);
|
||||
List<String> headerRow = List.of("Id", "First Name", "lastname", "email", "homestate");
|
||||
|
||||
BulkLoadProfile bulkLoadProfile = new BulkLoadMappingSuggester().suggestBulkLoadMappingProfile(tableStructure, headerRow, false);
|
||||
BulkLoadProfile bulkLoadProfile = new BulkLoadMappingSuggester().suggestBulkLoadMappingProfile(tableStructure, headerRow);
|
||||
assertEquals("v1", bulkLoadProfile.getVersion());
|
||||
assertEquals("FLAT", bulkLoadProfile.getLayout());
|
||||
assertNull(getFieldByName(bulkLoadProfile, "id"));
|
||||
@ -73,7 +73,7 @@ class BulkLoadMappingSuggesterTest extends BaseTest
|
||||
BulkLoadTableStructure tableStructure = BulkLoadTableStructureBuilder.buildTableStructure(TestUtils.TABLE_NAME_ORDER);
|
||||
List<String> headerRow = List.of("orderNo", "shipto name", "sku", "quantity");
|
||||
|
||||
BulkLoadProfile bulkLoadProfile = new BulkLoadMappingSuggester().suggestBulkLoadMappingProfile(tableStructure, headerRow, false);
|
||||
BulkLoadProfile bulkLoadProfile = new BulkLoadMappingSuggester().suggestBulkLoadMappingProfile(tableStructure, headerRow);
|
||||
assertEquals("v1", bulkLoadProfile.getVersion());
|
||||
assertEquals("TALL", bulkLoadProfile.getLayout());
|
||||
assertEquals(0, getFieldByName(bulkLoadProfile, "orderNo").getColumnIndex());
|
||||
@ -93,7 +93,7 @@ class BulkLoadMappingSuggesterTest extends BaseTest
|
||||
BulkLoadTableStructure tableStructure = BulkLoadTableStructureBuilder.buildTableStructure(TestUtils.TABLE_NAME_ORDER);
|
||||
List<String> headerRow = List.of("Order No", "Ship To Name", "Order Line: SKU", "Order Line: Quantity");
|
||||
|
||||
BulkLoadProfile bulkLoadProfile = new BulkLoadMappingSuggester().suggestBulkLoadMappingProfile(tableStructure, headerRow, false);
|
||||
BulkLoadProfile bulkLoadProfile = new BulkLoadMappingSuggester().suggestBulkLoadMappingProfile(tableStructure, headerRow);
|
||||
assertEquals("v1", bulkLoadProfile.getVersion());
|
||||
assertEquals("TALL", bulkLoadProfile.getLayout());
|
||||
assertEquals(0, getFieldByName(bulkLoadProfile, "orderNo").getColumnIndex());
|
||||
@ -120,7 +120,7 @@ class BulkLoadMappingSuggesterTest extends BaseTest
|
||||
BulkLoadTableStructure tableStructure = BulkLoadTableStructureBuilder.buildTableStructure(TestUtils.TABLE_NAME_ORDER);
|
||||
List<String> headerRow = List.of("orderNo", "ship to name", "address 1", "address 2");
|
||||
|
||||
BulkLoadProfile bulkLoadProfile = new BulkLoadMappingSuggester().suggestBulkLoadMappingProfile(tableStructure, headerRow, false);
|
||||
BulkLoadProfile bulkLoadProfile = new BulkLoadMappingSuggester().suggestBulkLoadMappingProfile(tableStructure, headerRow);
|
||||
assertEquals(0, getFieldByName(bulkLoadProfile, "orderNo").getColumnIndex());
|
||||
assertEquals(1, getFieldByName(bulkLoadProfile, "shipToName").getColumnIndex());
|
||||
assertEquals(2, getFieldByName(bulkLoadProfile, "address1").getColumnIndex());
|
||||
@ -136,7 +136,7 @@ class BulkLoadMappingSuggesterTest extends BaseTest
|
||||
BulkLoadTableStructure tableStructure = BulkLoadTableStructureBuilder.buildTableStructure(TestUtils.TABLE_NAME_ORDER);
|
||||
List<String> headerRow = List.of("orderNo", "ship to name", "address 1", "address 2");
|
||||
|
||||
BulkLoadProfile bulkLoadProfile = new BulkLoadMappingSuggester().suggestBulkLoadMappingProfile(tableStructure, headerRow, false);
|
||||
BulkLoadProfile bulkLoadProfile = new BulkLoadMappingSuggester().suggestBulkLoadMappingProfile(tableStructure, headerRow);
|
||||
assertEquals(0, getFieldByName(bulkLoadProfile, "orderNo").getColumnIndex());
|
||||
assertEquals(1, getFieldByName(bulkLoadProfile, "shipToName").getColumnIndex());
|
||||
assertEquals(2, getFieldByName(bulkLoadProfile, "address").getColumnIndex());
|
||||
@ -152,7 +152,7 @@ class BulkLoadMappingSuggesterTest extends BaseTest
|
||||
BulkLoadTableStructure tableStructure = BulkLoadTableStructureBuilder.buildTableStructure(TestUtils.TABLE_NAME_ORDER);
|
||||
List<String> headerRow = List.of("orderNo", "ship to name", "address", "address 2");
|
||||
|
||||
BulkLoadProfile bulkLoadProfile = new BulkLoadMappingSuggester().suggestBulkLoadMappingProfile(tableStructure, headerRow, false);
|
||||
BulkLoadProfile bulkLoadProfile = new BulkLoadMappingSuggester().suggestBulkLoadMappingProfile(tableStructure, headerRow);
|
||||
assertEquals(0, getFieldByName(bulkLoadProfile, "orderNo").getColumnIndex());
|
||||
assertEquals(1, getFieldByName(bulkLoadProfile, "shipToName").getColumnIndex());
|
||||
assertEquals(2, getFieldByName(bulkLoadProfile, "address1").getColumnIndex());
|
||||
@ -177,7 +177,7 @@ class BulkLoadMappingSuggesterTest extends BaseTest
|
||||
BulkLoadTableStructure tableStructure = BulkLoadTableStructureBuilder.buildTableStructure(TestUtils.TABLE_NAME_ORDER);
|
||||
List<String> headerRow = List.of("orderNo", "ship to name", "sku", "quantity1", "sku 2", "quantity 2");
|
||||
|
||||
BulkLoadProfile bulkLoadProfile = new BulkLoadMappingSuggester().suggestBulkLoadMappingProfile(tableStructure, headerRow, false);
|
||||
BulkLoadProfile bulkLoadProfile = new BulkLoadMappingSuggester().suggestBulkLoadMappingProfile(tableStructure, headerRow);
|
||||
assertEquals("v1", bulkLoadProfile.getVersion());
|
||||
assertEquals("WIDE", bulkLoadProfile.getLayout());
|
||||
assertEquals(0, getFieldByName(bulkLoadProfile, "orderNo").getColumnIndex());
|
||||
|
@ -0,0 +1,193 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.scheduler.processes;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
import com.kingsrook.qqq.backend.core.BaseTest;
|
||||
import com.kingsrook.qqq.backend.core.actions.processes.QProcessCallbackFactory;
|
||||
import com.kingsrook.qqq.backend.core.actions.processes.RunProcessAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
|
||||
import com.kingsrook.qqq.backend.core.model.common.TimeZonePossibleValueSourceMetaDataProvider;
|
||||
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.scheduledjobs.ScheduledJob;
|
||||
import com.kingsrook.qqq.backend.core.model.scheduledjobs.ScheduledJobsMetaDataProvider;
|
||||
import com.kingsrook.qqq.backend.core.model.session.QSystemUserSession;
|
||||
import com.kingsrook.qqq.backend.core.scheduler.QScheduleManager;
|
||||
import com.kingsrook.qqq.backend.core.utils.TestUtils;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Unit test for AbstractRecordSyncToScheduledJobProcess
|
||||
*******************************************************************************/
|
||||
class AbstractRecordSyncToScheduledJobProcessTest extends BaseTest
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@BeforeEach
|
||||
void beforeEach() throws QException
|
||||
{
|
||||
QInstance qInstance = QContext.getQInstance();
|
||||
new ScheduledJobsMetaDataProvider().defineAll(qInstance, TestUtils.MEMORY_BACKEND_NAME, null);
|
||||
qInstance.addProcess(new SyncPersonToScheduledJobProcess().produce(qInstance));
|
||||
qInstance.addPossibleValueSource(new TimeZonePossibleValueSourceMetaDataProvider().produce());
|
||||
QScheduleManager.initInstance(qInstance, QSystemUserSession::new);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void test() throws QException
|
||||
{
|
||||
QRecord person = new InsertAction().execute(new InsertInput(TestUtils.TABLE_NAME_PERSON_MEMORY)
|
||||
.withRecord(new QRecord().withValue("id", 1701).withValue("firstName", "Darin")))
|
||||
.getRecords().get(0);
|
||||
|
||||
RunProcessInput input = new RunProcessInput();
|
||||
input.setProcessName(SyncPersonToScheduledJobProcess.class.getSimpleName());
|
||||
input.setCallback(QProcessCallbackFactory.forRecord(person));
|
||||
input.setFrontendStepBehavior(RunProcessInput.FrontendStepBehavior.SKIP);
|
||||
new RunProcessAction().execute(input);
|
||||
|
||||
List<ScheduledJob> scheduledJobs = new QueryAction().execute(new QueryInput(ScheduledJob.TABLE_NAME).withIncludeAssociations(true)).getRecordEntities(ScheduledJob.class);
|
||||
assertEquals(1, scheduledJobs.size());
|
||||
ScheduledJob scheduledJob = scheduledJobs.get(0);
|
||||
assertEquals(TestUtils.TABLE_NAME_PERSON_MEMORY, scheduledJob.getForeignKeyType());
|
||||
assertEquals(person.getValueString("id"), scheduledJob.getForeignKeyValue());
|
||||
assertEquals(60, scheduledJob.getRepeatSeconds());
|
||||
assertTrue(scheduledJob.getIsActive());
|
||||
assertEquals(4, scheduledJob.getJobParameters().size());
|
||||
assertEquals(TestUtils.PROCESS_NAME_GREET_PEOPLE, scheduledJob.getJobParameters().stream().filter(jp -> jp.getKey().equals("processName")).findFirst().get().getValue());
|
||||
assertEquals("true", scheduledJob.getJobParameters().stream().filter(jp -> jp.getKey().equals("isScheduledJob")).findFirst().get().getValue());
|
||||
assertEquals(person.getValueString("id"), scheduledJob.getJobParameters().stream().filter(jp -> jp.getKey().equals(TestUtils.TABLE_NAME_PERSON_MEMORY + "Id")).findFirst().get().getValue());
|
||||
assertEquals(person.getValueString("id"), scheduledJob.getJobParameters().stream().filter(jp -> jp.getKey().equals("recordId")).findFirst().get().getValue());
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
// re-run - it should update the repeat seconds (per custom logic in test class below) //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
new RunProcessAction().execute(input);
|
||||
scheduledJobs = new QueryAction().execute(new QueryInput(ScheduledJob.TABLE_NAME).withIncludeAssociations(true)).getRecordEntities(ScheduledJob.class);
|
||||
assertEquals(1, scheduledJobs.size());
|
||||
scheduledJob = scheduledJobs.get(0);
|
||||
assertEquals(61, scheduledJob.getRepeatSeconds());
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
public static class SyncPersonToScheduledJobProcess extends AbstractRecordSyncToScheduledJobProcess
|
||||
{
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
@Override
|
||||
protected ScheduledJob customizeScheduledJob(ScheduledJob scheduledJob, QRecord sourceRecord) throws QException
|
||||
{
|
||||
if(scheduledJob.getRepeatSeconds() != null)
|
||||
{
|
||||
///////////////////////////////////
|
||||
// increment by one on an update //
|
||||
///////////////////////////////////
|
||||
return scheduledJob.withRepeatSeconds(scheduledJob.getRepeatSeconds() + 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
return scheduledJob.withRepeatSeconds(60);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
@Override
|
||||
protected String getScheduledJobForeignKeyType()
|
||||
{
|
||||
return TestUtils.TABLE_NAME_PERSON_MEMORY;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
@Override
|
||||
protected String getRecordForeignKeyFieldName()
|
||||
{
|
||||
return "id";
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
@Override
|
||||
protected String getRecordForeignKeyPossibleValueSourceName()
|
||||
{
|
||||
return TestUtils.TABLE_NAME_PERSON_MEMORY;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
@Override
|
||||
protected String getSourceTableName()
|
||||
{
|
||||
return TestUtils.TABLE_NAME_PERSON_MEMORY;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
@Override
|
||||
protected String getProcessNameScheduledJobParameter()
|
||||
{
|
||||
return TestUtils.PROCESS_NAME_GREET_PEOPLE;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,74 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.scheduler.processes;
|
||||
|
||||
|
||||
import com.kingsrook.qqq.backend.core.BaseTest;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.DeleteAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.scheduledjobs.ScheduledJob;
|
||||
import com.kingsrook.qqq.backend.core.utils.TestUtils;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Unit test for BaseSyncToScheduledJobTableCustomizer
|
||||
*******************************************************************************/
|
||||
class BaseSyncToScheduledJobTableCustomizerTest extends BaseTest
|
||||
{
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@BeforeEach
|
||||
void beforeEach() throws QException
|
||||
{
|
||||
new AbstractRecordSyncToScheduledJobProcessTest().beforeEach();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void test() throws QException
|
||||
{
|
||||
QTableMetaData table = QContext.getQInstance().getTable(TestUtils.TABLE_NAME_PERSON_MEMORY);
|
||||
BaseSyncToScheduledJobTableCustomizer.setTableCustomizers(table, new AbstractRecordSyncToScheduledJobProcessTest.SyncPersonToScheduledJobProcess());
|
||||
|
||||
QRecord person = new InsertAction().execute(new InsertInput(TestUtils.TABLE_NAME_PERSON_MEMORY).withRecord(new QRecord().withValue("firstName", "Darin"))).getRecords().get(0);
|
||||
assertEquals(1, QueryAction.execute(ScheduledJob.TABLE_NAME, null).size());
|
||||
|
||||
new DeleteAction().execute(new DeleteInput(TestUtils.TABLE_NAME_PERSON_MEMORY).withPrimaryKey(person.getValue("id")));
|
||||
assertEquals(0, QueryAction.execute(ScheduledJob.TABLE_NAME, null).size());
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.utils.collections;
|
||||
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import com.kingsrook.qqq.backend.core.BaseTest;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Unit test for TypeTolerantKeyMap
|
||||
*******************************************************************************/
|
||||
class TypeTolerantKeyMapTest extends BaseTest
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void test()
|
||||
{
|
||||
TypeTolerantKeyMap<QRecord> map = new TypeTolerantKeyMap<>(QFieldType.INTEGER);
|
||||
map.put(1, new QRecord().withValue("id", 1));
|
||||
map.put("2", new QRecord().withValue("id", 2));
|
||||
map.put(3.0, new QRecord().withValue("id", 3));
|
||||
map.put(new BigDecimal("4.00"), new QRecord().withValue("id", 4));
|
||||
|
||||
for(int i=1; i<=4; i++)
|
||||
{
|
||||
assertTrue(map.containsKey(i));
|
||||
assertEquals(i, map.get(i).getValueInteger("id"));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -111,7 +111,7 @@ public abstract class AbstractRDBMSAction
|
||||
**
|
||||
** That is, table.backendDetails.tableName if set -- else, table.name
|
||||
*******************************************************************************/
|
||||
protected String getTableName(QTableMetaData table)
|
||||
public static String getTableName(QTableMetaData table)
|
||||
{
|
||||
if(table.getBackendDetails() instanceof RDBMSTableBackendDetails details)
|
||||
{
|
||||
@ -130,7 +130,7 @@ public abstract class AbstractRDBMSAction
|
||||
**
|
||||
** That is, field.backendName if set -- else, field.name
|
||||
*******************************************************************************/
|
||||
protected String getColumnName(QFieldMetaData field)
|
||||
public static String getColumnName(QFieldMetaData field)
|
||||
{
|
||||
if(field.getBackendName() != null)
|
||||
{
|
||||
|
@ -0,0 +1,331 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.module.rdbms.model.metadata;
|
||||
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.DatabaseMetaData;
|
||||
import java.sql.ResultSet;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import com.kingsrook.qqq.backend.core.instances.QInstanceEnricher;
|
||||
import com.kingsrook.qqq.backend.core.instances.assessment.QInstanceAssessor;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
import com.kingsrook.qqq.backend.module.rdbms.actions.AbstractRDBMSAction;
|
||||
import com.kingsrook.qqq.backend.module.rdbms.jdbc.ConnectionManager;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class RDBMSBackendAssessor
|
||||
{
|
||||
private QInstanceAssessor assessor;
|
||||
private RDBMSBackendMetaData backendMetaData;
|
||||
private List<QTableMetaData> tables;
|
||||
|
||||
private Map<String, QFieldType> typeMap = new HashMap<>();
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public RDBMSBackendAssessor(QInstanceAssessor assessor, RDBMSBackendMetaData backendMetaData, List<QTableMetaData> tables)
|
||||
{
|
||||
this.assessor = assessor;
|
||||
this.backendMetaData = backendMetaData;
|
||||
this.tables = tables;
|
||||
|
||||
////////////////////////////////////////////////
|
||||
// these are types as returned by mysql //
|
||||
// let null in here mean unsupported QQQ type //
|
||||
////////////////////////////////////////////////
|
||||
typeMap.put("TEXT", QFieldType.TEXT);
|
||||
typeMap.put("BINARY", QFieldType.BLOB);
|
||||
typeMap.put("SET", null);
|
||||
typeMap.put("VARBINARY", QFieldType.BLOB);
|
||||
typeMap.put("MEDIUMBLOB", QFieldType.BLOB);
|
||||
typeMap.put("NUMERIC", QFieldType.INTEGER);
|
||||
typeMap.put("BIGINT UNSIGNED", QFieldType.INTEGER);
|
||||
typeMap.put("MEDIUMINT UNSIGNED", QFieldType.INTEGER);
|
||||
typeMap.put("SMALLINT UNSIGNED", QFieldType.INTEGER);
|
||||
typeMap.put("TINYINT UNSIGNED", QFieldType.INTEGER);
|
||||
typeMap.put("BIT", null);
|
||||
typeMap.put("FLOAT", null);
|
||||
typeMap.put("REAL", null);
|
||||
typeMap.put("VARCHAR", QFieldType.STRING);
|
||||
typeMap.put("BOOL", QFieldType.BOOLEAN);
|
||||
typeMap.put("YEAR", null);
|
||||
typeMap.put("TIME", QFieldType.TIME);
|
||||
typeMap.put("TIMESTAMP", QFieldType.DATE_TIME);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void assess()
|
||||
{
|
||||
try(Connection connection = new ConnectionManager().getConnection(backendMetaData))
|
||||
{
|
||||
////////////////////////////////////////////////////////////////////
|
||||
// read data type ids (integers) to names, for field-type mapping //
|
||||
////////////////////////////////////////////////////////////////////
|
||||
DatabaseMetaData databaseMetaData;
|
||||
Map<Integer, String> dataTypeMap = new HashMap<>();
|
||||
try
|
||||
{
|
||||
databaseMetaData = connection.getMetaData();
|
||||
ResultSet typeInfoResultSet = databaseMetaData.getTypeInfo();
|
||||
while(typeInfoResultSet.next())
|
||||
{
|
||||
String name = typeInfoResultSet.getString("TYPE_NAME");
|
||||
Integer id = typeInfoResultSet.getInt("DATA_TYPE");
|
||||
dataTypeMap.put(id, name);
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
assessor.addError("Error loading metaData from RDBMS for backendName: " + backendMetaData.getName() + " - assessment cannot be completed.", e);
|
||||
return;
|
||||
}
|
||||
|
||||
///////////////////////////////////////
|
||||
// process each table in the backend //
|
||||
///////////////////////////////////////
|
||||
for(QTableMetaData table : tables)
|
||||
{
|
||||
String tableName = AbstractRDBMSAction.getTableName(table);
|
||||
|
||||
try
|
||||
{
|
||||
///////////////////////////////
|
||||
// check if the table exists //
|
||||
///////////////////////////////
|
||||
String databaseName = backendMetaData.getDatabaseName(); // these work for mysql - unclear about other vendors.
|
||||
String schemaName = null;
|
||||
try(ResultSet tableResultSet = databaseMetaData.getTables(databaseName, schemaName, tableName, null))
|
||||
{
|
||||
if(!tableResultSet.next())
|
||||
{
|
||||
assessor.addError("Table: " + table.getName() + " was not found in backend: " + backendMetaData.getName());
|
||||
assessor.addSuggestion(suggestCreateTable(table));
|
||||
continue;
|
||||
}
|
||||
|
||||
//////////////////////////////
|
||||
// read the table's columns //
|
||||
//////////////////////////////
|
||||
Map<String, QFieldMetaData> columnMap = new HashMap<>();
|
||||
String primaryKeyColumnName = null;
|
||||
try(ResultSet columnsResultSet = databaseMetaData.getColumns(databaseName, schemaName, tableName, null))
|
||||
{
|
||||
while(columnsResultSet.next())
|
||||
{
|
||||
String columnName = columnsResultSet.getString("COLUMN_NAME");
|
||||
String columnSize = columnsResultSet.getString("COLUMN_SIZE");
|
||||
Integer dataTypeId = columnsResultSet.getInt("DATA_TYPE");
|
||||
String isNullable = columnsResultSet.getString("IS_NULLABLE");
|
||||
String isAutoIncrement = columnsResultSet.getString("IS_AUTOINCREMENT");
|
||||
|
||||
String dataTypeName = dataTypeMap.get(dataTypeId);
|
||||
QFieldMetaData columnMetaData = new QFieldMetaData(columnName, typeMap.get(dataTypeName));
|
||||
columnMap.put(columnName, columnMetaData);
|
||||
|
||||
if("YES".equals(isAutoIncrement))
|
||||
{
|
||||
primaryKeyColumnName = columnName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/////////////////////////////////
|
||||
// diff the columns and fields //
|
||||
/////////////////////////////////
|
||||
for(QFieldMetaData column : columnMap.values())
|
||||
{
|
||||
boolean fieldExists = table.getFields().values().stream().anyMatch(f -> column.getName().equals(AbstractRDBMSAction.getColumnName(f)));
|
||||
if(!fieldExists)
|
||||
{
|
||||
assessor.addWarning("Table: " + table.getName() + " has a column which was not found in the metaData: " + column.getName());
|
||||
assessor.addSuggestion("// in QTableMetaData.withName(\"" + table.getName() + "\")\n"
|
||||
+ ".withField(new QFieldMetaData(\"" + column.getName() + "\", QFieldType." + column.getType() + ").withBackendName(\"" + column.getName() + "\")"); // todo - column_name to fieldName
|
||||
}
|
||||
}
|
||||
|
||||
for(QFieldMetaData field : table.getFields().values())
|
||||
{
|
||||
String columnName = AbstractRDBMSAction.getColumnName(field);
|
||||
boolean columnExists = columnMap.values().stream().anyMatch(c -> c.getName().equals(columnName));
|
||||
if(!columnExists)
|
||||
{
|
||||
assessor.addError("Table: " + table.getName() + " has a field which was not found in the database: " + field.getName());
|
||||
assessor.addSuggestion("/* For table [" + tableName + "] in backend [" + table.getBackendName() + " (database " + databaseName + ")]: */\n"
|
||||
+ "ALTER TABLE " + tableName + " ADD " + QInstanceEnricher.inferBackendName(columnName) + " " + getDatabaseTypeForField(table, field) + ";");
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////
|
||||
// read unique constraints from the database //
|
||||
///////////////////////////////////////////////
|
||||
Map<String, Set<String>> uniqueIndexMap = new HashMap<>();
|
||||
try(ResultSet indexInfoResultSet = databaseMetaData.getIndexInfo(databaseName, schemaName, tableName, true, true))
|
||||
{
|
||||
while(indexInfoResultSet.next())
|
||||
{
|
||||
String indexName = indexInfoResultSet.getString("INDEX_NAME");
|
||||
String columnName = indexInfoResultSet.getString("COLUMN_NAME");
|
||||
uniqueIndexMap.computeIfAbsent(indexName, k -> new HashSet<>());
|
||||
uniqueIndexMap.get(indexName).add(columnName);
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////////
|
||||
// diff the unique keys //
|
||||
//////////////////////////
|
||||
for(UniqueKey uniqueKey : CollectionUtils.nonNullList(table.getUniqueKeys()))
|
||||
{
|
||||
Set<String> fieldNames = uniqueKey.getFieldNames().stream().map(fieldName -> AbstractRDBMSAction.getColumnName(table.getField(fieldName))).collect(Collectors.toSet());
|
||||
if(!uniqueIndexMap.containsValue(fieldNames))
|
||||
{
|
||||
assessor.addWarning("Table: " + table.getName() + " specifies a uniqueKey which was not found in the database: " + uniqueKey.getFieldNames());
|
||||
assessor.addSuggestion("/* For table [" + tableName + "] in backend [" + table.getBackendName() + " (database " + databaseName + ")]: */\n"
|
||||
+ "ALTER TABLE " + tableName + " ADD UNIQUE (" + StringUtils.join(", ", fieldNames) + ");");
|
||||
}
|
||||
}
|
||||
|
||||
for(Set<String> uniqueIndex : uniqueIndexMap.values())
|
||||
{
|
||||
//////////////////////////
|
||||
// skip the primary key //
|
||||
//////////////////////////
|
||||
if(uniqueIndex.size() == 1 && uniqueIndex.contains(primaryKeyColumnName))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
boolean foundInTableMetaData = false;
|
||||
for(UniqueKey uniqueKey : CollectionUtils.nonNullList(table.getUniqueKeys()))
|
||||
{
|
||||
Set<String> fieldNames = uniqueKey.getFieldNames().stream().map(fieldName -> AbstractRDBMSAction.getColumnName(table.getField(fieldName))).collect(Collectors.toSet());
|
||||
if(uniqueIndex.equals(fieldNames))
|
||||
{
|
||||
foundInTableMetaData = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(!foundInTableMetaData)
|
||||
{
|
||||
assessor.addWarning("Table: " + table.getName() + " has a unique index which was not found in the metaData: " + uniqueIndex);
|
||||
assessor.addSuggestion("// in QTableMetaData.withName(\"" + table.getName() + "\")\n"
|
||||
+ ".withUniqueKey(new UniqueKey(\"" + StringUtils.join("\", \"", uniqueIndex) + "\"))");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
assessor.addError("Error assessing table: " + table.getName() + " in backend: " + backendMetaData.getName(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
assessor.addError("Error connecting to RDBMS for backendName: " + backendMetaData.getName(), e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private String suggestCreateTable(QTableMetaData table)
|
||||
{
|
||||
StringBuilder rs = new StringBuilder("/* For table [" + table.getName() + "] in backend [" + table.getBackendName() + " (database " + (backendMetaData.getDatabaseName()) + ")]: */\n");
|
||||
rs.append("CREATE TABLE ").append(AbstractRDBMSAction.getTableName(table)).append("\n");
|
||||
rs.append("(\n");
|
||||
|
||||
List<String> fields = new ArrayList<>();
|
||||
for(QFieldMetaData field : table.getFields().values())
|
||||
{
|
||||
fields.add(" " + AbstractRDBMSAction.getColumnName(field) + " " + getDatabaseTypeForField(table, field));
|
||||
}
|
||||
|
||||
rs.append(StringUtils.join(",\n", fields));
|
||||
|
||||
rs.append("\n);");
|
||||
return (rs.toString());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private String getDatabaseTypeForField(QTableMetaData table, QFieldMetaData field)
|
||||
{
|
||||
return switch(field.getType())
|
||||
{
|
||||
case STRING ->
|
||||
{
|
||||
int n = Objects.requireNonNullElse(field.getMaxLength(), 250);
|
||||
yield ("VARCHAR(" + n + ")");
|
||||
}
|
||||
case INTEGER ->
|
||||
{
|
||||
String suffix = table.getPrimaryKeyField().equals(field.getName()) ? " AUTO_INCREMENT PRIMARY KEY" : "";
|
||||
yield ("INTEGER" + suffix);
|
||||
}
|
||||
case LONG ->
|
||||
{
|
||||
String suffix = table.getPrimaryKeyField().equals(field.getName()) ? " AUTO_INCREMENT PRIMARY KEY" : "";
|
||||
yield ("BIGINT" + suffix);
|
||||
}
|
||||
case DECIMAL -> "DECIMAL(10,2)";
|
||||
case BOOLEAN -> "BOOLEAN";
|
||||
case DATE -> "DATE";
|
||||
case TIME -> "TIME";
|
||||
case DATE_TIME -> "TIMESTAMP";
|
||||
case TEXT -> "TEXT";
|
||||
case HTML -> "TEXT";
|
||||
case PASSWORD -> "VARCHAR(40)";
|
||||
case BLOB -> "BLOB";
|
||||
};
|
||||
}
|
||||
}
|
@ -22,12 +22,18 @@
|
||||
package com.kingsrook.qqq.backend.module.rdbms.model.metadata;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
|
||||
import com.kingsrook.qqq.backend.core.instances.QMetaDataVariableInterpreter;
|
||||
import com.kingsrook.qqq.backend.core.instances.assessment.Assessable;
|
||||
import com.kingsrook.qqq.backend.core.instances.assessment.QInstanceAssessor;
|
||||
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.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.module.rdbms.RDBMSBackendModule;
|
||||
import com.kingsrook.qqq.backend.module.rdbms.strategy.BaseRDBMSActionStrategy;
|
||||
import com.kingsrook.qqq.backend.module.rdbms.strategy.RDBMSActionStrategyInterface;
|
||||
@ -36,7 +42,7 @@ import com.kingsrook.qqq.backend.module.rdbms.strategy.RDBMSActionStrategyInterf
|
||||
/*******************************************************************************
|
||||
** Meta-data to provide details of an RDBMS backend (e.g., connection params)
|
||||
*******************************************************************************/
|
||||
public class RDBMSBackendMetaData extends QBackendMetaData
|
||||
public class RDBMSBackendMetaData extends QBackendMetaData implements Assessable
|
||||
{
|
||||
private String vendor;
|
||||
private String hostName;
|
||||
@ -580,4 +586,25 @@ public class RDBMSBackendMetaData extends QBackendMetaData
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public void assess(QInstanceAssessor qInstanceAssessor, QInstance qInstance)
|
||||
{
|
||||
List<QTableMetaData> tables = new ArrayList<>();
|
||||
for(QTableMetaData table : qInstance.getTables().values())
|
||||
{
|
||||
if(Objects.equals(getName(), table.getBackendName()))
|
||||
{
|
||||
tables.add(table);
|
||||
}
|
||||
}
|
||||
|
||||
if(!tables.isEmpty())
|
||||
{
|
||||
new RDBMSBackendAssessor(qInstanceAssessor, this, tables).assess();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -101,12 +101,6 @@ public class RDBMSTableMetaDataBuilder
|
||||
String schemaName = null;
|
||||
String tableNameForMetaDataQueries = tableName;
|
||||
|
||||
if(backendMetaData.getVendor().equals("h2"))
|
||||
{
|
||||
databaseName = databaseName.toUpperCase();
|
||||
tableNameForMetaDataQueries = tableName.toUpperCase();
|
||||
}
|
||||
|
||||
try(ResultSet tableResultSet = databaseMetaData.getTables(databaseName, schemaName, tableNameForMetaDataQueries, null))
|
||||
{
|
||||
if(!tableResultSet.next())
|
||||
|
@ -153,12 +153,22 @@ public class TestUtils
|
||||
*******************************************************************************/
|
||||
public static RDBMSBackendMetaData defineBackend()
|
||||
{
|
||||
return (new RDBMSBackendMetaData()
|
||||
RDBMSBackendMetaData rdbmsBackendMetaData = new RDBMSBackendMetaData()
|
||||
.withName(DEFAULT_BACKEND_NAME)
|
||||
.withVendor("h2")
|
||||
.withHostName("mem")
|
||||
.withDatabaseName("test_database")
|
||||
.withUsername("sa"));
|
||||
.withUsername("sa");
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
// by default h2 up-shifts all names, which isn't how we expected //
|
||||
// things to be, so, tell it not to do that. //
|
||||
////////////////////////////////////////////////////////////////////
|
||||
String jdbcUrl = ConnectionManager.getJdbcUrl(rdbmsBackendMetaData);
|
||||
jdbcUrl += ";DATABASE_TO_UPPER=FALSE";
|
||||
rdbmsBackendMetaData.setJdbcUrl(jdbcUrl);
|
||||
|
||||
return rdbmsBackendMetaData;
|
||||
}
|
||||
|
||||
|
||||
|
@ -438,8 +438,8 @@ class QueryManagerTest extends BaseTest
|
||||
""");
|
||||
List<Map<String, Object>> rows = QueryManager.executeStatementForRows(connection, "SELECT * FROM test_table");
|
||||
assertNotNull(rows);
|
||||
assertEquals(47, rows.get(0).get("INT_COL"));
|
||||
assertEquals("Q", rows.get(0).get("CHAR_COL"));
|
||||
assertEquals(47, rows.get(0).get("int_col"));
|
||||
assertEquals("Q", rows.get(0).get("char_col"));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,117 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.module.rdbms.model.metadata;
|
||||
|
||||
|
||||
import java.sql.Connection;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.instances.assessment.QInstanceAssessor;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey;
|
||||
import com.kingsrook.qqq.backend.module.rdbms.BaseTest;
|
||||
import com.kingsrook.qqq.backend.module.rdbms.TestUtils;
|
||||
import com.kingsrook.qqq.backend.module.rdbms.jdbc.ConnectionManager;
|
||||
import com.kingsrook.qqq.backend.module.rdbms.jdbc.QueryManager;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Unit test for RDBMSBackendAssessor
|
||||
*******************************************************************************/
|
||||
class RDBMSBackendAssessorTest extends BaseTest
|
||||
{
|
||||
private static final QLogger LOG = QLogger.getLogger(RDBMSBackendAssessorTest.class);
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testSuccess() throws Exception
|
||||
{
|
||||
TestUtils.primeTestDatabase("prime-test-database.sql");
|
||||
QInstanceAssessor assessor = new QInstanceAssessor(QContext.getQInstance());
|
||||
assessor.assess();
|
||||
System.out.println(assessor.getSummary());
|
||||
assertEquals(0, assessor.getErrors().size());
|
||||
assertEquals(0, assessor.getWarnings().size());
|
||||
assertEquals(0, assessor.getExitCode());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testTableIssues() throws Exception
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// start from primed database, but make a few alters to it and the meta-data //
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
TestUtils.primeTestDatabase("prime-test-database.sql");
|
||||
ConnectionManager connectionManager = new ConnectionManager();
|
||||
try(Connection connection = connectionManager.getConnection(TestUtils.defineBackend()))
|
||||
{
|
||||
QueryManager.executeUpdate(connection, "ALTER TABLE person ADD COLUMN suffix VARCHAR(20)");
|
||||
QueryManager.executeUpdate(connection, "ALTER TABLE person ADD UNIQUE u_name (first_name, last_name)");
|
||||
}
|
||||
|
||||
QContext.getQInstance().getTable(TestUtils.TABLE_NAME_PERSON)
|
||||
.withField(new QFieldMetaData("middleName", QFieldType.STRING))
|
||||
.withUniqueKey(new UniqueKey("firstName", "middleName", "lastName"));
|
||||
|
||||
///////////////////////////
|
||||
// un-prime the database //
|
||||
///////////////////////////
|
||||
QInstanceAssessor assessor = new QInstanceAssessor(QContext.getQInstance());
|
||||
assessor.assess();
|
||||
LOG.info(assessor.getSummary());
|
||||
assertNotEquals(0, assessor.getErrors().size());
|
||||
assertNotEquals(0, assessor.getExitCode());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testTotalFailure() throws Exception
|
||||
{
|
||||
///////////////////////////
|
||||
// un-prime the database //
|
||||
///////////////////////////
|
||||
TestUtils.primeTestDatabase("drop-test-database.sql");
|
||||
QInstanceAssessor assessor = new QInstanceAssessor(QContext.getQInstance());
|
||||
assessor.assess();
|
||||
System.out.println(assessor.getSummary());
|
||||
assertNotEquals(0, assessor.getErrors().size());
|
||||
assertNotEquals(0, assessor.getExitCode());
|
||||
}
|
||||
|
||||
}
|
@ -122,6 +122,7 @@ public class SharingMetaDataProvider
|
||||
qInstance.addTable(new QTableMetaData()
|
||||
.withName(User.TABLE_NAME)
|
||||
.withPrimaryKeyField("id")
|
||||
.withBackendDetails(new RDBMSTableBackendDetails().withTableName("user"))
|
||||
.withBackendName(TestUtils.DEFAULT_BACKEND_NAME)
|
||||
.withFieldsFromEntity(User.class)
|
||||
.withRecordSecurityLock(new RecordSecurityLock()
|
||||
@ -132,6 +133,7 @@ public class SharingMetaDataProvider
|
||||
qInstance.addTable(new QTableMetaData()
|
||||
.withName(Group.TABLE_NAME)
|
||||
.withPrimaryKeyField("id")
|
||||
.withBackendDetails(new RDBMSTableBackendDetails().withTableName("group"))
|
||||
.withBackendName(TestUtils.DEFAULT_BACKEND_NAME)
|
||||
.withFieldsFromEntity(Group.class));
|
||||
QInstanceEnricher.setInferredFieldBackendNames(qInstance.getTable(Group.TABLE_NAME));
|
||||
@ -139,6 +141,7 @@ public class SharingMetaDataProvider
|
||||
qInstance.addTable(new QTableMetaData()
|
||||
.withName(Client.TABLE_NAME)
|
||||
.withPrimaryKeyField("id")
|
||||
.withBackendDetails(new RDBMSTableBackendDetails().withTableName("client"))
|
||||
.withBackendName(TestUtils.DEFAULT_BACKEND_NAME)
|
||||
.withFieldsFromEntity(Client.class));
|
||||
QInstanceEnricher.setInferredFieldBackendNames(qInstance.getTable(Client.TABLE_NAME));
|
||||
|
@ -0,0 +1,32 @@
|
||||
--
|
||||
-- QQQ - Low-code Application Framework for Engineers.
|
||||
-- Copyright (C) 2021-2022. Kingsrook, LLC
|
||||
-- 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
-- contact@kingsrook.com
|
||||
-- https://github.com/Kingsrook/
|
||||
--
|
||||
-- This program is free software: you can redistribute it and/or modify
|
||||
-- it under the terms of the GNU Affero General Public License as
|
||||
-- published by the Free Software Foundation, either version 3 of the
|
||||
-- License, or (at your option) any later version.
|
||||
--
|
||||
-- This program is distributed in the hope that it will be useful,
|
||||
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
-- GNU Affero General Public License for more details.
|
||||
--
|
||||
-- You should have received a copy of the GNU Affero General Public License
|
||||
-- along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS person;
|
||||
DROP TABLE IF EXISTS personal_id_card;
|
||||
DROP TABLE IF EXISTS carrier;
|
||||
DROP TABLE IF EXISTS line_item_extrinsic;
|
||||
DROP TABLE IF EXISTS order_line;
|
||||
DROP TABLE IF EXISTS item;
|
||||
DROP TABLE IF EXISTS `order`;
|
||||
DROP TABLE IF EXISTS order_instructions;
|
||||
DROP TABLE IF EXISTS warehouse_store_int;
|
||||
DROP TABLE IF EXISTS store;
|
||||
DROP TABLE IF EXISTS warehouse;
|
50
qqq-dev-tools/pom.xml
Normal file
50
qqq-dev-tools/pom.xml
Normal file
@ -0,0 +1,50 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
|
||||
http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>com.kingsrook.qqq</groupId>
|
||||
<artifactId>qqq-dev-tools</artifactId>
|
||||
<version>1.0.0-SNAPSHOT</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>QQQ Dev Tools</name>
|
||||
<description>Tools for developers of QQQ (is that the framework or applications or qbits or what?)</description>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>17</maven.compiler.source>
|
||||
<maven.compiler.target>17</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<junit.version>5.10.0</junit.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
<version>${junit.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.11.0</version>
|
||||
<configuration>
|
||||
<source>17</source>
|
||||
<target>17</target>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>3.1.2</version>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
@ -0,0 +1,342 @@
|
||||
package com.kingsrook.qqq.devtools;
|
||||
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** todo picocli this project and class
|
||||
*******************************************************************************/
|
||||
public class CreateNewQBit
|
||||
{
|
||||
private String name;
|
||||
private String root;
|
||||
|
||||
private static ExecutorService executorService = null;
|
||||
|
||||
private static String SED = "/opt/homebrew/bin/gsed"; // needs to be a version that supports -i (in-place edit)
|
||||
private static String GIT = "/usr/bin/git";
|
||||
private static String CP = "/bin/cp";
|
||||
private static String MV = "/bin/mv";
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static void main(String[] args)
|
||||
{
|
||||
args = new String[] { "/Users/dkelkhoff/git/kingsrook/qbits", "webhooks" };
|
||||
|
||||
if(args.length < 2)
|
||||
{
|
||||
System.out.println("Usage: java CreateNewQBit root-dir qbit-name");
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
CreateNewQBit instance = new CreateNewQBit();
|
||||
instance.root = args[0];
|
||||
instance.name = args[1];
|
||||
System.exit(instance.run());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public int run()
|
||||
{
|
||||
try
|
||||
{
|
||||
String wordsName = makeWordsName(name);
|
||||
wordsName = stripQBitPrefix(wordsName);
|
||||
String dashName = makeDashName(wordsName);
|
||||
String packageName = makePackageName(wordsName);
|
||||
String className = makeClassName(wordsName);
|
||||
String varName = makeVarName(wordsName);
|
||||
|
||||
if(!new File(root).exists())
|
||||
{
|
||||
System.err.println("ERROR: Root directory [" + root + "] does not exist.");
|
||||
return (1);
|
||||
}
|
||||
|
||||
File template = new File(root + File.separator + "TEMPLATE");
|
||||
if(!template.exists())
|
||||
{
|
||||
System.err.println("ERROR: Template directory [TEMPLATE] does not exist under [" + root + "].");
|
||||
return (1);
|
||||
}
|
||||
|
||||
File dir = new File(root + File.separator + "qbit-" + dashName);
|
||||
if(dir.exists())
|
||||
{
|
||||
System.err.println("ERROR: Directory [" + dashName + "] already exists under [" + root + "].");
|
||||
return (1);
|
||||
}
|
||||
|
||||
System.out.println("Creating qbit-" + dashName + ":");
|
||||
System.out.printf("%13s %s\n", "packgaename:", packageName);
|
||||
System.out.printf("%13s %s\n", "ClassName:", className);
|
||||
System.out.printf("%13s %s\n", "varName:", varName);
|
||||
System.out.println();
|
||||
|
||||
System.out.println("Copying template...");
|
||||
ProcessResult cpResult = run(new ProcessBuilder(CP, "-rv", template.getAbsolutePath(), dir.getAbsolutePath()));
|
||||
System.out.print(cpResult.stdout());
|
||||
System.out.println();
|
||||
|
||||
System.out.println("Renaming files...");
|
||||
renameFiles(dir, packageName, className);
|
||||
System.out.println();
|
||||
|
||||
System.out.println("Updating file contents...");
|
||||
replacePlaceholders(dir, dashName, packageName, className, varName);
|
||||
System.out.println();
|
||||
|
||||
System.out.println("Init'ing git repo...");
|
||||
run(new ProcessBuilder(GIT, "init").directory(dir));
|
||||
System.out.println();
|
||||
|
||||
// git remote add origin https://github.com/Kingsrook/${name}.git ?
|
||||
// echo https://app.circleci.com/projects/project-dashboard/github/Kingsrook after initial push
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
e.printStackTrace();
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
void renameFiles(File dir, String packageName, String className) throws Exception
|
||||
{
|
||||
String srcPath = dir.getAbsolutePath() + "/src/main/java/com/kingsrook/qbits";
|
||||
String packagePath = packageName.replace('.', '/');
|
||||
System.out.print(run(new ProcessBuilder(MV, "-v", srcPath + "/todo/TodoQBitConfig.java", srcPath + "/todo/" + className + "QBitConfig.java")).stdout());
|
||||
System.out.print(run(new ProcessBuilder(MV, "-v", srcPath + "/todo/TodoQBitProducer.java", srcPath + "/todo/" + className + "QBitProducer.java")).stdout());
|
||||
System.out.print(run(new ProcessBuilder(MV, "-v", srcPath + "/todo", srcPath + "/" + packagePath)).stdout());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
static void replacePlaceholders(File dir, String dashName, String packageName, String className, String varName) throws Exception
|
||||
{
|
||||
for(File file : dir.listFiles())
|
||||
{
|
||||
if(file.isDirectory())
|
||||
{
|
||||
replacePlaceholders(file, dashName, packageName, className, varName);
|
||||
continue;
|
||||
}
|
||||
|
||||
System.out.println("Replacing placeholders in: " + file.getAbsolutePath());
|
||||
replaceOne("dashName", dashName, file);
|
||||
replaceOne("packageName", packageName, file);
|
||||
replaceOne("className", className, file);
|
||||
replaceOne("varName", varName, file);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
static void replaceOne(String from, String to, File file) throws Exception
|
||||
{
|
||||
run(new ProcessBuilder(SED, "s/\\${" + from + "}/" + to + "/g", "-i", file.getAbsolutePath()));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
public record ProcessResult(Integer exitCode, String stdout, String stderr)
|
||||
{
|
||||
|
||||
/***************************************************************************
|
||||
*
|
||||
***************************************************************************/
|
||||
public boolean hasStdout()
|
||||
{
|
||||
return stdout != null && !stdout.isEmpty();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
*
|
||||
***************************************************************************/
|
||||
public boolean hasStderr()
|
||||
{
|
||||
return stderr != null && !stderr.isEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
public static ProcessResult run(ProcessBuilder builder) throws Exception
|
||||
{
|
||||
StringBuilder stdout = new StringBuilder();
|
||||
StringBuilder stderr = new StringBuilder();
|
||||
|
||||
Process process = builder.start();
|
||||
Future<?> stdoutFuture = getExecutorService().submit(new StreamGobbler(process.getInputStream(), stdout::append));
|
||||
Future<?> stderrFuture = getExecutorService().submit(new StreamGobbler(process.getErrorStream(), stderr::append));
|
||||
|
||||
int exitCode = process.waitFor();
|
||||
stdoutFuture.get();
|
||||
stderrFuture.get();
|
||||
|
||||
return (new ProcessResult(exitCode, stdout.toString(), stderr.toString()));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
private static class StreamGobbler implements Runnable
|
||||
{
|
||||
private InputStream inputStream;
|
||||
private Consumer<String> consumer;
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
public StreamGobbler(InputStream inputStream, Consumer<String> consumer)
|
||||
{
|
||||
this.inputStream = inputStream;
|
||||
this.consumer = consumer;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
new BufferedReader(new InputStreamReader(inputStream)).lines().forEach(s -> consumer.accept(s + System.lineSeparator()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
private static ExecutorService getExecutorService()
|
||||
{
|
||||
if(executorService == null)
|
||||
{
|
||||
executorService = Executors.newCachedThreadPool();
|
||||
}
|
||||
return (executorService);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
private String makeWordsName(String s)
|
||||
{
|
||||
if(s.contains("-"))
|
||||
{
|
||||
return (s.toLowerCase().replace('-', ' '));
|
||||
}
|
||||
|
||||
if(s.matches(".*[A-Z].*"))
|
||||
{
|
||||
return s.replaceAll("([A-Z])", "$1'").toLowerCase().trim();
|
||||
}
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
static String stripQBitPrefix(String s)
|
||||
{
|
||||
return (s.replaceFirst("^qbit(s) ", ""));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
static String makeDashName(String s)
|
||||
{
|
||||
return (s.replace(' ', '-'));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
static String makePackageName(String s)
|
||||
{
|
||||
return (s.replace(" ", ""));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
static String makeClassName(String s)
|
||||
{
|
||||
StringBuilder rs = new StringBuilder();
|
||||
String[] words = s.split(" ");
|
||||
for(String word : words)
|
||||
{
|
||||
rs.append(word.substring(0, 1).toUpperCase());
|
||||
if(word.length() > 1)
|
||||
{
|
||||
rs.append(word.substring(1));
|
||||
}
|
||||
}
|
||||
return rs.toString();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
static String makeVarName(String s)
|
||||
{
|
||||
String className = makeClassName(s);
|
||||
return className.substring(0, 1).toLowerCase() + (className.length() == 1 ? "" : className.substring(1));
|
||||
}
|
||||
}
|
@ -56,6 +56,9 @@ public class ApiInstanceMetaDataProvider
|
||||
public static final String TABLE_NAME_API_LOG = "apiLog";
|
||||
public static final String TABLE_NAME_API_LOG_USER = "apiLogUser";
|
||||
|
||||
public static final String API_NAME_PVS_NAME = "apiName";
|
||||
public static final String API_VERSION_PVS_NAME = "apiVersion";
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -142,7 +145,7 @@ public class ApiInstanceMetaDataProvider
|
||||
}
|
||||
|
||||
instance.addPossibleValueSource(new QPossibleValueSource()
|
||||
.withName("apiName")
|
||||
.withName(API_NAME_PVS_NAME)
|
||||
.withType(QPossibleValueSourceType.ENUM)
|
||||
.withEnumValues(apiNamePossibleValues));
|
||||
|
||||
@ -152,7 +155,7 @@ public class ApiInstanceMetaDataProvider
|
||||
}
|
||||
|
||||
instance.addPossibleValueSource(new QPossibleValueSource()
|
||||
.withName("apiVersion")
|
||||
.withName(API_VERSION_PVS_NAME)
|
||||
.withType(QPossibleValueSourceType.ENUM)
|
||||
.withEnumValues(apiVersionPossibleValues));
|
||||
}
|
||||
|
@ -122,18 +122,6 @@
|
||||
<build>
|
||||
<sourceDirectory>src/main/java</sourceDirectory>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>3.5.3</version>
|
||||
<configuration>
|
||||
<additionalClasspathElements>
|
||||
<additionalClasspathElement>
|
||||
${project.basedir}/src/test/resources/static-site.jar
|
||||
</additionalClasspathElement>
|
||||
</additionalClasspathElements>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
|
@ -1925,26 +1925,18 @@ public class QJavalinImplementation
|
||||
{
|
||||
String searchTerm = context.queryParam("searchTerm");
|
||||
String ids = context.queryParam("ids");
|
||||
String labels = context.queryParam("labels");
|
||||
|
||||
SearchPossibleValueSourceInput input = new SearchPossibleValueSourceInput();
|
||||
setupSession(context, input);
|
||||
input.setPossibleValueSourceName(possibleValueSourceName);
|
||||
input.setSearchTerm(searchTerm);
|
||||
input.setDefaultQueryFilter(defaultFilter);
|
||||
input.setPathParamMap(context.pathParamMap());
|
||||
input.setQueryParamMap(context.queryParamMap());
|
||||
|
||||
if(StringUtils.hasContent(ids))
|
||||
{
|
||||
List<Serializable> idList = new ArrayList<>(Arrays.asList(ids.split(",")));
|
||||
input.setIdList(idList);
|
||||
}
|
||||
else if(StringUtils.hasContent(labels))
|
||||
{
|
||||
List<String> labelList = new ArrayList<>(Arrays.asList(labels.split(",")));
|
||||
input.setLabelList(labelList);
|
||||
}
|
||||
|
||||
SearchPossibleValueSourceOutput output = new SearchPossibleValueSourceAction().execute(input);
|
||||
|
||||
|
@ -25,8 +25,10 @@ package com.kingsrook.qqq.backend.javalin;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
import com.kingsrook.qqq.backend.core.instances.QInstanceValidator;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QSupplementalInstanceMetaData;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import com.kingsrook.qqq.middleware.javalin.metadata.JavalinRouteProviderMetaData;
|
||||
import org.apache.logging.log4j.Level;
|
||||
|
||||
@ -329,4 +331,16 @@ public class QJavalinMetaData implements QSupplementalInstanceMetaData
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public void validate(QInstance qInstance, QInstanceValidator validator)
|
||||
{
|
||||
for(JavalinRouteProviderMetaData routeProviderMetaData : CollectionUtils.nonNullList(routeProviders))
|
||||
{
|
||||
routeProviderMetaData.validate(qInstance, validator);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -23,8 +23,13 @@ package com.kingsrook.qqq.middleware.javalin.metadata;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
import com.kingsrook.qqq.backend.core.instances.QInstanceValidator;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QMetaDataObject;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
import com.kingsrook.qqq.middleware.javalin.routeproviders.authentication.RouteAuthenticatorInterface;
|
||||
import com.kingsrook.qqq.middleware.javalin.routeproviders.contexthandlers.RouteProviderContextHandlerInterface;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -32,6 +37,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
|
||||
*******************************************************************************/
|
||||
public class JavalinRouteProviderMetaData implements QMetaDataObject
|
||||
{
|
||||
private String name;
|
||||
private String hostedPath;
|
||||
|
||||
private String fileSystemPath;
|
||||
@ -40,6 +46,7 @@ public class JavalinRouteProviderMetaData implements QMetaDataObject
|
||||
private List<String> methods;
|
||||
|
||||
private QCodeReference routeAuthenticator;
|
||||
private QCodeReference contextHandler;
|
||||
|
||||
|
||||
|
||||
@ -206,4 +213,90 @@ public class JavalinRouteProviderMetaData implements QMetaDataObject
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for contextHandler
|
||||
*******************************************************************************/
|
||||
public QCodeReference getContextHandler()
|
||||
{
|
||||
return (this.contextHandler);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for contextHandler
|
||||
*******************************************************************************/
|
||||
public void setContextHandler(QCodeReference contextHandler)
|
||||
{
|
||||
this.contextHandler = contextHandler;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for contextHandler
|
||||
*******************************************************************************/
|
||||
public JavalinRouteProviderMetaData withContextHandler(QCodeReference contextHandler)
|
||||
{
|
||||
this.contextHandler = contextHandler;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for name
|
||||
*******************************************************************************/
|
||||
public String getName()
|
||||
{
|
||||
return (this.name);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for name
|
||||
*******************************************************************************/
|
||||
public void setName(String name)
|
||||
{
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for name
|
||||
*******************************************************************************/
|
||||
public JavalinRouteProviderMetaData withName(String name)
|
||||
{
|
||||
this.name = name;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
public void validate(QInstance qInstance, QInstanceValidator validator)
|
||||
{
|
||||
String prefix = "In javalinRouteProvider '" + name + "', ";
|
||||
if(StringUtils.hasContent(processName))
|
||||
{
|
||||
validator.assertCondition(qInstance.getProcesses().containsKey(processName), prefix + "unrecognized process name: " + processName + " in a javalinRouteProvider");
|
||||
}
|
||||
|
||||
if(routeAuthenticator != null)
|
||||
{
|
||||
validator.validateSimpleCodeReference(prefix + "routeAuthenticator ", routeAuthenticator, RouteAuthenticatorInterface.class);
|
||||
}
|
||||
|
||||
if(contextHandler != null)
|
||||
{
|
||||
validator.validateSimpleCodeReference(prefix + "contextHandler ", contextHandler, RouteProviderContextHandlerInterface.class);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -22,34 +22,27 @@
|
||||
package com.kingsrook.qqq.middleware.javalin.routeproviders;
|
||||
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.io.Serializable;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
|
||||
import com.kingsrook.qqq.backend.core.actions.processes.RunProcessAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.StorageAction;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.storage.StorageInput;
|
||||
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.session.QSystemUserSession;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||
import com.kingsrook.qqq.backend.javalin.QJavalinImplementation;
|
||||
import com.kingsrook.qqq.backend.javalin.QJavalinUtils;
|
||||
import com.kingsrook.qqq.middleware.javalin.QJavalinRouteProviderInterface;
|
||||
import com.kingsrook.qqq.middleware.javalin.metadata.JavalinRouteProviderMetaData;
|
||||
import com.kingsrook.qqq.middleware.javalin.routeproviders.authentication.RouteAuthenticatorInterface;
|
||||
import com.kingsrook.qqq.middleware.javalin.routeproviders.contexthandlers.DefaultRouteProviderContextHandler;
|
||||
import com.kingsrook.qqq.middleware.javalin.routeproviders.contexthandlers.RouteProviderContextHandlerInterface;
|
||||
import io.javalin.apibuilder.ApiBuilder;
|
||||
import io.javalin.apibuilder.EndpointGroup;
|
||||
import io.javalin.http.Context;
|
||||
import io.javalin.http.HttpStatus;
|
||||
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||
|
||||
|
||||
@ -65,6 +58,7 @@ public class ProcessBasedRouter implements QJavalinRouteProviderInterface
|
||||
private final List<String> methods;
|
||||
|
||||
private QCodeReference routeAuthenticator;
|
||||
private QCodeReference contextHandler;
|
||||
|
||||
private QInstance qInstance;
|
||||
|
||||
@ -88,6 +82,7 @@ public class ProcessBasedRouter implements QJavalinRouteProviderInterface
|
||||
{
|
||||
this(routeProvider.getHostedPath(), routeProvider.getProcessName(), routeProvider.getMethods());
|
||||
setRouteAuthenticator(routeProvider.getRouteAuthenticator());
|
||||
setContextHandler(routeProvider.getContextHandler());
|
||||
}
|
||||
|
||||
|
||||
@ -188,72 +183,27 @@ public class ProcessBasedRouter implements QJavalinRouteProviderInterface
|
||||
{
|
||||
LOG.info("Running process to serve route", logPair("processName", processName), logPair("path", context.path()));
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
// handle request (either using route's specific context handler, or a default one) //
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
RouteProviderContextHandlerInterface contextHandler = createContextHandler();
|
||||
contextHandler.handleRequest(context, input);
|
||||
|
||||
// todo - make the inputStream available to the process to stream results?
|
||||
// maybe via the callback object??? input.setCallback(new QProcessCallback() {});
|
||||
// context.resultInputStream();
|
||||
|
||||
/////////////////////
|
||||
// run the process //
|
||||
/////////////////////
|
||||
input.setFrontendStepBehavior(RunProcessInput.FrontendStepBehavior.SKIP);
|
||||
input.addValue("path", context.path());
|
||||
input.addValue("method", context.method());
|
||||
input.addValue("pathParams", new HashMap<>(context.pathParamMap()));
|
||||
input.addValue("queryParams", new HashMap<>(context.queryParamMap()));
|
||||
input.addValue("formParams", new HashMap<>(context.formParamMap()));
|
||||
input.addValue("cookies", new HashMap<>(context.cookieMap()));
|
||||
input.addValue("requestHeaders", new HashMap<>(context.headerMap()));
|
||||
|
||||
RunProcessOutput runProcessOutput = new RunProcessAction().execute(input);
|
||||
|
||||
/////////////////
|
||||
// headers map //
|
||||
/////////////////
|
||||
Serializable headers = runProcessOutput.getValue("responseHeaders");
|
||||
if(headers instanceof Map headersMap)
|
||||
/////////////////////
|
||||
// handle response //
|
||||
/////////////////////
|
||||
if(contextHandler.handleResponse(context, runProcessOutput))
|
||||
{
|
||||
for(Object key : headersMap.keySet())
|
||||
{
|
||||
context.header(ValueUtils.getValueAsString(key), ValueUtils.getValueAsString(headersMap.get(key)));
|
||||
}
|
||||
}
|
||||
|
||||
// todo - make the inputStream available to the process
|
||||
// maybe via the callback object??? input.setCallback(new QProcessCallback() {});
|
||||
// context.resultInputStream();
|
||||
|
||||
//////////////
|
||||
// response //
|
||||
//////////////
|
||||
Integer statusCode = runProcessOutput.getValueInteger("statusCode");
|
||||
String redirectURL = runProcessOutput.getValueString("redirectURL");
|
||||
String responseString = runProcessOutput.getValueString("responseString");
|
||||
byte[] responseBytes = runProcessOutput.getValueByteArray("responseBytes");
|
||||
StorageInput responseStorageInput = (StorageInput) runProcessOutput.getValue("responseStorageInput");
|
||||
|
||||
if(StringUtils.hasContent(redirectURL))
|
||||
{
|
||||
context.redirect(redirectURL, statusCode == null ? HttpStatus.FOUND : HttpStatus.forStatus(statusCode));
|
||||
return;
|
||||
}
|
||||
|
||||
if(statusCode != null)
|
||||
{
|
||||
context.status(statusCode);
|
||||
}
|
||||
|
||||
if(StringUtils.hasContent(responseString))
|
||||
{
|
||||
context.result(responseString);
|
||||
return;
|
||||
}
|
||||
|
||||
if(responseBytes != null && responseBytes.length > 0)
|
||||
{
|
||||
context.result(responseBytes);
|
||||
return;
|
||||
}
|
||||
|
||||
if(responseStorageInput != null)
|
||||
{
|
||||
InputStream inputStream = new StorageAction().getInputStream(responseStorageInput);
|
||||
context.result(inputStream);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -271,6 +221,23 @@ public class ProcessBasedRouter implements QJavalinRouteProviderInterface
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
private RouteProviderContextHandlerInterface createContextHandler()
|
||||
{
|
||||
if(contextHandler != null)
|
||||
{
|
||||
return QCodeLoader.getAdHoc(RouteProviderContextHandlerInterface.class, this.contextHandler);
|
||||
}
|
||||
else
|
||||
{
|
||||
return (new DefaultRouteProviderContextHandler());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for routeAuthenticator
|
||||
*******************************************************************************/
|
||||
@ -300,4 +267,35 @@ public class ProcessBasedRouter implements QJavalinRouteProviderInterface
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for contextHandler
|
||||
*******************************************************************************/
|
||||
public QCodeReference getContextHandler()
|
||||
{
|
||||
return (this.contextHandler);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for contextHandler
|
||||
*******************************************************************************/
|
||||
public void setContextHandler(QCodeReference contextHandler)
|
||||
{
|
||||
this.contextHandler = contextHandler;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for contextHandler
|
||||
*******************************************************************************/
|
||||
public ProcessBasedRouter withContextHandler(QCodeReference contextHandler)
|
||||
{
|
||||
this.contextHandler = contextHandler;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -41,6 +41,7 @@ public class ProcessBasedRouterPayload extends QProcessPayload
|
||||
private Map<String, List<String>> queryParams;
|
||||
private Map<String, List<String>> formParams;
|
||||
private Map<String, String> cookies;
|
||||
private String bodyString;
|
||||
|
||||
private Integer statusCode;
|
||||
private String redirectURL;
|
||||
@ -410,4 +411,35 @@ public class ProcessBasedRouterPayload extends QProcessPayload
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for bodyString
|
||||
*******************************************************************************/
|
||||
public String getBodyString()
|
||||
{
|
||||
return (this.bodyString);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for bodyString
|
||||
*******************************************************************************/
|
||||
public void setBodyString(String bodyString)
|
||||
{
|
||||
this.bodyString = bodyString;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for bodyString
|
||||
*******************************************************************************/
|
||||
public ProcessBasedRouterPayload withBodyString(String bodyString)
|
||||
{
|
||||
this.bodyString = bodyString;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -49,14 +49,16 @@ import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||
public class SimpleFileSystemDirectoryRouter implements QJavalinRouteProviderInterface
|
||||
{
|
||||
private static final QLogger LOG = QLogger.getLogger(SimpleFileSystemDirectoryRouter.class);
|
||||
public static boolean loadStaticFilesFromJar = false;
|
||||
|
||||
|
||||
private final String hostedPath;
|
||||
private final String fileSystemPath;
|
||||
|
||||
private QCodeReference routeAuthenticator;
|
||||
|
||||
private QInstance qInstance;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor
|
||||
**
|
||||
@ -65,24 +67,6 @@ public class SimpleFileSystemDirectoryRouter implements QJavalinRouteProviderInt
|
||||
{
|
||||
this.hostedPath = hostedPath;
|
||||
this.fileSystemPath = fileSystemPath;
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// read the property to see if we should load static files from the jar file or from the file system //
|
||||
// Javan only supports loading via one method per path, so its a choice of one or the other... //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
try
|
||||
{
|
||||
String propertyName = "qqq.javalin.enableStaticFilesFromJar"; // TODO: make a more general way to handle properties like this system-wide via a central config class
|
||||
String propertyValue = System.getProperty(propertyName, "");
|
||||
if(propertyValue.equals("true"))
|
||||
{
|
||||
loadStaticFilesFromJar = true;
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -113,24 +97,6 @@ public class SimpleFileSystemDirectoryRouter implements QJavalinRouteProviderInt
|
||||
**
|
||||
***************************************************************************/
|
||||
private void handleJavalinStaticFileConfig(StaticFileConfig staticFileConfig)
|
||||
{
|
||||
|
||||
if(!hostedPath.startsWith("/"))
|
||||
{
|
||||
LOG.warn("hostedPath [" + hostedPath + "] should probably start with a leading slash...");
|
||||
}
|
||||
|
||||
/// /////////////////////////////////////////////////////////////////////////////////////
|
||||
// Handle loading static files from the jar OR the filesystem based on system property //
|
||||
/// /////////////////////////////////////////////////////////////////////////////////////
|
||||
if(SimpleFileSystemDirectoryRouter.loadStaticFilesFromJar)
|
||||
{
|
||||
staticFileConfig.directory = fileSystemPath;
|
||||
staticFileConfig.hostedPath = hostedPath;
|
||||
staticFileConfig.location = Location.CLASSPATH;
|
||||
LOG.info("Static File Config : hostedPath [" + hostedPath + "] : directory [" + staticFileConfig.directory + "] : location [CLASSPATH]");
|
||||
}
|
||||
else
|
||||
{
|
||||
URL resource = getClass().getClassLoader().getResource(fileSystemPath);
|
||||
if(resource == null)
|
||||
@ -143,12 +109,14 @@ public class SimpleFileSystemDirectoryRouter implements QJavalinRouteProviderInt
|
||||
throw new RuntimeException(message);
|
||||
}
|
||||
|
||||
if(!hostedPath.startsWith("/"))
|
||||
{
|
||||
LOG.warn("hostedPath [" + hostedPath + "] should probably start with a leading slash...");
|
||||
}
|
||||
|
||||
staticFileConfig.directory = resource.getFile();
|
||||
staticFileConfig.hostedPath = hostedPath;
|
||||
staticFileConfig.location = Location.EXTERNAL;
|
||||
LOG.info("Static File Config : hostedPath [" + hostedPath + "] : directory [" + staticFileConfig.directory + "] : location [EXTERNAL]");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
@ -0,0 +1,159 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.middleware.javalin.routeproviders.contexthandlers;
|
||||
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.io.Serializable;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.StorageAction;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.storage.StorageInput;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||
import io.javalin.http.Context;
|
||||
import io.javalin.http.HttpStatus;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** default implementation of this interface. reads the request body as a string
|
||||
*******************************************************************************/
|
||||
public class DefaultRouteProviderContextHandler implements RouteProviderContextHandlerInterface
|
||||
{
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public void handleRequest(Context context, RunProcessInput input)
|
||||
{
|
||||
input.addValue("path", context.path());
|
||||
input.addValue("method", context.method());
|
||||
input.addValue("pathParams", new HashMap<>(context.pathParamMap()));
|
||||
input.addValue("queryParams", new HashMap<>(context.queryParamMap()));
|
||||
input.addValue("cookies", new HashMap<>(context.cookieMap()));
|
||||
input.addValue("requestHeaders", new HashMap<>(context.headerMap()));
|
||||
|
||||
handleRequestBody(context, input);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
protected void handleRequestBody(Context context, RunProcessInput input)
|
||||
{
|
||||
input.addValue("formParams", new HashMap<>(context.formParamMap()));
|
||||
input.addValue("bodyString", context.body());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public boolean handleResponse(Context context, RunProcessOutput runProcessOutput) throws QException
|
||||
{
|
||||
handleResponseHeaders(context, runProcessOutput);
|
||||
|
||||
//////////////
|
||||
// response //
|
||||
//////////////
|
||||
Integer statusCode = runProcessOutput.getValueInteger("statusCode");
|
||||
String redirectURL = runProcessOutput.getValueString("redirectURL");
|
||||
|
||||
if(StringUtils.hasContent(redirectURL))
|
||||
{
|
||||
context.redirect(redirectURL, statusCode == null ? HttpStatus.FOUND : HttpStatus.forStatus(statusCode));
|
||||
return true;
|
||||
}
|
||||
|
||||
if(statusCode != null)
|
||||
{
|
||||
context.status(statusCode);
|
||||
}
|
||||
|
||||
if(handleResponseBody(context, runProcessOutput))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
protected void handleResponseHeaders(Context context, RunProcessOutput runProcessOutput)
|
||||
{
|
||||
/////////////////
|
||||
// headers map //
|
||||
/////////////////
|
||||
Serializable headers = runProcessOutput.getValue("responseHeaders");
|
||||
if(headers instanceof Map headersMap)
|
||||
{
|
||||
for(Object key : headersMap.keySet())
|
||||
{
|
||||
context.header(ValueUtils.getValueAsString(key), ValueUtils.getValueAsString(headersMap.get(key)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
protected boolean handleResponseBody(Context context, RunProcessOutput runProcessOutput) throws QException
|
||||
{
|
||||
String responseString = runProcessOutput.getValueString("responseString");
|
||||
byte[] responseBytes = runProcessOutput.getValueByteArray("responseBytes");
|
||||
StorageInput responseStorageInput = (StorageInput) runProcessOutput.getValue("responseStorageInput");
|
||||
if(StringUtils.hasContent(responseString))
|
||||
{
|
||||
context.result(responseString);
|
||||
return true;
|
||||
}
|
||||
|
||||
if(responseBytes != null && responseBytes.length > 0)
|
||||
{
|
||||
context.result(responseBytes);
|
||||
return true;
|
||||
}
|
||||
|
||||
if(responseStorageInput != null)
|
||||
{
|
||||
InputStream inputStream = new StorageAction().getInputStream(responseStorageInput);
|
||||
context.result(inputStream);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.middleware.javalin.routeproviders.contexthandlers;
|
||||
|
||||
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessOutput;
|
||||
import io.javalin.http.Context;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** interface for how to handle the javalin context for a process based route provider.
|
||||
** e.g., taking things like query params and the request body into the process input
|
||||
** and similarly for the http response from the process output..
|
||||
*******************************************************************************/
|
||||
public interface RouteProviderContextHandlerInterface
|
||||
{
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
void handleRequest(Context context, RunProcessInput runProcessInput);
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
boolean handleResponse(Context context, RunProcessOutput runProcessOutput) throws QException;
|
||||
|
||||
}
|
@ -1504,13 +1504,6 @@ paths:
|
||||
name: "person.bulkDelete"
|
||||
stepFlow: "LINEAR"
|
||||
tableName: "person"
|
||||
person.bulkEditWithFile:
|
||||
hasPermission: true
|
||||
isHidden: true
|
||||
label: "Person Bulk Edit With File"
|
||||
name: "person.bulkEditWithFile"
|
||||
stepFlow: "LINEAR"
|
||||
tableName: "person"
|
||||
tables:
|
||||
person:
|
||||
capabilities:
|
||||
|
@ -29,7 +29,6 @@ import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.instances.AbstractQQQApplication;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.javalin.TestUtils;
|
||||
import com.kingsrook.qqq.middleware.javalin.routeproviders.SimpleFileSystemDirectoryRouter;
|
||||
import com.kingsrook.qqq.middleware.javalin.specs.v1.MiddlewareVersionV1;
|
||||
import io.javalin.http.HttpStatus;
|
||||
import kong.unirest.HttpResponse;
|
||||
@ -53,16 +52,6 @@ class QApplicationJavalinServerTest
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
private static AbstractQQQApplication getQqqApplication()
|
||||
{
|
||||
return new TestApplication();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@ -71,7 +60,6 @@ class QApplicationJavalinServerTest
|
||||
{
|
||||
javalinServer.stop();
|
||||
TestApplication.callCount = 0;
|
||||
System.clearProperty("qqq.javalin.enableStaticFilesFromJar");
|
||||
}
|
||||
|
||||
|
||||
@ -208,48 +196,6 @@ class QApplicationJavalinServerTest
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testStaticRouterFilesFromExternal() throws Exception
|
||||
{
|
||||
System.setProperty("qqq.javalin.enableStaticFilesFromJar", "false");
|
||||
|
||||
javalinServer = new QApplicationJavalinServer(getQqqApplication())
|
||||
.withServeFrontendMaterialDashboard(false)
|
||||
.withPort(PORT);
|
||||
javalinServer.start();
|
||||
|
||||
Unirest.config().setDefaultResponseEncoding("UTF-8");
|
||||
HttpResponse<String> response = Unirest.get("http://localhost:" + PORT + "/statically-served/foo.html").asString();
|
||||
assertEquals("Foo? Bar!", response.getBody());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testStaticRouterFilesFromClasspath() throws Exception
|
||||
{
|
||||
System.setProperty("qqq.javalin.enableStaticFilesFromJar", "true");
|
||||
|
||||
javalinServer = new QApplicationJavalinServer(new QApplicationJavalinServerTest.TestApplication())
|
||||
.withServeFrontendMaterialDashboard(false)
|
||||
.withPort(PORT)
|
||||
.withAdditionalRouteProvider(new SimpleFileSystemDirectoryRouter("/statically-served-from-jar", "static-site-from-jar/"));
|
||||
|
||||
javalinServer.start();
|
||||
|
||||
Unirest.config().setDefaultResponseEncoding("UTF-8");
|
||||
HttpResponse<String> response = Unirest.get("http://localhost:" + PORT + "/statically-served-from-jar/foo-in-jar.html").asString();
|
||||
assertEquals("Foo in a Jar!\n", response.getBody());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@ -350,6 +296,16 @@ class QApplicationJavalinServerTest
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
private static AbstractQQQApplication getQqqApplication()
|
||||
{
|
||||
return new TestApplication();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
|
Binary file not shown.
Reference in New Issue
Block a user