diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java index fa4d1a23..a214ee6b 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java @@ -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 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 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 assertObjectCanBeCasted(String errorPrefix, Class 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); } } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/variants/BackendVariantsConfig.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/variants/BackendVariantsConfig.java index 20237e7a..290c099f 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/variants/BackendVariantsConfig.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/variants/BackendVariantsConfig.java @@ -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 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); + } + + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/variants/BackendVariantsUtil.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/variants/BackendVariantsUtil.java new file mode 100644 index 00000000..a94fbc4e --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/variants/BackendVariantsUtil.java @@ -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 . + */ + +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) unsafeFunction).apply(variantId); + } + else if(o instanceof Function function) + { + record = ((Function) 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; + } +} diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java index 9ce5d9f6..8419b05a 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java @@ -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 + { + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public QRecord apply(Serializable serializable) + { + return null; + } + } + + + /*************************************************************************** + ** + ***************************************************************************/ + public static class VariantRecordUnsafeFunction implements UnsafeFunction + { + /*************************************************************************** + ** + ***************************************************************************/ + @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) diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/metadata/variants/BackendVariantsUtilTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/metadata/variants/BackendVariantsUtilTest.java new file mode 100644 index 00000000..5957ee61 --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/metadata/variants/BackendVariantsUtilTest.java @@ -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 . + */ + +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")); + } + +} \ No newline at end of file