feat: always keep fields that are part of any record locks on a table… #3

Merged
darinkelkhoff merged 1 commits from feature/keep-record-lock-and-meta-data-marked-fields into develop 2026-03-09 13:42:49 +00:00
3 changed files with 361 additions and 28 deletions

No files matched your search

@@ -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<String> getFieldsToKeepFromJoinTable(String joinTableName, AbstractTableActionInput tableActionInput, Map<String, Set<String>> getFieldsToKeepFromJoinTableCache)
protected Set<String> getFieldsToKeepFromJoinTable(String joinTableName, AbstractTableActionInput tableActionInput, Map<String, Set<String>> getFieldsToKeepFromJoinTableCache)
{
return (getFieldsToKeepFromJoinTableCache.computeIfAbsent(joinTableName, k ->
{
@@ -298,7 +302,7 @@ public class CustomizableTableViewsTablePersonalizer implements TableMetaDataPer
/***************************************************************************
*
***************************************************************************/
private static Map<String, QFieldMetaData> getFieldsToKeepForTable(TableView tableView, QTableMetaData cloneTable, AbstractTableActionInput tableActionInput)
protected Map<String, QFieldMetaData> getFieldsToKeepForTable(TableView tableView, QTableMetaData cloneTable, AbstractTableActionInput tableActionInput)
{
Map<String, QFieldMetaData> 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.
*
* <p>built-in rules are:</p>
* <ul>
* <li>field is required</li>
* <li>field is primary key</li>
* <li>field is part of table's security locks</li>
* <li>there's supplemental metadata from this qbit that says to keep the field</li>
* <li>this is an insert or update, and there's a {@link DynamicDefaultValueBehavior}</li>
* </ul>
*
* 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<RecordSecurityLock> 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
@@ -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 <https://www.gnu.org/licenses/>.
*/
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);
}
}
@@ -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();
}
}
/*******************************************************************************
**
*******************************************************************************/