sinclairzx81 / typebox

Json Schema Type Builder with Static Type Resolution for TypeScript
Other
4.65k stars 150 forks source link

Type.Date() cause errors with fastify and querybuilder returning type Date #822

Closed tzezar closed 3 months ago

tzezar commented 3 months ago

I encountered a problem. Using querybuilders e.g. drizzle the timestamp field from postgresql has a Date type. Specifically Date | null. When creating a schema for the response body, fastify spits out an error that I don't understand too much, and the answer found in another post doesn't work.

I will show you some code to give context to this problem.

export const GetProductHandler = async (
    req: FastifyRequestTypebox<typeof GetProductSchema>,
    reply: FastifyReplyTypebox<typeof GetProductSchema>
) => {
    const { name } = req.query
    const { id } = req.params

    let results = await db.select({
        id: sku.id,
        name: sku.name,
        createdAt: sku.createdAt,
        unit: {
            id: unit.id,
            name: unit.name
        }
    })
    .from(sku)
    .innerJoin(unit, eq(unit.id, sku.stockUnitId))
    reply.status(200).send(results)
}

results type:

let results: {
    id: number;
    name: string;
    createdAt: Date | null;
    unit: {
        name: string | null;
        id: number;
    };
}[]

route schema:


export const GetProductSchema = {
  querystring: Type.Object({
    name: Type.String(),
  }),
  params: Type.Object({
    id: Type.String(),
  }),
  response: {
    200: Type.Array(
      Type.Object({
        id: Type.Number(),
        name: Type.String(),
        createdAt: Type.Union([Type.Date(), Type.Null()]),
        unit: Type.Object({
          id: Type.Number(),
          name: Type.Union([Type.String(), Type.Null()])
        })
      })
    ),
  }
}

response schema type (the same as results I provide to handler)

let t: Static<typeof GetProductSchema.response[200]> 

let t: {
    name: string;
    id: number;
    createdAt: Date | null;
    unit: {
        name: string | null;
        id: number;
    };
}[]

and problem comes probably from this part: createdAt: Type.Union([Type.Date(), Type.Null()]),

I tried some solutions from simmilar topic I found:

// Transforms a ISO8601 encoded Date String into a JavaScript Date object
const DateTime = Type.Transform(Type.Date({ format: 'datetime' }))
  .Decode(value => value.toISOString())
  .Encode(value => new Date(value))

but I failed doing so.

Finally the error I receive in the console:

{"level":50,"time":1712672946047,"pid":23440,"hostname":"DESKTOP-0U7E6K0","err":{"type":"FastifyError","message":"Failed building the 
serialization schema for GET: /products/:id, due to error schema is invalid: data/items/properties/createdAt/anyOf/0/type must be equal to one of the allowed values","stack":"FastifyError: Failed building the serialization schema for GET: /products/:id, due to error schema is invalid: data/items/properties/createdAt/anyOf/0/type must be equal to one of the allowed values\n    at Boot.<anonymous> (D:\\backend-v2\\node_modules\\fastify\\lib\\route.js:425:21)\n    at Object.onceWrapper (node:events:632:28)\n    at Boot.emit (node:events:530:35)\n    at Boot.emit (node:domain:488:12)\n    at D:\\backend-v2\\node_modules\\avvio\\boot.js:102:12\n    at D:\\backend-v2\\node_modules\\avvio\\boot.js:428:7\n    at done (D:\\backend-v2\\node_modules\\avvio\\lib\\plugin.js:221:5)\n    at check (D:\\backend-v2\\node_modules\\avvio\\lib\\plugin.js:245:9)\n    at node:internal/process/task_queues:140:7\n    at AsyncResource.runInAsyncScope (node:async_hooks:206:9)","code":"FST_ERR_SCH_SERIALIZATION_BUILD","name":"FastifyError","statusCode":500},"msg":"Failed building the serialization schema for GET: /products/:id, due to error schema is invalid: data/items/properties/createdAt/anyOf/0/type must be equal to one of the allowed values"}
[nodemon] app crashed - waiting for file changes before starting...
sinclairzx81 commented 3 months ago

@tzezar Hi,

When using Fastify, you shouldn't use Type.Date. TypeBox added this type as an extended type, but the schema it produces is unknown to Ajv (which is the validator Fastify uses internally). The issue stems from Json Schema (and Json) being unable to encode JavaScript Date objects, and that dates need to be encoded as either number or string (iso8601). For this reason, you should define the schema as either Type.Number (timestamp) or Type.String (iso8601)

Transforms

If you're using https://github.com/fastify/fastify-type-provider-typebox, unfortunately you won't be able to use Type.Transform as this is only applicable to TypeBox's validators (specifically the Encode / Decode functions). There are ways to integrate Encode/Decode into the Fastify validation pipeline, but as of today, the Encode/Decode hasn't been integrated into the Fastify type provider project (so if you want to use these Transform types, you will need to implement them manually).

From memory, you will need to override via the setValidationCompiler and preValidationHook handler. It looks like I've drafted an experimental implementation of Encode/Decode which is noted at the following issue comment.

https://github.com/fastify/fastify-type-provider-typebox/issues/128#issuecomment-1960644297

I may take a look at providing the Type Provider project a more fleshed out implementation later in the year, but if you do try out the implementation noted on this comment thread and it works good for you, feel free to submit a PR to the provider project, ill be able to help review the PR.

Hope this helps. S

sinclairzx81 commented 3 months ago

@tzezar Hey, might close off this issue as it's generally related to Fastify (and Drizzle) integration.

I can only really speak for Fastify integrations (as that project I've had experience with). But as a general rule, when working with projects that are based on the Json Schema specification, only use the [Json] types and avoid the [JavaScript] types (as the latter will only work with TypeBox validators). The types listed for either category can be found here.

https://github.com/sinclairzx81/typebox?tab=readme-ov-file#types-json https://github.com/sinclairzx81/typebox?tab=readme-ov-file#javascript-types

If you have any follow up questions, feel free to ping on this thread. Cheers S