Open benevbright opened 1 year ago
@sinclairzx81 could you give some help here?
@benevbright Hi, TypeBox doesn't implement string formats by default, so you will need to specify these yourself. This is somewhat similar to Ajv where formats are typically loaded via the auxiliary ajv-formats
package (Which I think Fastify defaultly configures Ajv with)
To get the common formats available to the TypeBox Compiler, add the following script to your project. This script lifts the formats from ajv-formats
and makes them available to TypeBox, each is registered via the FormatRegistry.Set
function. You should import this script with import './formats'
import { FormatRegistry } from '@sinclair/typebox'
// -------------------------------------------------------------------------------------------
// Format Registration
// -------------------------------------------------------------------------------------------
FormatRegistry.Set('date-time', (value) => IsDateTime(value, true))
FormatRegistry.Set('date', (value) => IsDate(value))
FormatRegistry.Set('time', (value) => IsTime(value))
FormatRegistry.Set('email', (value) => IsEmail(value))
FormatRegistry.Set('uuid', (value) => IsUuid(value))
FormatRegistry.Set('url', (value) => IsUrl(value))
FormatRegistry.Set('ipv6', (value) => IsIPv6(value))
FormatRegistry.Set('ipv4', (value) => IsIPv4(value))
// -------------------------------------------------------------------------------------------
// https://github.com/ajv-validator/ajv-formats/blob/master/src/formats.ts
// -------------------------------------------------------------------------------------------
const UUID = /^(?:urn:uuid:)?[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/i
const DATE_TIME_SEPARATOR = /t|\s/i
const TIME = /^(\d\d):(\d\d):(\d\d(?:\.\d+)?)(z|([+-])(\d\d)(?::?(\d\d))?)?$/i
const DATE = /^(\d\d\d\d)-(\d\d)-(\d\d)$/
const DAYS = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
const IPV4 = /^(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$/
const IPV6 = /^((([0-9a-f]{1,4}:){7}([0-9a-f]{1,4}|:))|(([0-9a-f]{1,4}:){6}(:[0-9a-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9a-f]{1,4}:){5}(((:[0-9a-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9a-f]{1,4}:){4}(((:[0-9a-f]{1,4}){1,3})|((:[0-9a-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-f]{1,4}:){3}(((:[0-9a-f]{1,4}){1,4})|((:[0-9a-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-f]{1,4}:){2}(((:[0-9a-f]{1,4}){1,5})|((:[0-9a-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-f]{1,4}:){1}(((:[0-9a-f]{1,4}){1,6})|((:[0-9a-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9a-f]{1,4}){1,7})|((:[0-9a-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))$/i
const URL = /^(?:https?|wss?|ftp):\/\/(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z0-9\u{00a1}-\u{ffff}]+-)*[a-z0-9\u{00a1}-\u{ffff}]+)(?:\.(?:[a-z0-9\u{00a1}-\u{ffff}]+-)*[a-z0-9\u{00a1}-\u{ffff}]+)*(?:\.(?:[a-z\u{00a1}-\u{ffff}]{2,})))(?::\d{2,5})?(?:\/[^\s]*)?$/iu
const EMAIL = /^[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i
function IsLeapYear(year: number): boolean {
return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0)
}
function IsDate(str: string): boolean {
const matches: string[] | null = DATE.exec(str)
if (!matches) return false
const year: number = +matches[1]
const month: number = +matches[2]
const day: number = +matches[3]
return month >= 1 && month <= 12 && day >= 1 && day <= (month === 2 && IsLeapYear(year) ? 29 : DAYS[month])
}
function IsTime(str: string, strictTimeZone?: boolean): boolean {
const matches: string[] | null = TIME.exec(str)
if (!matches) return false
const hr: number = +matches[1]
const min: number = +matches[2]
const sec: number = +matches[3]
const tz: string | undefined = matches[4]
const tzSign: number = matches[5] === '-' ? -1 : 1
const tzH: number = +(matches[6] || 0)
const tzM: number = +(matches[7] || 0)
if (tzH > 23 || tzM > 59 || (strictTimeZone && !tz)) return false
if (hr <= 23 && min <= 59 && sec < 60) return true
const utcMin = min - tzM * tzSign
const utcHr = hr - tzH * tzSign - (utcMin < 0 ? 1 : 0)
return (utcHr === 23 || utcHr === -1) && (utcMin === 59 || utcMin === -1) && sec < 61
}
function IsDateTime(value: string, strictTimeZone?: boolean): boolean {
const dateTime: string[] = value.split(DATE_TIME_SEPARATOR)
return dateTime.length === 2 && IsDate(dateTime[0]) && IsTime(dateTime[1], strictTimeZone)
}
function IsEmail(value: string) {
return EMAIL.test(value)
}
function IsUuid(value: string) {
return UUID.test(value)
}
function IsUrl(value: string) {
return URL.test(value)
}
function IsIPv6(value: string) {
return IPV6.test(value)
}
function IsIPv4(value: string) {
return IPV4.test(value)
}
Additional Information on Format and Type registration can be found https://github.com/sinclairzx81/typebox#typesystem.
It might be a nice idea to include these format configurations by default within this package.
Hope this helps! S
hi @sinclairzx81 Thanks a lot always!
It might be a nice idea to include these format configurations by default within this package.
That's good idea. Since with ajv
, it works out of the box.
@benevbright Would you like to send a Pull Request to address this issue? Remember to add unit tests.
Hi! I experienced the same issue with 'date-time', and this would be a very welcome addition!
I was able to get it working with the script above in my app. This post is edited, since I made the simple mistake of using an invalid string when testing. I've left this code as an example of how it can be done instead :)
Installed dependencies:
"@fastify/type-provider-typebox": "^3.4.0",
"@sinclair/typebox": "^0.28.15",
"fastify": "^4.17.0",
App:
import './routes/typebox-formats-hack' // Includes relevant parts from above for 'date-time'
...
const f = fastify({
// some custom options
})
.setValidatorCompiler(TypeBoxValidatorCompiler)
.withTypeProvider<TypeBoxTypeProvider>()
await fastify.register(myRouter, { prefix: '/my-prefix/:customParam' }) // Actually using a few layers of FastifyPluginAsync
Route schemas:
const myQuerySchema = Type.Object({
start: Type.Optional(Type.String({ format: 'date-time' } )),
end: Type.Optional(Type.String({ format: 'date-time' }))
}, { additionalProperties: false })
export const myFullSchema = {
querystring: myQuerySchema,
params: myParamsSchema, // Omitted for brevity
response: {
200: myResponseSchema // Omitted for brevity
},
}
// Type checking in the handler doesn't work without this
export interface MyRoute extends RouteGenericInterface {
Querystring: Static<typeof myQuerySchema>
Params: Static<typeof myParamsSchema>
Reply: Static<typeof myResponseSchema>
}
export const myRouteOptions: RouteShorthandOptions = {
schema: myFullSchema
}
Route handler:
// From example here: https://github.com/fastify/fastify-type-provider-typebox
export type FastifyRequestTypebox<TSchema extends FastifySchema> = FastifyRequest<
RouteGenericInterface,
RawServerDefault,
RawRequestDefaultExpression<RawServerDefault>,
TSchema,
TypeBoxTypeProvider
>;
export type FastifyReplyTypebox<TSchema extends FastifySchema> = FastifyReply<
RawServerDefault,
RawRequestDefaultExpression,
RawReplyDefaultExpression,
RouteGenericInterface,
ContextConfigDefault,
TSchema,
TypeBoxTypeProvider
>
export const tokensRouter: FastifyPluginAsync = async (fastify) => {
fastify.get<MyRoute>('/my-endpoint', myRouteOptions, myHandler)
}
export const myHandler: RouteHandler<MyRoute> = async function (req: FastifyRequestTypebox<typeof myFullSchema>, reply: FastifyReplyTypebox<typeof myFullSchema>) {
// Custom endpoint logic here
}
Response when calling GET on /my-endpoint with an invalid string, e.g. "2023-07-01" (just a date):
{
"name": "ValidationError",
"message": "querystring/start Expected string to match format 'date-time'"
}
@johanehr What's the value of your start
param?
@benevbright, thanks for the quick response!
I should have looked at it with fresh eyes - I was actually sending in an invalid string, that would be parsed correctly by luxon DateTime ("2023-07-01"). It turns out that it does work after all with e.g. "2023-07-01T01:23:45+01:00"!
Sorry for the confusion (I'll update my comment accordingly, as it could still be a useful example for someone).
Prerequisites
Fastify version
4.15.0
Plugin version
3.2.0
Node.js version
18
Operating system
macOS
Operating system version (i.e. 20.04, 11.3, 10)
13.3.1
Description
Type.String({ format: "date" }) doesn't work in body schema validator
This works fine with
ajv
validator but as soon as I enableTypeBoxValidatorCompiler
, It fails to receive request with the error above.Steps to Reproduce
use
Type.String({ format: "date" })
in body schema withTypeBoxValidatorCompiler
enabledExpected Behavior
No response