Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6809,7 +6809,7 @@ export const ulidWire = z

export const dateTimeWire = z
.string()
.datetime()
.datetime({ offset: true })

.describe(
'[RFC3339](https://tools.ietf.org/html/rfc3339) formatted date-time string in UTC.',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -295,3 +295,79 @@ describe('nullable cursor response validation', () => {
expect(result.value?.meta.page.previous).toBe(42)
})
})

// RFC 3339 permits a numeric UTC offset, not just `Z`, and the API emits them.
describe('strict validation accepts RFC 3339 numeric UTC offsets', () => {
it('accepts meter query rows whose from/to carry a numeric offset', async () => {
fetchMock.route(
'*',
json({
data: [
{
value: '10',
from: '2024-01-01T00:00:00+02:00',
to: '2024-01-02T00:00:00-07:00',
dimensions: { subject: 'customer-1' },
},
],
}),
)

const result = await funcs.queryMeter(client(true), { meterId, body: {} })

expect(result.error).toBeUndefined()
expect(result.ok).toBe(true)
expect(result.value?.data[0]?.value).toBe('10')
// the offset is a real instant, not a UTC wall-clock reading
expect(result.value?.data[0]?.from?.toISOString()).toBe(
'2023-12-31T22:00:00.000Z',
)
expect(result.value?.data[0]?.to?.toISOString()).toBe(
'2024-01-02T07:00:00.000Z',
)
})

it('still accepts a plain Z timestamp', async () => {
fetchMock.route(
'*',
json({
data: [
{
value: '10',
from: '2024-01-01T00:00:00Z',
to: '2024-01-02T00:00:00Z',
dimensions: { subject: 'customer-1' },
},
],
}),
)

const result = await funcs.queryMeter(client(true), { meterId, body: {} })

expect(result.ok).toBe(true)
expect(result.value?.data[0]?.from?.toISOString()).toBe(
'2024-01-01T00:00:00.000Z',
)
})

it('still rejects a genuinely malformed timestamp', async () => {
fetchMock.route(
'*',
json({
data: [
{
value: '10',
from: 'not-a-timestamp',
to: '2024-01-02T00:00:00Z',
dimensions: { subject: 'customer-1' },
},
],
}),
)

const result = await funcs.queryMeter(client(true), { meterId, body: {} })

expect(result.ok).toBe(false)
expect(result.error).toBeInstanceOf(ValidationError)
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -216,10 +216,16 @@ function scalarBaseType($: Typekit, type: Scalar) {
* pass types them `z.date()` (the SDK surface takes and returns `Date`; the
* runtime wire mapper converts at the request/response boundary), while the
* wire pass keeps the RFC 3339 string the JSON payload actually carries.
*
* `offset: true` because RFC 3339 permits a numeric UTC offset, not just `Z`,
* and the API emits them; zod's default rejects everything but `Z`.
*/
function dateTimeBaseType() {
if (useWireMode()) {
return zodMemberExpr(callPart('string'), callPart('datetime'))
return zodMemberExpr(
callPart('string'),
callPart('datetime', '{ offset: true }'),
)
}
return zodMemberExpr(callPart('date'))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -293,3 +293,79 @@ describe('nullable cursor response validation', () => {
expect(result.value?.meta.page.previous).toBe(42)
})
})

// RFC 3339 permits a numeric UTC offset, not just `Z`, and the API emits them.
describe('strict validation accepts RFC 3339 numeric UTC offsets', () => {
it('accepts meter query rows whose from/to carry a numeric offset', async () => {
fetchMock.route(
'*',
json({
data: [
{
value: '10',
from: '2024-01-01T00:00:00+02:00',
to: '2024-01-02T00:00:00-07:00',
dimensions: { subject: 'customer-1' },
},
],
}),
)

const result = await funcs.queryMeter(client(true), { meterId, body: {} })

expect(result.error).toBeUndefined()
expect(result.ok).toBe(true)
expect(result.value?.data[0]?.value).toBe('10')
// the offset is a real instant, not a UTC wall-clock reading
expect(result.value?.data[0]?.from?.toISOString()).toBe(
'2023-12-31T22:00:00.000Z',
)
expect(result.value?.data[0]?.to?.toISOString()).toBe(
'2024-01-02T07:00:00.000Z',
)
})

it('still accepts a plain Z timestamp', async () => {
fetchMock.route(
'*',
json({
data: [
{
value: '10',
from: '2024-01-01T00:00:00Z',
to: '2024-01-02T00:00:00Z',
dimensions: { subject: 'customer-1' },
},
],
}),
)

const result = await funcs.queryMeter(client(true), { meterId, body: {} })

expect(result.ok).toBe(true)
expect(result.value?.data[0]?.from?.toISOString()).toBe(
'2024-01-01T00:00:00.000Z',
)
})

it('still rejects a genuinely malformed timestamp', async () => {
fetchMock.route(
'*',
json({
data: [
{
value: '10',
from: 'not-a-timestamp',
to: '2024-01-02T00:00:00Z',
dimensions: { subject: 'customer-1' },
},
],
}),
)

const result = await funcs.queryMeter(client(true), { meterId, body: {} })

expect(result.ok).toBe(false)
expect(result.error).toBeInstanceOf(ValidationError)
})
})
Loading