@@ -30,6 +30,7 @@ type fieldFiltersTarget struct {
3030 ULID * filters.FilterULID `json:"ulid,omitempty"`
3131 DateTime * filters.FilterDateTime `json:"datetime,omitempty"`
3232 Labels * filters.FilterLabels `json:"labels,omitempty"`
33+ Timestamp * time.Time `json:"timestamp,omitempty"`
3334}
3435
3536// validatorErrorResponse mirrors the AIP-style error body produced by
@@ -229,6 +230,52 @@ func TestFieldFilterValidation(t *testing.T) {
229230 wantReasonSubstr : "must be an object" ,
230231 },
231232
233+ // Shared.DateTime scalar (plain timestamp field, not a filter wrapper).
234+ // The OAS validator enforces RFC-3339 date-time format (kin-openapi regex).
235+ // Valid: UTC (Z), positive/negative numeric offsets, sub-second precision, leap second.
236+ {name : "timestamp valid UTC Z" , query : "filter[timestamp]=2024-01-01T00:00:00Z" , wantStatus : http .StatusNoContent },
237+ // + must be percent-encoded in query strings; raw + decodes to space.
238+ {name : "timestamp valid positive offset" , query : "filter[timestamp]=2024-06-15T12:30:00%2B05:30" , wantStatus : http .StatusNoContent },
239+ {name : "timestamp valid negative offset" , query : "filter[timestamp]=2024-06-15T12:30:00-07:00" , wantStatus : http .StatusNoContent },
240+ {name : "timestamp valid sub-second precision" , query : "filter[timestamp]=2024-01-02T03:04:05.999Z" , wantStatus : http .StatusNoContent },
241+ {name : "timestamp valid leap second" , query : "filter[timestamp]=2016-12-31T23:59:60Z" , wantStatus : http .StatusNoContent },
242+ // Invalid: not a date-time string at all.
243+ {
244+ name : "timestamp invalid not a datetime" ,
245+ query : "filter[timestamp]=not-a-datetime" ,
246+ wantStatus : http .StatusBadRequest ,
247+ wantField : "timestamp" ,
248+ wantRule : "format" ,
249+ wantReasonSubstr : "date-time" ,
250+ },
251+ // Invalid: date-only (missing time component).
252+ {
253+ name : "timestamp invalid date only" ,
254+ query : "filter[timestamp]=2024-01-01" ,
255+ wantStatus : http .StatusBadRequest ,
256+ wantField : "timestamp" ,
257+ wantRule : "format" ,
258+ wantReasonSubstr : "date-time" ,
259+ },
260+ // Invalid: missing timezone designator.
261+ {
262+ name : "timestamp invalid no timezone" ,
263+ query : "filter[timestamp]=2024-01-01T00:00:00" ,
264+ wantStatus : http .StatusBadRequest ,
265+ wantField : "timestamp" ,
266+ wantRule : "format" ,
267+ wantReasonSubstr : "date-time" ,
268+ },
269+ // Invalid: space separator instead of T.
270+ {
271+ name : "timestamp invalid space separator" ,
272+ query : "filter[timestamp]=2024-01-01+00:00:00Z" ,
273+ wantStatus : http .StatusBadRequest ,
274+ wantField : "timestamp" ,
275+ wantRule : "format" ,
276+ wantReasonSubstr : "date-time" ,
277+ },
278+
232279 // Multiple filters in one request — independent fields can be combined.
233280 {
234281 name : "combined boolean+numeric+string" ,
@@ -528,6 +575,13 @@ func TestFieldFilterParse(t *testing.T) {
528575 wantParse : fieldFiltersTarget {Labels : & filters.FilterLabels {"key" : {Contains : lo .ToPtr ("team" )}}},
529576 },
530577
578+ // Shared.DateTime scalar — plain *time.Time field, not a filter wrapper.
579+ {
580+ name : "timestamp short" ,
581+ query : "filter[timestamp]=2024-01-02T03:04:05Z" ,
582+ wantParse : fieldFiltersTarget {Timestamp : & dt },
583+ },
584+
531585 // Multiple independent filters in one request.
532586 {
533587 name : "combined boolean+numeric+string" ,
0 commit comments