Add optional variantRecordLookupFunction to BackendVariantsConfig and validation of same; refactor up some shared backend code into BackendVariantsUtil

This commit is contained in:
2025-02-19 19:49:33 -06:00
parent be6d1b888f
commit 8816177df8
5 changed files with 358 additions and 16 deletions

View File

@ -37,7 +37,9 @@ import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.TimeZone;
import java.util.function.Function;
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.TableCustomizers;
@ -115,6 +117,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 com.kingsrook.qqq.backend.core.utils.lambdas.UnsafeFunction;
import com.kingsrook.qqq.backend.core.utils.lambdas.UnsafeLambda;
import org.apache.commons.lang.BooleanUtils;
import org.quartz.CronExpression;
@ -556,8 +559,8 @@ public class QInstanceValidator
{
assertCondition(StringUtils.hasContent(backendVariantsConfig.getVariantTypeKey()), "Missing variantTypeKey in backendVariantsConfig in [" + backendName + "]");
String optionsTableName = backendVariantsConfig.getOptionsTableName();
QTableMetaData optionsTable = qInstance.getTable(optionsTableName);
String optionsTableName = backendVariantsConfig.getOptionsTableName();
QTableMetaData optionsTable = qInstance.getTable(optionsTableName);
if(assertCondition(StringUtils.hasContent(optionsTableName), "Missing optionsTableName in backendVariantsConfig in [" + backendName + "]"))
{
if(assertCondition(optionsTable != null, "Unrecognized optionsTableName [" + optionsTableName + "] in backendVariantsConfig in [" + backendName + "]"))
@ -573,7 +576,11 @@ public class QInstanceValidator
Map<BackendVariantSetting, String> backendSettingSourceFieldNameMap = backendVariantsConfig.getBackendSettingSourceFieldNameMap();
if(assertCondition(CollectionUtils.nullSafeHasContents(backendSettingSourceFieldNameMap), "Missing or empty backendSettingSourceFieldNameMap in backendVariantsConfig in [" + backendName + "]"))
{
if(optionsTable != null)
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
// only validate field names in the backendSettingSourceFieldNameMap if there is NOT a variantRecordSupplier //
// (the idea being, that the supplier might be building a record with fieldNames that aren't in the table... //
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
if(optionsTable != null && backendVariantsConfig.getVariantRecordLookupFunction() == null)
{
for(Map.Entry<BackendVariantSetting, String> entry : backendSettingSourceFieldNameMap.entrySet())
{
@ -581,6 +588,11 @@ public class QInstanceValidator
}
}
}
if(backendVariantsConfig.getVariantRecordLookupFunction() != null)
{
validateSimpleCodeReference("VariantRecordSupplier in backendVariantsConfig in backend [" + backendName + "]: ", backendVariantsConfig.getVariantRecordLookupFunction(), UnsafeFunction.class, Function.class);
}
}
}
else
@ -1404,7 +1416,7 @@ public class QInstanceValidator
////////////////////////////////////////////////////////////////////////
if(customizerInstance != null && tableCustomizer.getExpectedType() != null)
{
assertObjectCanBeCasted(prefix, tableCustomizer.getExpectedType(), customizerInstance);
assertObjectCanBeCasted(prefix, customizerInstance, tableCustomizer.getExpectedType());
}
}
}
@ -1416,18 +1428,31 @@ public class QInstanceValidator
/*******************************************************************************
** Make sure that a given object can be casted to an expected type.
*******************************************************************************/
private <T> T assertObjectCanBeCasted(String errorPrefix, Class<T> expectedType, Object object)
private void assertObjectCanBeCasted(String errorPrefix, Object object, Class<?>... anyOfExpectedClasses)
{
T castedObject = null;
try
for(Class<?> expectedClass : anyOfExpectedClasses)
{
castedObject = expectedType.cast(object);
try
{
expectedClass.cast(object);
return;
}
catch(ClassCastException e)
{
/////////////////////////////////////
// try next type (if there is one) //
/////////////////////////////////////
}
}
catch(ClassCastException e)
if(anyOfExpectedClasses.length == 1)
{
errors.add(errorPrefix + "CodeReference is not of the expected type: " + expectedType);
errors.add(errorPrefix + "CodeReference is not of the expected type: " + anyOfExpectedClasses[0]);
}
else
{
errors.add(errorPrefix + "CodeReference is not any of the expected types: " + Arrays.stream(anyOfExpectedClasses).map(c -> c.getName()).collect(Collectors.joining(", ")));
}
return castedObject;
}
@ -2171,7 +2196,8 @@ public class QInstanceValidator
/*******************************************************************************
**
*******************************************************************************/
private void validateSimpleCodeReference(String prefix, QCodeReference codeReference, Class<?> expectedClass)
@SafeVarargs
private void validateSimpleCodeReference(String prefix, QCodeReference codeReference, Class<?>... anyOfExpectedClasses)
{
if(!preAssertionsForCodeReference(codeReference, prefix))
{
@ -2199,7 +2225,7 @@ public class QInstanceValidator
////////////////////////////////////////////////////////////////////////
if(classInstance != null)
{
assertObjectCanBeCasted(prefix, expectedClass, classInstance);
assertObjectCanBeCasted(prefix, classInstance, anyOfExpectedClasses);
}
}
}

View File

@ -25,6 +25,7 @@ package com.kingsrook.qqq.backend.core.model.metadata.variants;
import java.util.HashMap;
import java.util.Map;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
/*******************************************************************************
@ -37,13 +38,17 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
** field names in that table that they come from. e.g., a backend may have a
** username attribute, whose value comes from a field named "theUser" in the
** variant options table.
** - an optional code reference to a variantRecordLookupFunction - to customize
** how the variant record is looked up (such as, adding joined or other custom
** fields).
*******************************************************************************/
public class BackendVariantsConfig
{
private String variantTypeKey;
private String optionsTableName;
private QQueryFilter optionsFilter;
private String optionsTableName;
private QQueryFilter optionsFilter;
private QCodeReference variantRecordLookupFunction;
private Map<BackendVariantSetting, String> backendSettingSourceFieldNameMap;
@ -186,4 +191,35 @@ public class BackendVariantsConfig
return (this);
}
/*******************************************************************************
** Getter for variantRecordLookupFunction
*******************************************************************************/
public QCodeReference getVariantRecordLookupFunction()
{
return (this.variantRecordLookupFunction);
}
/*******************************************************************************
** Setter for variantRecordLookupFunction
*******************************************************************************/
public void setVariantRecordLookupFunction(QCodeReference variantRecordLookupFunction)
{
this.variantRecordLookupFunction = variantRecordLookupFunction;
}
/*******************************************************************************
** Fluent setter for variantRecordLookupFunction
*******************************************************************************/
public BackendVariantsConfig withVariantRecordLookupFunction(QCodeReference variantRecordLookupFunction)
{
this.variantRecordLookupFunction = variantRecordLookupFunction;
return (this);
}
}

View File

@ -0,0 +1,106 @@
/*
* 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.variants;
import java.io.Serializable;
import java.util.function.Function;
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
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.get.GetInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput;
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.session.QSession;
import com.kingsrook.qqq.backend.core.utils.lambdas.UnsafeFunction;
/*******************************************************************************
** Utility methods for backends working with Variants.
*******************************************************************************/
public class BackendVariantsUtil
{
/*******************************************************************************
** Get the variant id from the session for the backend.
*******************************************************************************/
public static Serializable getVariantId(QBackendMetaData backendMetaData) throws QException
{
QSession session = QContext.getQSession();
String variantTypeKey = backendMetaData.getBackendVariantsConfig().getVariantTypeKey();
if(session.getBackendVariants() == null || !session.getBackendVariants().containsKey(variantTypeKey))
{
throw (new QException("Could not find Backend Variant information in session under key '" + variantTypeKey + "' for Backend '" + backendMetaData.getName() + "'"));
}
Serializable variantId = session.getBackendVariants().get(variantTypeKey);
return variantId;
}
/*******************************************************************************
** For backends that use variants, look up the variant record (in theory, based
** on an id in the session's backend variants map, then fetched from the backend's
** variant options table.
*******************************************************************************/
@SuppressWarnings("unchecked")
public static QRecord getVariantRecord(QBackendMetaData backendMetaData) throws QException
{
Serializable variantId = getVariantId(backendMetaData);
QRecord record;
if(backendMetaData.getBackendVariantsConfig().getVariantRecordLookupFunction() != null)
{
Object o = QCodeLoader.getAdHoc(Object.class, backendMetaData.getBackendVariantsConfig().getVariantRecordLookupFunction());
if(o instanceof UnsafeFunction<?,?,?> unsafeFunction)
{
record = ((UnsafeFunction<Serializable, QRecord, QException>) unsafeFunction).apply(variantId);
}
else if(o instanceof Function<?,?> function)
{
record = ((Function<Serializable, QRecord>) function).apply(variantId);
}
else
{
throw (new QException("Backend Variant's recordLookupFunction is not of any expected type (should have been caught by instance validation??)"));
}
}
else
{
GetInput getInput = new GetInput();
getInput.setShouldMaskPasswords(false);
getInput.setTableName(backendMetaData.getBackendVariantsConfig().getOptionsTableName());
getInput.setPrimaryKey(variantId);
GetOutput getOutput = new GetAction().execute(getInput);
record = getOutput.getRecord();
}
if(record == null)
{
throw (new QException("Could not find Backend Variant in table " + backendMetaData.getBackendVariantsConfig().getOptionsTableName() + " with id '" + variantId + "'"));
}
return record;
}
}

View File

@ -103,6 +103,7 @@ import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwith
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.LoadViaDeleteStep;
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.StreamedETLWithFrontendProcess;
import com.kingsrook.qqq.backend.core.utils.TestUtils;
import com.kingsrook.qqq.backend.core.utils.lambdas.UnsafeFunction;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@ -246,6 +247,79 @@ public class QInstanceValidatorTest extends BaseTest
.withOptionsTableName(TestUtils.TABLE_NAME_PERSON)
.withBackendSettingSourceFieldNameMap(Map.of(setting, "noSuchField")))),
"Unrecognized fieldName [noSuchField] in backendSettingSourceFieldNameMap");
assertValidationFailureReasons((qInstance) -> qInstance.addBackend(new QBackendMetaData()
.withName("variant")
.withUsesVariants(true)
.withBackendVariantsConfig(new BackendVariantsConfig()
.withVariantTypeKey("myVariant")
.withOptionsTableName(TestUtils.TABLE_NAME_PERSON)
.withVariantRecordLookupFunction(new QCodeReference(CustomizerThatIsNotOfTheRightBaseClass.class))
.withBackendSettingSourceFieldNameMap(Map.of(setting, "no-field-but-okay-custom-supplier"))
)),
"VariantRecordSupplier in backendVariantsConfig in backend [variant]: CodeReference is not any of the expected types: com.kingsrook.qqq.backend.core.utils.lambdas.UnsafeFunction, java.util.function.Function");
assertValidationSuccess((qInstance) -> qInstance.addBackend(new QBackendMetaData()
.withName("variant")
.withUsesVariants(true)
.withBackendVariantsConfig(new BackendVariantsConfig()
.withVariantTypeKey("myVariant")
.withOptionsTableName(TestUtils.TABLE_NAME_PERSON)
.withBackendSettingSourceFieldNameMap(Map.of(setting, "firstName"))
)));
assertValidationSuccess((qInstance) -> qInstance.addBackend(new QBackendMetaData()
.withName("variant")
.withUsesVariants(true)
.withBackendVariantsConfig(new BackendVariantsConfig()
.withVariantTypeKey("myVariant")
.withOptionsTableName(TestUtils.TABLE_NAME_PERSON)
.withVariantRecordLookupFunction(new QCodeReference(VariantRecordFunction.class))
.withBackendSettingSourceFieldNameMap(Map.of(setting, "no-field-but-okay-custom-supplier"))
)));
assertValidationSuccess((qInstance) -> qInstance.addBackend(new QBackendMetaData()
.withName("variant")
.withUsesVariants(true)
.withBackendVariantsConfig(new BackendVariantsConfig()
.withVariantTypeKey("myVariant")
.withOptionsTableName(TestUtils.TABLE_NAME_PERSON)
.withVariantRecordLookupFunction(new QCodeReference(VariantRecordUnsafeFunction.class))
.withBackendSettingSourceFieldNameMap(Map.of(setting, "no-field-but-okay-custom-supplier"))
)));
}
/***************************************************************************
**
***************************************************************************/
public static class VariantRecordFunction implements Function<Serializable, QRecord>
{
/***************************************************************************
**
***************************************************************************/
@Override
public QRecord apply(Serializable serializable)
{
return null;
}
}
/***************************************************************************
**
***************************************************************************/
public static class VariantRecordUnsafeFunction implements UnsafeFunction<Serializable, QRecord, QException>
{
/***************************************************************************
**
***************************************************************************/
@Override
public QRecord apply(Serializable serializable) throws QException
{
return null;
}
}
@ -2437,7 +2511,7 @@ public class QInstanceValidatorTest extends BaseTest
{
int noOfReasons = actualReasons == null ? 0 : actualReasons.size();
assertEquals(expectedReasons.length, noOfReasons, "Expected number of validation failure reasons.\nExpected reasons: " + String.join(",", expectedReasons)
+ "\nActual reasons: " + (noOfReasons > 0 ? String.join("\n", actualReasons) : "--"));
+ "\nActual reasons: " + (noOfReasons > 0 ? String.join("\n", actualReasons) : "--"));
}
for(String reason : expectedReasons)

View File

@ -0,0 +1,100 @@
/*
* 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.variants;
import java.util.Map;
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.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
import com.kingsrook.qqq.backend.core.utils.TestUtils;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
/*******************************************************************************
** Unit test for BackendVariantsUtil
*******************************************************************************/
class BackendVariantsUtilTest extends BaseTest
{
/*******************************************************************************
**
*******************************************************************************/
@Test
void testGetVariantId() throws QException
{
QBackendMetaData myBackend = getBackendMetaData();
assertThatThrownBy(() -> BackendVariantsUtil.getVariantId(myBackend))
.hasMessageContaining("Could not find Backend Variant information in session under key 'yourSelectedShape' for Backend 'TestBackend'");
QContext.getQSession().setBackendVariants(Map.of("yourSelectedShape", 1701));
assertEquals(1701, BackendVariantsUtil.getVariantId(myBackend));
}
/***************************************************************************
**
***************************************************************************/
private static QBackendMetaData getBackendMetaData()
{
QBackendMetaData myBackend = new QBackendMetaData()
.withName("TestBackend")
.withUsesVariants(true)
.withBackendVariantsConfig(new BackendVariantsConfig()
.withOptionsTableName(TestUtils.TABLE_NAME_SHAPE)
.withVariantTypeKey("yourSelectedShape"));
return myBackend;
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testGetVariantRecord() throws QException
{
QBackendMetaData myBackend = getBackendMetaData();
TestUtils.insertDefaultShapes(QContext.getQInstance());
assertThatThrownBy(() -> BackendVariantsUtil.getVariantRecord(myBackend))
.hasMessageContaining("Could not find Backend Variant information in session under key 'yourSelectedShape' for Backend 'TestBackend'");
QContext.getQSession().setBackendVariants(Map.of("yourSelectedShape", 1701));
assertThatThrownBy(() -> BackendVariantsUtil.getVariantRecord(myBackend))
.hasMessageContaining("Could not find Backend Variant in table shape with id '1701'");
QContext.getQSession().setBackendVariants(Map.of("yourSelectedShape", 1));
QRecord variantRecord = BackendVariantsUtil.getVariantRecord(myBackend);
assertEquals(1, variantRecord.getValueInteger("id"));
assertNotNull(variantRecord.getValue("name"));
}
}