From 49ebea22a177043c6f7cbb40de402a5b9b00dc37 Mon Sep 17 00:00:00 2001 From: James Maes Date: Wed, 14 Oct 2026 15:54:23 -0500 Subject: [PATCH] fix(config): never serialize the alerts runtime (child lists failed to render) Record-view child lists, such as a notification's Recipients, failed with "Error in JSON Serialization". ChildRecordListData carries the child QTableMetaData, whose SourceQBitAware getSourceQBitConfig() hands Jackson the AlertsQBitConfig; its getRuntime() returns an AlertsRuntime that points back at the config, so serialization recursed to Jackson's 1000-level nesting limit. getRuntime() and getAudienceResolver() are now @JsonIgnore, as in the cms, quick-search and sql-workbench configs. ChildListWidgetsJsonTest renders the Recipients list with rows and every child-list widget, and serializes each one; all three tests fail without the fix. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 3 + .../qbits/alerts/AlertsQBitConfig.java | 7 +- .../metadata/ChildListWidgetsJsonTest.java | 128 ++++++++++++++++++ 3 files changed, 137 insertions(+), 1 deletion(-) create mode 100644 src/test/java/com/kingsrook/qbits/alerts/metadata/ChildListWidgetsJsonTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 2943790..94329d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/src/main/java/com/kingsrook/qbits/alerts/AlertsQBitConfig.java b/src/main/java/com/kingsrook/qbits/alerts/AlertsQBitConfig.java index 37f1e95..6e3d02c 100644 --- a/src/main/java/com/kingsrook/qbits/alerts/AlertsQBitConfig.java +++ b/src/main/java/com/kingsrook/qbits/alerts/AlertsQBitConfig.java @@ -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); diff --git a/src/test/java/com/kingsrook/qbits/alerts/metadata/ChildListWidgetsJsonTest.java b/src/test/java/com/kingsrook/qbits/alerts/metadata/ChildListWidgetsJsonTest.java new file mode 100644 index 0000000..1ce37f5 --- /dev/null +++ b/src/test/java/com/kingsrook/qbits/alerts/metadata/ChildListWidgetsJsonTest.java @@ -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 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 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()); + } +} -- 2.54.0