Merged feature/webhooks into integration

This commit is contained in:
2025-06-18 19:41:58 -05:00
19 changed files with 1071 additions and 35 deletions

View File

@ -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;
}
}

View File

@ -401,6 +401,7 @@ public class DeleteAction
if(CollectionUtils.nullSafeHasContents(associatedKeys)) if(CollectionUtils.nullSafeHasContents(associatedKeys))
{ {
DeleteInput nextLevelDeleteInput = new DeleteInput(); DeleteInput nextLevelDeleteInput = new DeleteInput();
nextLevelDeleteInput.setFlags(deleteInput.getFlags());
nextLevelDeleteInput.setTransaction(deleteInput.getTransaction()); nextLevelDeleteInput.setTransaction(deleteInput.getTransaction());
nextLevelDeleteInput.setTableName(association.getAssociatedTableName()); nextLevelDeleteInput.setTableName(association.getAssociatedTableName());
nextLevelDeleteInput.setPrimaryKeys(associatedKeys); nextLevelDeleteInput.setPrimaryKeys(associatedKeys);

View File

@ -34,7 +34,6 @@ import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import com.kingsrook.qqq.backend.core.actions.AbstractQActionFunction; import com.kingsrook.qqq.backend.core.actions.AbstractQActionFunction;
import com.kingsrook.qqq.backend.core.actions.ActionHelper; 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.audits.DMLAuditAction;
import com.kingsrook.qqq.backend.core.actions.automation.AutomationStatus; import com.kingsrook.qqq.backend.core.actions.automation.AutomationStatus;
import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationStatusUpdater; import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationStatusUpdater;
@ -158,7 +157,7 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
////////////////////////////////////////////////// //////////////////////////////////////////////////
// insert any associations in the input records // // insert any associations in the input records //
////////////////////////////////////////////////// //////////////////////////////////////////////////
manageAssociations(table, insertOutput.getRecords(), insertInput.getTransaction()); manageAssociations(table, insertOutput.getRecords(), insertInput);
////////////////// //////////////////
// do the audit // // do the audit //
@ -386,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())) for(Association association : CollectionUtils.nonNullList(table.getAssociations()))
{ {
@ -419,7 +418,8 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
if(CollectionUtils.nullSafeHasContents(nextLevelInserts)) if(CollectionUtils.nullSafeHasContents(nextLevelInserts))
{ {
InsertInput nextLevelInsertInput = new InsertInput(); InsertInput nextLevelInsertInput = new InsertInput();
nextLevelInsertInput.setTransaction(transaction); nextLevelInsertInput.withFlags(insertInput.getFlags());
nextLevelInsertInput.setTransaction(insertInput.getTransaction());
nextLevelInsertInput.setTableName(association.getAssociatedTableName()); nextLevelInsertInput.setTableName(association.getAssociatedTableName());
nextLevelInsertInput.setRecords(nextLevelInserts); nextLevelInsertInput.setRecords(nextLevelInserts);
InsertOutput nextLevelInsertOutput = new InsertAction().execute(nextLevelInsertInput); InsertOutput nextLevelInsertOutput = new InsertAction().execute(nextLevelInsertInput);

View File

@ -126,6 +126,7 @@ public class ReplaceAction extends AbstractQActionFunction<ReplaceInput, Replace
InsertInput insertInput = new InsertInput(); InsertInput insertInput = new InsertInput();
insertInput.setTableName(table.getName()); insertInput.setTableName(table.getName());
insertInput.setRecords(insertList); insertInput.setRecords(insertList);
insertInput.withFlags(input.getFlags());
insertInput.setTransaction(transaction); insertInput.setTransaction(transaction);
insertInput.setOmitDmlAudit(input.getOmitDmlAudit()); insertInput.setOmitDmlAudit(input.getOmitDmlAudit());
InsertOutput insertOutput = new InsertAction().execute(insertInput); InsertOutput insertOutput = new InsertAction().execute(insertInput);
@ -135,6 +136,7 @@ public class ReplaceAction extends AbstractQActionFunction<ReplaceInput, Replace
UpdateInput updateInput = new UpdateInput(); UpdateInput updateInput = new UpdateInput();
updateInput.setTableName(table.getName()); updateInput.setTableName(table.getName());
updateInput.setRecords(updateList); updateInput.setRecords(updateList);
updateInput.withFlags(input.getFlags());
updateInput.setTransaction(transaction); updateInput.setTransaction(transaction);
updateInput.setOmitDmlAudit(input.getOmitDmlAudit()); updateInput.setOmitDmlAudit(input.getOmitDmlAudit());
UpdateOutput updateOutput = new UpdateAction().execute(updateInput); UpdateOutput updateOutput = new UpdateAction().execute(updateInput);
@ -151,6 +153,7 @@ public class ReplaceAction extends AbstractQActionFunction<ReplaceInput, Replace
DeleteInput deleteInput = new DeleteInput(); DeleteInput deleteInput = new DeleteInput();
deleteInput.setTableName(table.getName()); deleteInput.setTableName(table.getName());
deleteInput.setQueryFilter(deleteFilter); deleteInput.setQueryFilter(deleteFilter);
deleteInput.withFlags(input.getFlags());
deleteInput.setTransaction(transaction); deleteInput.setTransaction(transaction);
deleteInput.setOmitDmlAudit(input.getOmitDmlAudit()); deleteInput.setOmitDmlAudit(input.getOmitDmlAudit());
DeleteOutput deleteOutput = new DeleteAction().execute(deleteInput); DeleteOutput deleteOutput = new DeleteAction().execute(deleteInput);

View File

@ -605,6 +605,7 @@ public class UpdateAction
{ {
LOG.debug("Deleting associatedRecords", logPair("associatedTable", associatedTable.getName()), logPair("noOfRecords", queryOutput.getRecords().size())); LOG.debug("Deleting associatedRecords", logPair("associatedTable", associatedTable.getName()), logPair("noOfRecords", queryOutput.getRecords().size()));
DeleteInput deleteInput = new DeleteInput(); DeleteInput deleteInput = new DeleteInput();
deleteInput.setFlags(updateInput.getFlags());
deleteInput.setTransaction(updateInput.getTransaction()); deleteInput.setTransaction(updateInput.getTransaction());
deleteInput.setTableName(association.getAssociatedTableName()); deleteInput.setTableName(association.getAssociatedTableName());
deleteInput.setPrimaryKeys(queryOutput.getRecords().stream().map(r -> r.getValue(associatedTable.getPrimaryKeyField())).collect(Collectors.toList())); deleteInput.setPrimaryKeys(queryOutput.getRecords().stream().map(r -> r.getValue(associatedTable.getPrimaryKeyField())).collect(Collectors.toList()));
@ -617,6 +618,7 @@ public class UpdateAction
LOG.debug("Updating associatedRecords", logPair("associatedTable", associatedTable.getName()), logPair("noOfRecords", nextLevelUpdates.size())); LOG.debug("Updating associatedRecords", logPair("associatedTable", associatedTable.getName()), logPair("noOfRecords", nextLevelUpdates.size()));
UpdateInput nextLevelUpdateInput = new UpdateInput(); UpdateInput nextLevelUpdateInput = new UpdateInput();
nextLevelUpdateInput.setTransaction(updateInput.getTransaction()); nextLevelUpdateInput.setTransaction(updateInput.getTransaction());
nextLevelUpdateInput.setFlags(updateInput.getFlags());
nextLevelUpdateInput.setTableName(association.getAssociatedTableName()); nextLevelUpdateInput.setTableName(association.getAssociatedTableName());
nextLevelUpdateInput.setRecords(nextLevelUpdates); nextLevelUpdateInput.setRecords(nextLevelUpdates);
UpdateOutput nextLevelUpdateOutput = new UpdateAction().execute(nextLevelUpdateInput); UpdateOutput nextLevelUpdateOutput = new UpdateAction().execute(nextLevelUpdateInput);
@ -627,6 +629,7 @@ public class UpdateAction
LOG.debug("Inserting associatedRecords", logPair("associatedTable", associatedTable.getName()), logPair("noOfRecords", nextLevelUpdates.size())); LOG.debug("Inserting associatedRecords", logPair("associatedTable", associatedTable.getName()), logPair("noOfRecords", nextLevelUpdates.size()));
InsertInput nextLevelInsertInput = new InsertInput(); InsertInput nextLevelInsertInput = new InsertInput();
nextLevelInsertInput.setTransaction(updateInput.getTransaction()); nextLevelInsertInput.setTransaction(updateInput.getTransaction());
nextLevelInsertInput.setFlags(updateInput.getFlags());
nextLevelInsertInput.setTableName(association.getAssociatedTableName()); nextLevelInsertInput.setTableName(association.getAssociatedTableName());
nextLevelInsertInput.setRecords(nextLevelInserts); nextLevelInsertInput.setRecords(nextLevelInserts);
InsertOutput nextLevelInsertOutput = new InsertAction().execute(nextLevelInsertInput); InsertOutput nextLevelInsertOutput = new InsertAction().execute(nextLevelInsertInput);

View File

@ -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 @Override
public QPossibleValue<ID> getPossibleValue(Serializable idValue) public QPossibleValue<ID> getPossibleValue(Serializable idValue) throws QException
{ {
S sourceObject = getSourceObject(idValue); S sourceObject = getSourceObject(idValue);
if(sourceObject == null) if(sourceObject == null)

View File

@ -45,7 +45,7 @@ public interface QCustomPossibleValueProvider<T extends Serializable>
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
QPossibleValue<T> getPossibleValue(Serializable idValue); QPossibleValue<T> getPossibleValue(Serializable idValue) throws QException;
/******************************************************************************* /*******************************************************************************
** **

View File

@ -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
{
}

View File

@ -24,9 +24,12 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.delete;
import java.io.Serializable; import java.io.Serializable;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set;
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction; import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput; 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.InputSource;
import com.kingsrook.qqq.backend.core.model.actions.tables.QInputSource; import com.kingsrook.qqq.backend.core.model.actions.tables.QInputSource;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; 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 boolean omitDmlAudit = false;
private String auditContext = null; private String auditContext = null;
private Set<ActionFlag> flags;
/******************************************************************************* /*******************************************************************************
@ -295,4 +300,65 @@ public class DeleteInput extends AbstractTableActionInput
return (this); 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));
}
} }

View File

@ -23,9 +23,12 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.insert;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set;
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction; import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput; 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.InputSource;
import com.kingsrook.qqq.backend.core.model.actions.tables.QInputSource; import com.kingsrook.qqq.backend.core.model.actions.tables.QInputSource;
import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.data.QRecord;
@ -48,6 +51,8 @@ public class InsertInput extends AbstractTableActionInput
private boolean omitDmlAudit = false; private boolean omitDmlAudit = false;
private String auditContext = null; private String auditContext = null;
private Set<ActionFlag> flags;
/******************************************************************************* /*******************************************************************************
@ -316,4 +321,65 @@ public class InsertInput extends AbstractTableActionInput
return (this); 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));
}
} }

View File

@ -22,9 +22,12 @@
package com.kingsrook.qqq.backend.core.model.actions.tables.replace; package com.kingsrook.qqq.backend.core.model.actions.tables.replace;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set;
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction; import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput; 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.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey; import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey;
@ -45,6 +48,8 @@ public class ReplaceInput extends AbstractTableActionInput
private boolean omitDmlAudit = false; private boolean omitDmlAudit = false;
private Set<ActionFlag> flags;
/******************************************************************************* /*******************************************************************************
@ -303,4 +308,65 @@ public class ReplaceInput extends AbstractTableActionInput
return (this); 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));
}
} }

View File

@ -23,9 +23,12 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.update;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set;
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction; import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput; 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.InputSource;
import com.kingsrook.qqq.backend.core.model.actions.tables.QInputSource; import com.kingsrook.qqq.backend.core.model.actions.tables.QInputSource;
import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.data.QRecord;
@ -56,6 +59,8 @@ public class UpdateInput extends AbstractTableActionInput
private boolean omitModifyDateUpdate = false; private boolean omitModifyDateUpdate = false;
private String auditContext = null; private String auditContext = null;
private Set<ActionFlag> flags;
/******************************************************************************* /*******************************************************************************
@ -385,4 +390,65 @@ public class UpdateInput extends AbstractTableActionInput
return (this); 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));
}
} }

View File

@ -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);
}
}

View File

@ -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.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.audits.AuditLevel; import com.kingsrook.qqq.backend.core.model.metadata.audits.AuditLevel;
import com.kingsrook.qqq.backend.core.model.metadata.audits.QAuditRules; import com.kingsrook.qqq.backend.core.model.metadata.audits.QAuditRules;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.fields.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.QPossibleValueSource;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSourceType; import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSourceType;
import com.kingsrook.qqq.backend.core.model.metadata.tables.Capability; import com.kingsrook.qqq.backend.core.model.metadata.tables.Capability;
@ -125,10 +128,11 @@ public class QQQTablesMetaDataProvider
public QPossibleValueSource defineQQQTablePossibleValueSource() public QPossibleValueSource defineQQQTablePossibleValueSource()
{ {
return (new QPossibleValueSource() return (new QPossibleValueSource()
.withType(QPossibleValueSourceType.TABLE)
.withName(QQQTable.TABLE_NAME) .withName(QQQTable.TABLE_NAME)
.withTableName(QQQTable.TABLE_NAME)) .withType(QPossibleValueSourceType.CUSTOM)
.withOrderByField("label"); .withIdType(QFieldType.INTEGER)
.withCustomCodeReference(new QCodeReference(QQQTableCustomPossibleValueProvider.class))
.withValueFormatAndFields(PVSValueFormatAndFields.LABEL_ONLY));
} }
} }

View File

@ -29,6 +29,7 @@ import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger; import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput; 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.QCriteriaOperator;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
@ -52,10 +53,10 @@ import com.kingsrook.qqq.backend.core.utils.collections.ListBuilder;
/******************************************************************************* /*******************************************************************************
** Base class to manage creating scheduled jobs based on records in another table * Base class to manage creating scheduled jobs based on records in another table
** *
** Expected to be used via BaseSyncToScheduledJobTableCustomizer - see its javadoc. * Expected to be used via BaseSyncToScheduledJobTableCustomizer - see its javadoc.
** * @see BaseSyncToScheduledJobTableCustomizer
*******************************************************************************/ *******************************************************************************/
public abstract class AbstractRecordSyncToScheduledJobProcess extends AbstractTableSyncTransformStep implements MetaDataProducerInterface<QProcessMetaData> public abstract class AbstractRecordSyncToScheduledJobProcess extends AbstractTableSyncTransformStep implements MetaDataProducerInterface<QProcessMetaData>
{ {
@ -65,6 +66,20 @@ public abstract class AbstractRecordSyncToScheduledJobProcess extends AbstractTa
/***************************************************************************
* 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
}
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
@ -89,7 +104,6 @@ public abstract class AbstractRecordSyncToScheduledJobProcess extends AbstractTa
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/

View File

@ -40,9 +40,11 @@ import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryLine
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput; import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessOutput; 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.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.QCriteriaOperator;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.code.InitializableViaCodeReference; 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.QCodeReference;
@ -84,16 +86,44 @@ public class BaseSyncToScheduledJobTableCustomizer implements TableCustomizerInt
/*************************************************************************** /***************************************************************************
** * 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 void setTableCustomizers(QTableMetaData tableMetaData, AbstractRecordSyncToScheduledJobProcess syncProcess) public static QCodeReferenceWithProperties makeCodeReference(QTableMetaData tableMetaData, AbstractRecordSyncToScheduledJobProcess syncProcess)
{ {
QCodeReference codeReference = new QCodeReferenceWithProperties(BaseSyncToScheduledJobTableCustomizer.class, Map.of( return new QCodeReferenceWithProperties(BaseSyncToScheduledJobTableCustomizer.class, Map.of(
KEY_TABLE_NAME, tableMetaData.getName(), KEY_TABLE_NAME, tableMetaData.getName(),
KEY_SYNC_PROCESS_NAME, syncProcess.getClass().getSimpleName(), KEY_SYNC_PROCESS_NAME, syncProcess.getClass().getSimpleName(),
KEY_SCHEDULED_JOB_FOREIGN_KEY_TYPE, syncProcess.getScheduledJobForeignKeyType() 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_INSERT_RECORD, codeReference);
tableMetaData.withCustomizer(TableCustomizers.POST_UPDATE_RECORD, codeReference); tableMetaData.withCustomizer(TableCustomizers.POST_UPDATE_RECORD, codeReference);
tableMetaData.withCustomizer(TableCustomizers.POST_DELETE_RECORD, codeReference); tableMetaData.withCustomizer(TableCustomizers.POST_DELETE_RECORD, codeReference);
@ -138,6 +168,16 @@ public class BaseSyncToScheduledJobTableCustomizer implements TableCustomizerInt
@Override @Override
public List<QRecord> postInsertOrUpdate(AbstractActionInput input, List<QRecord> records, Optional<List<QRecord>> oldRecordList) throws QException 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); runSyncProcessForRecordList(records, syncProcessName);
return records; return records;
} }
@ -157,7 +197,17 @@ public class BaseSyncToScheduledJobTableCustomizer implements TableCustomizerInt
/*************************************************************************** /***************************************************************************
** * 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) public void runSyncProcessForRecordList(List<QRecord> records, String processName)
{ {
@ -199,7 +249,15 @@ public class BaseSyncToScheduledJobTableCustomizer implements TableCustomizerInt
/*************************************************************************** /***************************************************************************
** * 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) public void deleteScheduledJobsForRecordList(List<QRecord> records)
{ {
@ -296,15 +354,6 @@ public class BaseSyncToScheduledJobTableCustomizer implements TableCustomizerInt
} }
/*******************************************************************************
** Getter for KEY_SCHEDULED_JOB_FOREIGN_KEY_TYPE
*******************************************************************************/
public String getKEY_SCHEDULED_JOB_FOREIGN_KEY_TYPE()
{
return (BaseSyncToScheduledJobTableCustomizer.KEY_SCHEDULED_JOB_FOREIGN_KEY_TYPE);
}
/******************************************************************************* /*******************************************************************************
** Getter for scheduledJobForeignKeyType ** Getter for scheduledJobForeignKeyType
@ -335,5 +384,4 @@ public class BaseSyncToScheduledJobTableCustomizer implements TableCustomizerInt
return (this); return (this);
} }
} }

View File

@ -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);
}
}
}

View File

@ -80,7 +80,7 @@ class TablesCustomPossibleValueProviderTest extends BaseTest
** **
*******************************************************************************/ *******************************************************************************/
@Test @Test
void testGetPossibleValue() void testGetPossibleValue() throws QException
{ {
TablesCustomPossibleValueProvider provider = new TablesCustomPossibleValueProvider(); TablesCustomPossibleValueProvider provider = new TablesCustomPossibleValueProvider();

View File

@ -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));
}
}