From 0c406e9a4b9e73234639d11a78acd5170a33f1bc Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Thu, 29 Jan 2026 16:11:01 -0600 Subject: [PATCH] feat: always keep fields that are part of any record locks on a table, plus any that are marked by the application via supplemental meta-data as always-include --- ...stomizableTableViewsTablePersonalizer.java | 146 +++++++++++++---- .../CustomizableTableViewsFieldMetaData.java | 89 ++++++++++ ...izableTableViewsTablePersonalizerTest.java | 154 ++++++++++++++++++ 3 files changed, 361 insertions(+), 28 deletions(-) create mode 100644 src/main/java/com/kingsrook/qbits/customizabletableviews/model/CustomizableTableViewsFieldMetaData.java diff --git a/src/main/java/com/kingsrook/qbits/customizabletableviews/logic/CustomizableTableViewsTablePersonalizer.java b/src/main/java/com/kingsrook/qbits/customizabletableviews/logic/CustomizableTableViewsTablePersonalizer.java index 613da5d..f46296d 100644 --- a/src/main/java/com/kingsrook/qbits/customizabletableviews/logic/CustomizableTableViewsTablePersonalizer.java +++ b/src/main/java/com/kingsrook/qbits/customizabletableviews/logic/CustomizableTableViewsTablePersonalizer.java @@ -37,6 +37,7 @@ import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import com.kingsrook.qbits.customizabletableviews.model.CustomizableTable; +import com.kingsrook.qbits.customizabletableviews.model.CustomizableTableViewsFieldMetaData; import com.kingsrook.qbits.customizabletableviews.model.FieldAccessLevel; import com.kingsrook.qbits.customizabletableviews.model.TableView; import com.kingsrook.qbits.customizabletableviews.model.TableViewField; @@ -70,6 +71,9 @@ import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; import com.kingsrook.qqq.backend.core.model.metadata.fields.DynamicDefaultValueBehavior; import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldAndJoinTable; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QSupplementalFieldMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.security.MultiRecordSecurityLock; +import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock; import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.model.session.QUser; @@ -262,7 +266,7 @@ public class CustomizableTableViewsTablePersonalizer implements TableMetaDataPer /*************************************************************************** * ***************************************************************************/ - private Set getFieldsToKeepFromJoinTable(String joinTableName, AbstractTableActionInput tableActionInput, Map> getFieldsToKeepFromJoinTableCache) + protected Set getFieldsToKeepFromJoinTable(String joinTableName, AbstractTableActionInput tableActionInput, Map> getFieldsToKeepFromJoinTableCache) { return (getFieldsToKeepFromJoinTableCache.computeIfAbsent(joinTableName, k -> { @@ -298,7 +302,7 @@ public class CustomizableTableViewsTablePersonalizer implements TableMetaDataPer /*************************************************************************** * ***************************************************************************/ - private static Map getFieldsToKeepForTable(TableView tableView, QTableMetaData cloneTable, AbstractTableActionInput tableActionInput) + protected Map getFieldsToKeepForTable(TableView tableView, QTableMetaData cloneTable, AbstractTableActionInput tableActionInput) { Map cloneFields = cloneTable.getFields(); if(cloneFields == null) @@ -339,43 +343,129 @@ public class CustomizableTableViewsTablePersonalizer implements TableMetaDataPer ///////////////////////////////////////////// // next look for ones that always get kept // ///////////////////////////////////////////// - boolean isInsertOrUpdate = (tableActionInput instanceof InsertInput) || (tableActionInput instanceof UpdateInput); for(QFieldMetaData field : cloneTable.getFields().values()) { String fieldName = field.getName(); - if(fieldsToKeep.containsKey(fieldName)) + if(!fieldsToKeep.containsKey(fieldName)) { - continue; - } - - DynamicDefaultValueBehavior x = null; - boolean keep = false; - if(field.getIsRequired() || Objects.equals(fieldName, cloneTable.getPrimaryKeyField())) - { - /////////////////////////////////////////////////////////// - // always keep the primary key field and required fields // - /////////////////////////////////////////////////////////// - keep = true; - } - else if(isInsertOrUpdate && field.getBehaviorOnlyIfSet(DynamicDefaultValueBehavior.class) != null) - { - ////////////////////////////////////////////////////////////////////////////////////////////////////// - // if this is for an insert or update, then keep any fields that have a DynamicDefaultValueBehavior // - // e.g., createDates, modifyDates, things that capture userId // - ////////////////////////////////////////////////////////////////////////////////////////////////////// - keep = true; - } - - if(keep) - { - fieldsToKeep.put(fieldName, cloneFields.get(fieldName)); + if(shouldKeepFieldDueToRules(cloneTable, field, tableActionInput)) + { + fieldsToKeep.put(fieldName, cloneFields.get(fieldName)); + } } } + return fieldsToKeep; } + /*************************************************************************** + * check if any system-defined rules make it so that a field should always + * be kept. + * + *

built-in rules are:

+ *
    + *
  • field is required
  • + *
  • field is primary key
  • + *
  • field is part of table's security locks
  • + *
  • there's supplemental metadata from this qbit that says to keep the field
  • + *
  • this is an insert or update, and there's a {@link DynamicDefaultValueBehavior}
  • + *
+ * + * also - an application could override this method in a subclass to add + * additional rules. + ***************************************************************************/ + protected boolean shouldKeepFieldDueToRules(QTableMetaData cloneTable, QFieldMetaData field, AbstractTableActionInput tableActionInput) + { + if(field.getIsRequired() || Objects.equals(field.getName(), cloneTable.getPrimaryKeyField())) + { + /////////////////////////////////////////////////////////// + // always keep the primary key field and required fields // + /////////////////////////////////////////////////////////// + return (true); + } + + if(doRecordSecurityLocksContainField(cloneTable.getRecordSecurityLocks(), field)) + { + ///////////////////////////////////////////////////////////// + // keep fields that are part of the table's security locks // + ///////////////////////////////////////////////////////////// + return (true); + } + + if(doesSupplementalMetaDataSayToKeepField(field)) + { + ////////////////////////////////////////////////////////// + // keep a field if it's marked as being always-included // + ////////////////////////////////////////////////////////// + return (true); + } + + boolean isInsertOrUpdate = (tableActionInput instanceof InsertInput) || (tableActionInput instanceof UpdateInput); + if(isInsertOrUpdate && field.getBehaviorOnlyIfSet(DynamicDefaultValueBehavior.class) != null) + { + ////////////////////////////////////////////////////////////////////////////////////////////////////// + // if this is for an insert or update, then keep any fields that have a DynamicDefaultValueBehavior // + // e.g., createDates, modifyDates, things that capture userId // + ////////////////////////////////////////////////////////////////////////////////////////////////////// + return (true); + } + + //////////////////////////////////////////// + // by default, no rule matched, so say no // + //////////////////////////////////////////// + return (false); + } + + + + /*************************************************************************** + * Check if the supplemental meta data from this qbit for a field says + * that the field should always be kept. + ***************************************************************************/ + protected boolean doesSupplementalMetaDataSayToKeepField(QFieldMetaData field) + { + QSupplementalFieldMetaData supplementalMetaData = field.getSupplementalMetaData(CustomizableTableViewsFieldMetaData.TYPE); + if(supplementalMetaData instanceof CustomizableTableViewsFieldMetaData customizableTableViewsFieldMetaData) + { + if(CustomizableTableViewsFieldMetaData.Rule.ALWAYS_KEEP_FIELD.equals(customizableTableViewsFieldMetaData.getRule())) + { + return (true); + } + } + + return (false); + } + + + + /*************************************************************************** + * Check if a list of record security locks include a field, recursively + * processing {@link MultiRecordSecurityLock}s + ***************************************************************************/ + protected boolean doRecordSecurityLocksContainField(List locks, QFieldMetaData field) + { + for(RecordSecurityLock recordSecurityLock : CollectionUtils.nonNullList(locks)) + { + if(recordSecurityLock instanceof MultiRecordSecurityLock multiRecordSecurityLock) + { + if(doRecordSecurityLocksContainField(multiRecordSecurityLock.getLocks(), field)) + { + return (true); + } + } + else if(field.getName().equals(recordSecurityLock.getFieldName())) + { + return true; + } + } + + return false; + } + + + /*************************************************************************** * the assumption is, that this method is called AFTER we've identified that * a table is customizable (and customizaztion is active) - so - if we fail diff --git a/src/main/java/com/kingsrook/qbits/customizabletableviews/model/CustomizableTableViewsFieldMetaData.java b/src/main/java/com/kingsrook/qbits/customizabletableviews/model/CustomizableTableViewsFieldMetaData.java new file mode 100644 index 0000000..a3163ed --- /dev/null +++ b/src/main/java/com/kingsrook/qbits/customizabletableviews/model/CustomizableTableViewsFieldMetaData.java @@ -0,0 +1,89 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2026. 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.qbits.customizabletableviews.model; + + +import com.kingsrook.qqq.backend.core.model.metadata.fields.QSupplementalFieldMetaData; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class CustomizableTableViewsFieldMetaData extends QSupplementalFieldMetaData +{ + public static final String TYPE = CustomizableTableViewsFieldMetaData.class.getName(); + + private Rule rule; + + public enum Rule + { + ALWAYS_KEEP_FIELD + } + + /*************************************************************************** + * + ***************************************************************************/ + @Override + public String getType() + { + return TYPE; + } + + + /******************************************************************************* + * Getter for rule + * @see #withRule(Rule) + *******************************************************************************/ + public Rule getRule() + { + return (this.rule); + } + + + + /******************************************************************************* + * Setter for rule + * @see #withRule(Rule) + *******************************************************************************/ + public void setRule(Rule rule) + { + this.rule = rule; + } + + + + /******************************************************************************* + * Fluent setter for rule + * + * @param rule + * TODO document this property + * + * @return this + *******************************************************************************/ + public CustomizableTableViewsFieldMetaData withRule(Rule rule) + { + this.rule = rule; + return (this); + } + + +} diff --git a/src/test/java/com/kingsrook/qbits/customizabletableviews/logic/CustomizableTableViewsTablePersonalizerTest.java b/src/test/java/com/kingsrook/qbits/customizabletableviews/logic/CustomizableTableViewsTablePersonalizerTest.java index 37f4fda..4d081f4 100644 --- a/src/test/java/com/kingsrook/qbits/customizabletableviews/logic/CustomizableTableViewsTablePersonalizerTest.java +++ b/src/test/java/com/kingsrook/qbits/customizabletableviews/logic/CustomizableTableViewsTablePersonalizerTest.java @@ -34,6 +34,7 @@ import java.util.stream.Collectors; import com.kingsrook.qbits.customizabletableviews.BaseTest; import com.kingsrook.qbits.customizabletableviews.QFieldMetaDataAssert; import com.kingsrook.qbits.customizabletableviews.model.CustomizableTable; +import com.kingsrook.qbits.customizabletableviews.model.CustomizableTableViewsFieldMetaData; import com.kingsrook.qbits.customizabletableviews.model.FieldAccessLevel; import com.kingsrook.qbits.customizabletableviews.model.TableView; import com.kingsrook.qbits.customizabletableviews.model.TableViewField; @@ -61,6 +62,8 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn; import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinType; import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.security.MultiRecordSecurityLock; +import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.SectionFactory; import com.kingsrook.qqq.backend.core.model.session.QUser; @@ -548,6 +551,157 @@ class CustomizableTableViewsTablePersonalizerTest extends BaseTest + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testFieldsInSecurityLocks() + { + /////////////////////////// + // empty with empty case // + /////////////////////////// + QTableMetaData baseTable = new QTableMetaData() + .withPrimaryKeyField("id") + .withField(new QFieldMetaData("id", QFieldType.STRING).withIsEditable(false)) + .withField(new QFieldMetaData("a", QFieldType.STRING)) + .withField(new QFieldMetaData("b", QFieldType.STRING)) + .withField(new QFieldMetaData("c", QFieldType.STRING)) + .withField(new QFieldMetaData("d", QFieldType.STRING)); + + ////////////////////////////////////////////////////// + // with no security field, only id should come back // + ////////////////////////////////////////////////////// + { + QTableMetaData tableMetaData = new CustomizableTableViewsTablePersonalizer().applyViewToTable( + new TableView().withFields(List.of()), + baseTable.clone(), new QueryInput()); + + assertEquals(1, tableMetaData.getFields().size()); + QFieldMetaDataAssert.assertThat(tableMetaData.getField("id")).isNotNull(); + } + + /////////////////////////////////////////////////////// + // with a simple security field, it should come back // + /////////////////////////////////////////////////////// + baseTable.setRecordSecurityLocks(List.of( + new RecordSecurityLock().withFieldName("a") + )); + { + QTableMetaData tableMetaData = new CustomizableTableViewsTablePersonalizer().applyViewToTable( + new TableView().withFields(List.of()), + baseTable.clone(), new QueryInput()); + + assertEquals(2, tableMetaData.getFields().size()); + QFieldMetaDataAssert.assertThat(tableMetaData.getField("id")).isNotNull(); + QFieldMetaDataAssert.assertThat(tableMetaData.getField("a")).isNotNull(); + } + + ///////////////////////////////////////////////////////////// + // with a multi-lock, all included fields should come back // + ///////////////////////////////////////////////////////////// + baseTable.setRecordSecurityLocks(List.of( + new MultiRecordSecurityLock() + .withOperator(MultiRecordSecurityLock.BooleanOperator.OR) + .withLocks(List.of( + new RecordSecurityLock().withFieldName("b"), + new MultiRecordSecurityLock() + .withOperator(MultiRecordSecurityLock.BooleanOperator.AND) + .withLocks(List.of( + new RecordSecurityLock().withFieldName("c"), + new RecordSecurityLock().withFieldName("d") + )) + )) + )); + { + QTableMetaData tableMetaData = new CustomizableTableViewsTablePersonalizer().applyViewToTable( + new TableView().withFields(List.of()), + baseTable.clone(), new QueryInput()); + + assertEquals(4, tableMetaData.getFields().size()); + QFieldMetaDataAssert.assertThat(tableMetaData.getField("id")).isNotNull(); + QFieldMetaDataAssert.assertThat(tableMetaData.getField("b")).isNotNull(); + QFieldMetaDataAssert.assertThat(tableMetaData.getField("c")).isNotNull(); + QFieldMetaDataAssert.assertThat(tableMetaData.getField("d")).isNotNull(); + } + + /////////////////////////////////////////////////////////// + // make sure lock field from join table doesn't break us // + /////////////////////////////////////////////////////////// + baseTable.setRecordSecurityLocks(List.of( + new RecordSecurityLock().withFieldName("join.a") + )); + { + QTableMetaData tableMetaData = new CustomizableTableViewsTablePersonalizer().applyViewToTable( + new TableView().withFields(List.of()), + baseTable.clone(), new QueryInput()); + + assertEquals(1, tableMetaData.getFields().size()); + QFieldMetaDataAssert.assertThat(tableMetaData.getField("id")).isNotNull(); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testFieldsWithSupplementalMetaData() + { + /////////////////////////// + // empty with empty case // + /////////////////////////// + QTableMetaData baseTable = new QTableMetaData() + .withPrimaryKeyField("id") + .withField(new QFieldMetaData("id", QFieldType.STRING).withIsEditable(false)) + .withField(new QFieldMetaData("a", QFieldType.STRING)) + .withField(new QFieldMetaData("b", QFieldType.STRING)) + .withField(new QFieldMetaData("c", QFieldType.STRING)) + .withField(new QFieldMetaData("d", QFieldType.STRING)); + + ////////////////////////////////// + // base case, should just be id // + ////////////////////////////////// + { + QTableMetaData tableMetaData = new CustomizableTableViewsTablePersonalizer().applyViewToTable( + new TableView().withFields(List.of()), + baseTable.clone(), new QueryInput()); + + assertEquals(1, tableMetaData.getFields().size()); + QFieldMetaDataAssert.assertThat(tableMetaData.getField("id")).isNotNull(); + } + + /////////////////////////////////////////////////////////////////////// + // supplemental meta data, but without a rule should not be included // + /////////////////////////////////////////////////////////////////////// + baseTable.getField("a").withSupplementalMetaData(new CustomizableTableViewsFieldMetaData()); + { + QTableMetaData tableMetaData = new CustomizableTableViewsTablePersonalizer().applyViewToTable( + new TableView().withFields(List.of()), + baseTable.clone(), new QueryInput()); + + assertEquals(1, tableMetaData.getFields().size()); + QFieldMetaDataAssert.assertThat(tableMetaData.getField("id")).isNotNull(); + } + + ////////////////////////////////////////////////////////////////////// + // supplemental meta data, with the include rule should be included // + ////////////////////////////////////////////////////////////////////// + baseTable.getField("b").withSupplementalMetaData(new CustomizableTableViewsFieldMetaData() + .withRule(CustomizableTableViewsFieldMetaData.Rule.ALWAYS_KEEP_FIELD)); + { + QTableMetaData tableMetaData = new CustomizableTableViewsTablePersonalizer().applyViewToTable( + new TableView().withFields(List.of()), + baseTable.clone(), new QueryInput()); + + assertEquals(2, tableMetaData.getFields().size()); + QFieldMetaDataAssert.assertThat(tableMetaData.getField("id")).isNotNull(); + QFieldMetaDataAssert.assertThat(tableMetaData.getField("b")).isNotNull(); + } + } + + + /******************************************************************************* ** *******************************************************************************/ -- 2.54.0