feat(notification-outbox): additive NotificationId query filter for ?correlationId= deep links
Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29
This commit is contained in:
+7
-1
@@ -6,6 +6,11 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Messages.Notification;
|
|||||||
/// Outbox UI -> Central: paginated, filtered query over the notification outbox.
|
/// Outbox UI -> Central: paginated, filtered query over the notification outbox.
|
||||||
/// All filter fields are optional; <see cref="StuckOnly"/> restricts results to stuck notifications.
|
/// All filter fields are optional; <see cref="StuckOnly"/> restricts results to stuck notifications.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="NotificationIdFilter">
|
||||||
|
/// Exact match on a single notification — the <c>?correlationId=</c> deep link from the
|
||||||
|
/// Flow Debugger / Audit Log. Additive; <c>null</c> means "no filter" (legacy senders
|
||||||
|
/// compile unchanged).
|
||||||
|
/// </param>
|
||||||
public record NotificationOutboxQueryRequest(
|
public record NotificationOutboxQueryRequest(
|
||||||
string CorrelationId,
|
string CorrelationId,
|
||||||
string? StatusFilter,
|
string? StatusFilter,
|
||||||
@@ -18,7 +23,8 @@ public record NotificationOutboxQueryRequest(
|
|||||||
DateTimeOffset? To,
|
DateTimeOffset? To,
|
||||||
int PageNumber,
|
int PageNumber,
|
||||||
int PageSize,
|
int PageSize,
|
||||||
string? SourceNodeFilter = null);
|
string? SourceNodeFilter = null,
|
||||||
|
string? NotificationIdFilter = null);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A single notification row summarised for outbox UI display.
|
/// A single notification row summarised for outbox UI display.
|
||||||
|
|||||||
@@ -23,6 +23,10 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Types.Notifications;
|
|||||||
/// <c>"central-a"</c>, <c>"site-plant-a-node-a"</c>). Exact match; <c>null</c>
|
/// <c>"central-a"</c>, <c>"site-plant-a-node-a"</c>). Exact match; <c>null</c>
|
||||||
/// means "do not constrain".
|
/// means "do not constrain".
|
||||||
/// </param>
|
/// </param>
|
||||||
|
/// <param name="NotificationId">
|
||||||
|
/// Exact match on a single <c>NotificationId</c> — the <c>?correlationId=</c> deep link
|
||||||
|
/// from the Flow Debugger / Audit Log. <c>null</c> means "do not constrain".
|
||||||
|
/// </param>
|
||||||
public record NotificationOutboxFilter(
|
public record NotificationOutboxFilter(
|
||||||
NotificationStatus? Status = null,
|
NotificationStatus? Status = null,
|
||||||
NotificationType? Type = null,
|
NotificationType? Type = null,
|
||||||
@@ -33,4 +37,5 @@ public record NotificationOutboxFilter(
|
|||||||
DateTimeOffset? StuckCutoff = null,
|
DateTimeOffset? StuckCutoff = null,
|
||||||
DateTimeOffset? From = null,
|
DateTimeOffset? From = null,
|
||||||
DateTimeOffset? To = null,
|
DateTimeOffset? To = null,
|
||||||
string? SourceNode = null);
|
string? SourceNode = null,
|
||||||
|
string? NotificationId = null);
|
||||||
|
|||||||
+6
@@ -280,6 +280,12 @@ VALUES
|
|||||||
query = query.Where(n => n.Type == type);
|
query = query.Where(n => n.Type == type);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Exact match on one notification — the ?correlationId= deep link.
|
||||||
|
if (!string.IsNullOrEmpty(filter.NotificationId))
|
||||||
|
{
|
||||||
|
query = query.Where(n => n.NotificationId == filter.NotificationId);
|
||||||
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(filter.SourceSiteId))
|
if (!string.IsNullOrEmpty(filter.SourceSiteId))
|
||||||
{
|
{
|
||||||
query = query.Where(n => n.SourceSiteId == filter.SourceSiteId);
|
query = query.Where(n => n.SourceSiteId == filter.SourceSiteId);
|
||||||
|
|||||||
@@ -940,7 +940,8 @@ public class NotificationOutboxActor : ReceiveActor, IWithTimers
|
|||||||
StuckCutoff: request.StuckOnly ? StuckCutoff(now) : null,
|
StuckCutoff: request.StuckOnly ? StuckCutoff(now) : null,
|
||||||
From: request.From,
|
From: request.From,
|
||||||
To: request.To,
|
To: request.To,
|
||||||
SourceNode: request.SourceNodeFilter);
|
SourceNode: request.SourceNodeFilter,
|
||||||
|
NotificationId: request.NotificationIdFilter);
|
||||||
|
|
||||||
using var scope = _serviceProvider.CreateScope();
|
using var scope = _serviceProvider.CreateScope();
|
||||||
var repository = scope.ServiceProvider.GetRequiredService<INotificationOutboxRepository>();
|
var repository = scope.ServiceProvider.GetRequiredService<INotificationOutboxRepository>();
|
||||||
|
|||||||
+28
@@ -1,6 +1,7 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Notifications;
|
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Notifications;
|
||||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
|
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
|
||||||
|
using ZB.MOM.WW.ScadaBridge.Commons.Types.Notifications;
|
||||||
using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase;
|
using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase;
|
||||||
using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Repositories;
|
using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Repositories;
|
||||||
using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests.Migrations;
|
using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests.Migrations;
|
||||||
@@ -101,6 +102,33 @@ public class NotificationOutboxRepositoryIntegrationTests : IClassFixture<MsSqlM
|
|||||||
Assert.Equal(1, count);
|
Assert.Equal(1, count);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SkippableFact]
|
||||||
|
public async Task QueryAsync_NotificationIdFilter_ReturnsExactlyThatRow()
|
||||||
|
{
|
||||||
|
Skip.IfNot(_fixture.Available, _fixture.SkipReason);
|
||||||
|
|
||||||
|
// Flow Debugger / Audit Log ?correlationId= deep link: an exact match on one
|
||||||
|
// NotificationId must return that single row and count it alone.
|
||||||
|
var wantedId = Guid.NewGuid().ToString();
|
||||||
|
var otherId = Guid.NewGuid().ToString();
|
||||||
|
await using (var context = CreateContext())
|
||||||
|
{
|
||||||
|
var repo = new NotificationOutboxRepository(context);
|
||||||
|
await repo.InsertIfNotExistsAsync(MakeNotification(wantedId, subject: "Wanted"));
|
||||||
|
await repo.InsertIfNotExistsAsync(MakeNotification(otherId, subject: "Other"));
|
||||||
|
}
|
||||||
|
|
||||||
|
await using var queryContext = CreateContext();
|
||||||
|
var queryRepo = new NotificationOutboxRepository(queryContext);
|
||||||
|
var (rows, totalCount) = await queryRepo.QueryAsync(
|
||||||
|
new NotificationOutboxFilter(NotificationId: wantedId), pageNumber: 1, pageSize: 50);
|
||||||
|
|
||||||
|
Assert.Equal(1, totalCount);
|
||||||
|
var row = Assert.Single(rows);
|
||||||
|
Assert.Equal(wantedId, row.NotificationId);
|
||||||
|
Assert.Equal("Wanted", row.Subject);
|
||||||
|
}
|
||||||
|
|
||||||
// --- helpers ------------------------------------------------------------
|
// --- helpers ------------------------------------------------------------
|
||||||
|
|
||||||
private ScadaBridgeDbContext CreateContext()
|
private ScadaBridgeDbContext CreateContext()
|
||||||
|
|||||||
+34
@@ -115,6 +115,40 @@ public class NotificationOutboxActorQueryTests : TestKit
|
|||||||
Assert.Equal("site-1", staleSummary.SourceSiteId);
|
Assert.Equal("site-1", staleSummary.SourceSiteId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Query_WithNotificationIdFilter_PassesExactMatchToRepositoryFilter()
|
||||||
|
{
|
||||||
|
_repository.QueryAsync(
|
||||||
|
Arg.Any<NotificationOutboxFilter>(), Arg.Any<int>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
|
||||||
|
.Returns(((IReadOnlyList<Notification>)Array.Empty<Notification>(), 0));
|
||||||
|
var actor = CreateActor();
|
||||||
|
|
||||||
|
actor.Tell(
|
||||||
|
new NotificationOutboxQueryRequest(
|
||||||
|
CorrelationId: "corr-nid",
|
||||||
|
StatusFilter: null,
|
||||||
|
TypeFilter: null,
|
||||||
|
SourceSiteFilter: null,
|
||||||
|
ListNameFilter: null,
|
||||||
|
StuckOnly: false,
|
||||||
|
SubjectKeyword: null,
|
||||||
|
From: null,
|
||||||
|
To: null,
|
||||||
|
PageNumber: 1,
|
||||||
|
PageSize: 50,
|
||||||
|
NotificationIdFilter: "abc"),
|
||||||
|
TestActor);
|
||||||
|
|
||||||
|
var response = ExpectMsg<NotificationOutboxQueryResponse>();
|
||||||
|
Assert.True(response.Success);
|
||||||
|
|
||||||
|
// The ?correlationId= deep link from the Flow Debugger / Audit Log rides through
|
||||||
|
// to the repository as an exact-match NotificationId constraint.
|
||||||
|
_repository.Received(1).QueryAsync(
|
||||||
|
Arg.Is<NotificationOutboxFilter>(f => f.NotificationId == "abc"),
|
||||||
|
1, 50, Arg.Any<CancellationToken>());
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Query_WithStatusFilterString_ParsesToEnumOnFilter()
|
public void Query_WithStatusFilterString_ParsesToEnumOnFilter()
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user