fix(custom-fields): reject over-long STRING values and reload help after publication #61
No files matched your search
@@ -27,6 +27,8 @@ All notable changes are documented here, following [Keep a Changelog](https://ke
|
||||
|
||||
### Fixed
|
||||
|
||||
- Bounded STRING custom fields carry QQQ's native `ValueTooLongBehavior.ERROR`, so a write longer than `maxLength` is rejected with a field error and the stored value is preserved, instead of passing through unchecked (the QQQ default `PASS_THROUGH`).
|
||||
- Persisted help content is reloaded after each committed runtime metadata refresh, so sections and fields created by publishing a custom-field definition receive their stored `helpContent` rows without a restart.
|
||||
- QBit metadata reports the Maven artifact version from a filtered resource, including release versions.
|
||||
- `presence.changed` events and presence snapshots always carry `viewers`, sending `[]` once the last viewer leaves, as the schema requires (#52).
|
||||
|
||||
|
||||
@@ -29,10 +29,12 @@ import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizerInterfa
|
||||
import com.kingsrook.qqq.backend.core.context.CapturedContext;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.instances.QInstanceHelpContentManager;
|
||||
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.QInstance;
|
||||
|
||||
/*******************************************************************************
|
||||
** Publish committed policy changes through the core overlay.
|
||||
@@ -77,7 +79,7 @@ public class FeatureSettingsCustomizer implements TableCustomizerInterface
|
||||
var instance = QContext.getQInstance();
|
||||
if(transaction == null)
|
||||
{
|
||||
RuntimeMetaDataOverlay.refresh(instance);
|
||||
refreshMetadata(instance);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -86,7 +88,7 @@ public class FeatureSettingsCustomizer implements TableCustomizerInterface
|
||||
{
|
||||
try
|
||||
{
|
||||
QContext.withTemporaryContext(context, () -> RuntimeMetaDataOverlay.refresh(instance));
|
||||
QContext.withTemporaryContext(context, () -> refreshMetadata(instance));
|
||||
}
|
||||
catch(QException e)
|
||||
{
|
||||
@@ -95,4 +97,13 @@ public class FeatureSettingsCustomizer implements TableCustomizerInterface
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Bind persisted help after the overlay creates its dynamic fields and sections.
|
||||
*******************************************************************************/
|
||||
private static void refreshMetadata(QInstance instance) throws QException
|
||||
{
|
||||
RuntimeMetaDataOverlay.refresh(instance);
|
||||
QInstanceHelpContentManager.reloadHelpContent(instance);
|
||||
}
|
||||
}
|
||||
@@ -43,6 +43,7 @@ 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.fields.QVirtualFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueRangeBehavior;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.help.QHelpContent;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSourceType;
|
||||
@@ -123,6 +124,10 @@ public class CustomFieldsProvider implements RuntimeMetaDataProviderInterface
|
||||
.withCriteriaExpander(new QCodeReference(FieldCriteriaExpander.class));
|
||||
field.withLabel(definition.getValueString("label")).withIsRequired(json && Boolean.TRUE.equals(definition.getValueBoolean("required")))
|
||||
.withMaxLength(definition.getValueInteger("maxLength"));
|
||||
if(type == QFieldType.STRING && definition.getValueInteger("maxLength") != null)
|
||||
{
|
||||
field.withBehavior(ValueTooLongBehavior.ERROR);
|
||||
}
|
||||
if(definition.getValue("defaultValue") != null && json)
|
||||
{
|
||||
field.withDefaultValue(com.kingsrook.qqq.backend.core.utils.ValueUtils.getValueAsFieldType(type, definition.getValue("defaultValue")));
|
||||
|
||||
@@ -37,6 +37,7 @@ import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.DeleteAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.instances.QInstanceEnricher;
|
||||
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.actions.tables.delete.DeleteInput;
|
||||
@@ -44,9 +45,12 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.helpcontent.HelpContentMetaDataProvider;
|
||||
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.fields.JsonPathStorage;
|
||||
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.module.rdbms.model.metadata.RDBMSBackendMetaData;
|
||||
import com.kingsrook.qbits.collaboration.app.CollaborationHome;
|
||||
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.TableData;
|
||||
@@ -59,6 +63,48 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
*******************************************************************************/
|
||||
public class CustomFieldsTest extends PersonalServiceFixture
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Publication binds stored guidance to sections that did not exist at startup.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void publishedSectionReceivesStoredHelp() throws Exception
|
||||
{
|
||||
new HelpContentMetaDataProvider().defineAll(instance, "memory", table ->
|
||||
table.withPermissionRules(new QPermissionRules()
|
||||
.withLevel(PermissionLevel.NOT_PROTECTED)));
|
||||
new QInstanceEnricher(instance).enrich();
|
||||
admin();
|
||||
var help = new InsertAction().execute(new InsertInput("helpContent").withRecords(List.of(new QRecord()
|
||||
.withValue("key", "table:document;section:customFields").withValue("role", "ALL_SCREENS")
|
||||
.withValue("format", "MARKDOWN").withValue("content", "Synthetic dynamic section guidance.")))).getRecords().getFirst();
|
||||
assertThat(help.getErrors()).isNullOrEmpty();
|
||||
QRecord definition = draft("guided", "STRING");
|
||||
FieldDefinitions.apply(definition.getValue("id"), "Publish");
|
||||
assertThat(instance.getTable("document").getSection("customFields").getHelpContents())
|
||||
.anySatisfy(content -> assertThat(content.getContent()).contains("Synthetic dynamic section guidance."));
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** A configured string bound rejects writes without replacing persisted values.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void publishedStringRejectsValuesBeyondItsMaximumLength() throws Exception
|
||||
{
|
||||
admin();
|
||||
QRecord definition = draft("note", "STRING");
|
||||
new UpdateAction().execute(new UpdateInput(CollaborationTables.FIELD_DEFINITION).withRecords(List.of(
|
||||
new QRecord().withValue("id", definition.getValue("id")).withValue("maxLength", 5))));
|
||||
FieldDefinitions.apply(definition.getValue("id"), "Publish");
|
||||
login("alice");
|
||||
var valid = new UpdateAction().execute(new UpdateInput("document").withRecords(List.of(
|
||||
new QRecord().withValue("id", 1).withValue("cf_note", "valid")))).getRecords().getFirst();
|
||||
assertThat(valid.getErrors()).isNullOrEmpty();
|
||||
var invalid = new UpdateAction().execute(new UpdateInput("document").withRecords(List.of(
|
||||
new QRecord().withValue("id", 1).withValue("cf_note", "too long")))).getRecords().getFirst();
|
||||
assertThat(invalid.getErrors()).isNotEmpty();
|
||||
assertThat(query("cf_note", QCriteriaOperator.EQUALS, "valid")).hasSize(1);
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Refreshing a SELECT must replace its generated vocabulary and allow later fields.
|
||||
*******************************************************************************/
|
||||
|
||||
Reference in new issue
Block a user