elysiajs / elysia

Ergonomic Framework for Humans
https://elysiajs.com
MIT License
9.74k stars 207 forks source link

Allow multiple handlers with same path in something like guard #760

Open bogeychan opened 1 month ago

bogeychan commented 1 month ago

What is the problem this feature would solve?

Webhooks send POST requests to the same endpoint (based on event-literal). Someone on discord wanna separate this logic into different functions based on event.

Pseudo code:

const customerCreated = new Elysia({ name: "created" })
  .if(({ body }) => body.event === "customer.created")
  .post("/", () => console.log("customer created!"));

const customerUpdated = new Elysia({ name: "updated" })
  .if(({ body }) => body.event === "customer.updated")
  .post("/", () => console.log("customer updated!"));

const stripeWebhook = new Elysia().use(customerCreated).use(customerUpdated);

What is the feature you are proposing to solve the problem?

Allow something like this to work with the same path (/):

const webhooks = new Elysia({ name: "webhooks" })
  .guard(
    {
      body: t.Object({
        event: t.Literal("customer.created"),
        propCreated: t.String(),
      }),
    },
    (app) => app.post("/", () => "cr")
  )
  .guard(
    {
      body: t.Object({
        event: t.Literal("customer.updated"),
        propUpdated: t.String(),
      }),
    },
    (app) => app.post("/", () => "up")
  );

new Elysia().use(webhooks).listen(8080);

At the moment it throws a validation error:

Click to expand ```json { "type": "validation", "on": "body", "summary": "Property 'propUpdated' is missing", "property": "/propUpdated", "message": "Required property", "expected": { "event": "customer.updated", "propUpdated": "" }, "found": { "event": "customer.created" }, "errors": [ { "type": 45, "schema": { "type": "string" }, "path": "/propUpdated", "message": "Required property", "summary": "Property 'propUpdated' is missing" }, { "summary": "Expected 'customer.updated'", "type": 32, "schema": { "const": "customer.updated", "type": "string" }, "path": "/event", "value": "customer.created", "message": "Expected 'customer.updated'" }, { "type": 54, "schema": { "type": "string" }, "path": "/propUpdated", "message": "Expected string", "summary": "Expected property 'propUpdated' to be string but found: undefined" } ] } ```

If i send this body:

{
    "event": "customer.created",
    "propCreated": "1"
}

What alternatives have you considered?

I considered this, but it has a lot of unnecessary code:

This is not easy for newbies and i've to do isCustomerUpdated , ... checks because of typescript type guard limitation with nested unions.

Click to expand ```ts import { t, Elysia, type Static, type Context, type TSchema, type InputSchema, } from "elysia"; const customerCreated = t.Object({ event: t.Literal("customer.created"), propCreated: t.String(), }); type CustomerCreatedContext = StaticContextExclude< typeof customerWebhooks, Static >; const customerUpdated = t.Object({ event: t.Literal("customer.updated"), propUpdated: t.String(), }); type CustomerUpdatedContext = StaticContextExclude< typeof customerWebhooks, Static >; const customerWebhooks = { body: t.Union([customerCreated, customerUpdated]), } satisfies InputSchema; type StaticContext = Context<{ [Key in keyof T]: T[Key] extends TSchema ? Static : T[Key]; }>; type StaticContextExclude = Context<{ [Key in keyof T]: T[Key] extends TSchema ? Exclude, E> : Exclude; }>; function handleCustomerCreated({ body }: CustomerCreatedContext) { return body.propCreated; } function handleCustomerUpdated({ body }: CustomerUpdatedContext) { return body.propUpdated; } function isCustomerCreated( ctx: StaticContext ): ctx is CustomerCreatedContext { return ctx.body.event === "customer.created"; } function isCustomerUpdated( ctx: StaticContext ): ctx is CustomerUpdatedContext { return ctx.body.event === "customer.updated"; } const webhooks = new Elysia({ name: "webhooks" }).post( "/", (ctx) => { if (isCustomerCreated(ctx)) { return handleCustomerCreated(ctx); } else if (isCustomerUpdated(ctx)) { return handleCustomerUpdated(ctx); } }, customerWebhooks ); new Elysia().use(webhooks).listen(8080); ```