fix(config): never serialize the alerts runtime (child lists failed to render) #34

Merged
KofTwentyTwo merged 1 commits from feature/recipients-widget-json into develop 2026-10-14 21:10:02 +00:00
3 changed files with 137 additions and 1 deletions

No files matched your search

+3
View File
@@ -4,6 +4,9 @@ All notable changes to this project are documented here. The format follows Keep
## [0.1.0] - Unreleased
### Fixed
- Record-view child lists (e.g. a notification's Recipients) failed with "Error in JSON Serialization": the child table's meta-data includes the qbit config, and `AlertsQBitConfig.getRuntime()` points at a runtime that points back at the config, so Jackson recursed to its 1000-level nesting limit. `getRuntime()` and `getAudienceResolver()` are now `@JsonIgnore`, as in the other qbits' configs. A regression test renders and serializes every child-list widget.
### Changed
- Keep reminders and escalations immediate for digest subscribers while applying pause and quiet hours to their push channels. Preserve team audience resolution, mute scopes, both scheduled jobs and changesets 21-27.
- Apply user pause and local quiet hours to personal push channels (email, email lists and other non-IN_APP channels), including team members, with logged suppression reasons and a configurable quiet-hours severity bypass. The in-app bell remains unaffected, including digest delivery. Channel subscriptions are unaffected.
@@ -19,6 +19,7 @@ package com.kingsrook.qbits.alerts;
import java.util.ArrayList;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.kingsrook.qbits.alerts.model.AlertsAlert;
import com.kingsrook.qbits.alerts.model.AlertsChannel;
import com.kingsrook.qbits.alerts.model.AlertsDelivery;
@@ -118,6 +119,7 @@ public class AlertsQBitConfig implements QBitConfig
/*******************************************************************************
** Optional host resolver for ROLE and IDP_GROUP team members.
*******************************************************************************/
@JsonIgnore
public AlertAudienceResolver getAudienceResolver()
{
return (audienceResolver);
@@ -673,8 +675,11 @@ public class AlertsQBitConfig implements QBitConfig
/*******************************************************************************
** Getter for runtime (built by the producer)
** Getter for runtime (built by the producer). Never serialized: the runtime
** points back at this config, and QQQ meta-data JSON (e.g. a child-list
** widget's table meta-data) can include the qbit config.
*******************************************************************************/
@JsonIgnore
public AlertsRuntime getRuntime()
{
return (this.runtime);
@@ -0,0 +1,128 @@
/*
* Copyright 2026 Kingsrook, LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qbits.alerts.metadata;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.kingsrook.qbits.alerts.BaseAlertsTest;
import com.kingsrook.qbits.alerts.model.AlertsNotification;
import com.kingsrook.qqq.backend.core.actions.dashboard.RenderWidgetAction;
import com.kingsrook.qqq.backend.core.actions.dashboard.widgets.ChildRecordListRenderer;
import com.kingsrook.qqq.backend.core.alerts.NotificationAudience;
import com.kingsrook.qqq.backend.core.alerts.NotificationCategory;
import com.kingsrook.qqq.backend.core.alerts.NotificationSeverity;
import com.kingsrook.qqq.backend.core.alerts.NotificationSignal;
import com.kingsrook.qqq.backend.core.alerts.QAlerts;
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.ChildRecordListData;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.QWidgetData;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaDataInterface;
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/*******************************************************************************
** The record-view child lists serialize to JSON (the payload the record page
** fetches). The child table's meta-data carries the qbit config, whose
** runtime points back at the config: unless that back-reference is ignored,
** Jackson recurses until its nesting limit ("Error in JSON Serialization").
*******************************************************************************/
class ChildListWidgetsJsonTest extends BaseAlertsTest
{
/*******************************************************************************
** The qbit config itself serializes, without its runtime.
*******************************************************************************/
@Test
void testConfigSerializesWithoutRuntime()
{
assertThat(config.getRuntime()).isNotNull();
assertThat(JsonUtils.toJson(config)).doesNotContain("\"runtime\"").doesNotContain("\"audienceResolver\"");
}
/*******************************************************************************
** A notification's Recipients list, with recipient rows (the kof22 case).
*******************************************************************************/
@Test
void testRecipientsChildListSerializes() throws Exception
{
QAlerts.notify(new NotificationSignal("Run backup now finished with errors").withSeverity(NotificationSeverity.ERROR)
.withCategory(NotificationCategory.JOB).withAudience(NotificationAudience.users(VIEWER, ADMIN)));
QRecord notification = runtime.asSystem(() -> all(AlertsNotification.TABLE_NAME)).get(0);
loginAs(ADMIN);
QWidgetMetaDataInterface widget = qInstance.getWidget(AlertsTableMetaDataHelper.JOIN_NOTIFICATION_RECIPIENT);
assertThat(widget).isNotNull();
ChildRecordListData data = (ChildRecordListData) render(widget, notification.getValue("id"));
assertThat(data.getQueryOutput().getRecords()).hasSize(2);
String json = JsonUtils.toJson(data);
assertThat(json).contains("\"totalRows\":2").doesNotContain("\"runtime\"");
}
/*******************************************************************************
** Every child list in the qbit (rendered without a parent, as on an insert
** screen, which still sends the child table's meta-data).
*******************************************************************************/
@Test
void testEveryChildListSerializes() throws Exception
{
loginAs(ADMIN);
List<String> rendered = new ArrayList<>();
for(QWidgetMetaDataInterface widget : qInstance.getWidgets().values())
{
if(widget.getCodeReference() != null && ChildRecordListRenderer.class.getName().equals(widget.getCodeReference().getName()))
{
String json = JsonUtils.toJson(render(widget, null));
assertThat(json).as(widget.getName()).contains("\"childTableMetaData\"").doesNotContain("\"runtime\"");
rendered.add(widget.getName());
}
}
assertThat(rendered).contains(AlertsTableMetaDataHelper.JOIN_NOTIFICATION_RECIPIENT).hasSizeGreaterThan(5);
}
/*******************************************************************************
** Render a child-list widget as the record page does (its default values as
** query params, plus the parent id when there is one).
*******************************************************************************/
private QWidgetData render(QWidgetMetaDataInterface widget, Serializable id) throws Exception
{
Map<String, String> params = new HashMap<>();
widget.getDefaultValues().forEach((key, value) -> params.put(key, value == null ? null : String.valueOf(value)));
if(id != null)
{
params.put("id", String.valueOf(id));
}
RenderWidgetInput input = new RenderWidgetInput().withWidgetMetaData(widget);
input.setQueryParams(params);
return (new RenderWidgetAction().execute(input).getWidgetData());
}
}