Build ErsatzTV Image / CI toolchain image resolves (push) Successful in 9s
Build ErsatzTV Image / Delimiter ban (release path) (push) Successful in 21s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 10m54s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 7m34s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Successful in 6m59s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (push) Skipped
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (push) Skipped
Build ErsatzTV Image / Build & push image (amd64) (push) Failing after 1m27s
The three recurrence arrays are read CONJUNCTIVELY by AlternateScheduleSelector.GetScheduleForDate, so an empty set matches no date. `?? []` on an omitted array therefore returned HTTP 200 while storing an alternate-schedule or template item that could never apply, silently -- while the read side (#823) already read a NULL column as the All*() sets. Absent and explicitly-empty are two different requests and get two answers: ABSENT (missing, or explicit null) normalizes to AlternateScheduleSelector.All*(), the same symbols the read side substitutes; EXPLICIT [] is rejected with a 422 naming the consequence, via RecurrenceSetBounds called from both replace handlers. The rejection lives in the handlers, not the controller, because api.ffmpeg-profile-numeric-bounds' "accept an UNCHANGED bad value" rule binds hardest here: both PUT paths are whole-list replaces, so rejecting a pre-existing empty set would make every OTHER item in the list uneditable. That comparison needs the stored row. The validated set is derived from `incoming`, so the highest-Index catch-all -- whose recurrence the handler discards -- is excluded by construction. Verified: full ErsatzTV.Tests suite green; three mutation proofs with disjoint reddened sets; live-E2E against a real instance confirmed an OMITTED property round-trips as unrestricted (the Newtonsoft missing-property chain unit tests cannot reach), an explicit [] returns the 422, and [] on the catch-all is accepted. Cross-family cold review BLOCKED the first implementation with 3 findings, all real and all fixed; re-review returned MERGEABLE. Follow-up #894 filed: the SPA can still build the empty state the server rejects. fixes #880 Decisions-Edit: yes Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
49 lines
1.9 KiB
C#
49 lines
1.9 KiB
C#
#nullable enable
|
|
using ErsatzTV.Application.Playouts;
|
|
using ErsatzTV.Core.Scheduling;
|
|
|
|
namespace ErsatzTV.Controllers.Api.Requests;
|
|
|
|
public record PlayoutAlternateScheduleItemRequest(
|
|
int Id,
|
|
int ProgramScheduleId,
|
|
List<DayOfWeek>? DaysOfWeek,
|
|
List<int>? DaysOfMonth,
|
|
List<int>? MonthsOfYear,
|
|
bool LimitToDateRange,
|
|
int StartMonth,
|
|
int StartDay,
|
|
int? StartYear,
|
|
int EndMonth,
|
|
int EndDay,
|
|
int? EndYear)
|
|
{
|
|
// An ABSENT recurrence array means UNRESTRICTED, not "matches nothing" (#880). The three sets are
|
|
// conjunctive in AlternateScheduleSelector.GetScheduleForDate -- a miss on any one `continue`s -- so
|
|
// the previous `?? []` stored an item that could never apply on any date, and returned 200 while
|
|
// doing it. Absence now normalizes to the same All*() sets the READ side substitutes for a NULL
|
|
// column (AlternateScheduleSelector's read guard and both Mapper.ProjectToViewModel overloads), so
|
|
// the two halves of "this field is absent" finally agree -- residual (3) of
|
|
// `media.nullable-primitive-collection-mutation`.
|
|
//
|
|
// These are nullable so that ABSENT is distinguishable from an explicitly-sent `[]`, which is a
|
|
// different request and is REJECTED with a 422 in PlayoutController rather than normalized here.
|
|
// Newtonsoft maps both a missing property and an explicit `null` to null, so both read as absent;
|
|
// only a literal `[]` survives as empty.
|
|
public ReplacePlayoutAlternateSchedule ToReplaceItem(int index) =>
|
|
new(
|
|
Id,
|
|
index,
|
|
ProgramScheduleId,
|
|
DaysOfWeek ?? AlternateScheduleSelector.AllDaysOfWeek(),
|
|
DaysOfMonth ?? AlternateScheduleSelector.AllDaysOfMonth(),
|
|
MonthsOfYear ?? AlternateScheduleSelector.AllMonthsOfYear(),
|
|
LimitToDateRange,
|
|
StartMonth,
|
|
StartDay,
|
|
StartYear,
|
|
EndMonth,
|
|
EndDay,
|
|
EndYear);
|
|
}
|