Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: h3 adapter #358

Merged
merged 31 commits into from
Sep 29, 2023
Merged
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
8800f07
feat: h3 adapter
juliusmarminge Sep 16, 2023
415dff0
example
juliusmarminge Sep 16, 2023
9486619
json.body
juliusmarminge Sep 16, 2023
154bccc
format
juliusmarminge Sep 16, 2023
598f395
fix type to event
juliusmarminge Sep 16, 2023
da6a7a9
Apply suggestions from code review
juliusmarminge Sep 16, 2023
4bc0812
lint
juliusmarminge Sep 16, 2023
2ce840f
Merge branch 'main' into h3-adapter
juliusmarminge Sep 16, 2023
a18797a
labeler
juliusmarminge Sep 16, 2023
edaeb0b
skiplibcheck in right place (temp)
juliusmarminge Sep 16, 2023
79efa48
Merge branch 'main' into h3-adapter
juliusmarminge Sep 17, 2023
cceb6ce
fix test
juliusmarminge Sep 17, 2023
154d387
prettier-ignore
juliusmarminge Sep 17, 2023
cc1fbd0
event duhh
juliusmarminge Sep 17, 2023
cd4c998
route -> event
juliusmarminge Sep 17, 2023
3e7025d
doc
juliusmarminge Sep 17, 2023
ef9d4a3
Merge branch 'main' into h3-adapter
juliusmarminge Sep 18, 2023
3cc954a
Merge branch 'main' into h3-adapter
juliusmarminge Sep 28, 2023
165c25e
fixy
juliusmarminge Sep 28, 2023
84d43a7
rm nitro for h3
juliusmarminge Sep 29, 2023
3af7aa7
rev
juliusmarminge Sep 29, 2023
62d81ed
more rev
juliusmarminge Sep 29, 2023
e5af263
Merge branch 'main' into h3-adapter
juliusmarminge Sep 29, 2023
28b441d
fix: rewrite to avoid use of router in `createH3EventHandler` (#399)
danielroe Sep 29, 2023
e45ea22
readme
juliusmarminge Sep 29, 2023
d481c66
Apply suggestions from code review
juliusmarminge Sep 29, 2023
8343a77
simplify
juliusmarminge Sep 29, 2023
4d3adae
simplify
juliusmarminge Sep 29, 2023
b1e6d93
fmt
juliusmarminge Sep 29, 2023
38f8a15
🤦‍♂️
juliusmarminge Sep 29, 2023
63d83a6
Apply suggestions from code review
juliusmarminge Sep 29, 2023
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
6 changes: 5 additions & 1 deletion .github/labeler.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@
- any: ['packages/uploadthing/src/next.ts', 'packages/uploadthing/src/next-legacy.ts']

'backend adapters':
- any: ['packages/uploadthing/src/express.ts', 'packages/uploadthing/src/fastify.ts']
- any: [
'packages/uploadthing/src/express.ts',
'packages/uploadthing/src/h3.ts',
'packages/uploadthing/src/fastify.ts'
]

'sdk':
- any: ['packages/uploadthing/src/sdk/**']
128 changes: 128 additions & 0 deletions docs/src/pages/backend-adapters/h3.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import { Callout, Steps, Tabs } from "nextra-theme-docs";

# Getting started with H3

> Added in `v5.7`
markflorkowski marked this conversation as resolved.
Show resolved Hide resolved

H3 is a HTTP framework that powers the web server framework Nitro and other
full-stack frameworks like Nuxt and soon also SolidStart. This adapter will work
for all frameworks that use H3 under the hood.

## Package Setup

<Steps>

### Install the package

```sh npm2yarn
npm install uploadthing
```

### Add env variables

<Callout>
If you don't already have a uploadthing secret key, [sign
up](https://uploadthing.com/sign-in) and create one from the
[dashboard!](https://uploadthing.com/dashboard)
</Callout>

```bash copy
UPLOADTHING_SECRET=... # A secret key for your app (starts with sk_live_)
UPLOADTHING_APP_ID=... # Your app id
```

</Steps>

## Set Up A FileRouter

<Steps>

### Creating your first FileRoute

All files uploaded to uploadthing are associated with a FileRoute. Following
example is very minimalistic. To get full insight into what you can do with the
FileRoutes, please refer to the
[File Router API](/api-reference/server#file-routes).

```ts copy filename="src/uploadthing.ts"
import { createUploadthing, type FileRouter } from "uploadthing/fastify";
juliusmarminge marked this conversation as resolved.
Show resolved Hide resolved

const f = createUploadthing();

export const uploadRouter = {
videoAndImage: f({
image: {
maxFileSize: "4MB",
maxFileCount: 4,
},
video: {
maxFileSize: "16MB",
},
}).onUploadComplete((data) => {
console.log("upload completed", data);
}),
} satisfies FileRouter;

export type OurFileRouter = typeof uploadRouter;
```

### Create the H3 event handlers

<Tabs items={["Vanilla H3", "Nitro", "Nuxt"]}>
<Tabs.Tab>
```ts
import { createApp, createRouter } from "h3";

import { createH3EventHandler } from "uploadthing/h3";
import { uploadRouter } from "./router";

const app = createApp();
const router = createRouter();

router.use(
"/api/uploadthing",
createH3EventHandler({ router: uploadRouter })
);

app.use(router);

export { app }; // Run server with e.g. `listhen`
```

</Tabs.Tab>
<Tabs.Tab>
```ts filename="routes/api/uploadthing.ts"
import { createH3EventHandler } from "uploadthing/h3";

import { uploadRouter } from "./router";

export default createH3EventHandler({ router: uploadRouter });
```

</Tabs.Tab>
<Tabs.Tab>
```ts filename="server/api/uploadthing.ts"
import { createH3EventHandler } from "uploadthing/h3";

import { uploadRouter } from "./router";

export default createH3EventHandler({ router: uploadRouter });
```

</Tabs.Tab>
</Tabs>

### Use the FileRouter in your app

Please refer to client side examples:

<Callout>
Support for Vue is coming soon which will allow you to use the fullstack
framework Nuxt with Uploadthing.
</Callout>

- [NextJS App directory](/nextjs/appdir#creating-the-uploadthing-components-optional)
- [NextJS Pages directory](/nextjs/appdir#creating-the-uploadthing-components-optional)
- [SolidStart](/solid#use-the-filerouter-in-your-app-1)

</Steps>
1 change: 1 addition & 0 deletions examples/backend-adapters/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ that are included here are:
- [Express](./server/src/express.ts)
- [Fastify](./server/src/fastify.ts)
- [Hono](./server/src/hono.ts)
- [H3](./server/src/h3.ts)

You can start the Vite frontend as well as any of your preferred server using
the `pnpm dev:<your-server>` command. The Vite app will then be available on
Expand Down
1 change: 1 addition & 0 deletions examples/backend-adapters/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"dev:express": "concurrently \"npm run -w client dev\" \"npm run -w server dev:express\"",
"dev:fastify": "concurrently \"npm run -w client dev\" \"npm run -w server dev:fastify\"",
"dev:hono": "concurrently \"npm run -w client dev\" \"npm run -w server dev:hono\"",
"dev:h3": "concurrently \"npm run -w client dev\" \"npm run -w server dev:h3\"",
"build": "npm run -w client build && npm run -w server build",
"start": "concurrently \"npm run -w client start\" \"npm run -w server start\""
},
Expand Down
5 changes: 4 additions & 1 deletion examples/backend-adapters/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
"dev:elysia": "NODE_ENV=development bun run --hot src/elysia.ts",
"dev:express": "NODE_ENV=development tsx watch src/express.ts",
"dev:fastify": "NODE_ENV=development tsx watch src/fastify.ts",
"dev:hono": "NODE_ENV=development tsx watch src/hono.ts"
"dev:hono": "NODE_ENV=development tsx watch src/hono.ts",
"dev:h3": "listhen -w src/h3.ts"
},
"dependencies": {
"@hono/node-server": "^1.1.1",
Expand All @@ -15,7 +16,9 @@
"elysia": "^0.6.24",
"express": "^4.18.2",
"fastify": "^4.23.2",
"h3": "^1.8.1",
"hono": "^3.6.3",
"listhen": "^1.5.5",
"uploadthing": "^5.6.1"
},
"devDependencies": {
Expand Down
18 changes: 18 additions & 0 deletions examples/backend-adapters/server/src/h3.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { createApp, createRouter, eventHandler } from "h3";

import { createH3EventHandler } from "uploadthing/h3";

import { uploadRouter } from "./router";

const app = createApp();
const router = createRouter();

router.get(
"/api",
eventHandler(() => "Hello from H3!"),
);
router.use("/api/uploadthing", createH3EventHandler({ router: uploadRouter }));

app.use(router.handler);

export { app };
9 changes: 9 additions & 0 deletions packages/uploadthing/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@
"types": "./dist/fastify.d.ts",
"import": "./dist/fastify.mjs",
"require": "./dist/fastify.js"
},
"./h3": {
"types": "./dist/h3.d.ts",
"import": "./dist/h3.mjs",
"require": "./dist/h3.js"
}
},
"files": [
Expand Down Expand Up @@ -71,6 +76,9 @@
],
"fastify": [
"dist/fastify.d.ts"
],
"h3": [
"dist/h3.d.ts"
]
}
},
Expand All @@ -96,6 +104,7 @@
"eslint": "^8.47.0",
"express": "^4.18.2",
"fastify": "^4.23.2",
"h3": "^1.8.1",
"next": "13.4.4",
"solid-js": "^1.7.11",
"tailwindcss": "^3.3.2",
Expand Down
7 changes: 4 additions & 3 deletions packages/uploadthing/src/express.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,10 @@ export type { FileRouter } from "./internal/types";
export const createUploadthing = <TErrorShape extends Json>(
opts?: CreateBuilderOptions<TErrorShape>,
) =>
createBuilder<{ req: ExpressRequest; res: ExpressResponse }, TErrorShape>(
opts,
);
createBuilder<
{ req: ExpressRequest; res: ExpressResponse; event: undefined },
TErrorShape
>(opts);

export const createUploadthingExpressHandler = <TRouter extends FileRouter>(
opts: RouterWithConfig<TRouter>,
Expand Down
5 changes: 4 additions & 1 deletion packages/uploadthing/src/fastify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ export type { FileRouter } from "./internal/types";
export const createUploadthing = <TErrorShape extends Json>(
opts?: CreateBuilderOptions<TErrorShape>,
) =>
createBuilder<{ req: FastifyRequest; res: FastifyReply }, TErrorShape>(opts);
createBuilder<
{ req: FastifyRequest; res: FastifyReply; event: undefined },
TErrorShape
>(opts);

export const fastifyUploadthingPlugin = <TRouter extends FileRouter>(
fastify: FastifyInstance,
Expand Down
79 changes: 79 additions & 0 deletions packages/uploadthing/src/h3.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import type { H3Event } from "h3";
import {
assertMethod,
defineEventHandler,
getRequestHeaders,
getRequestURL,
readBody,
setHeaders,
setResponseStatus,
} from "h3";

import type { Json } from "@uploadthing/shared";
import { getStatusCodeFromError, UploadThingError } from "@uploadthing/shared";

import { UPLOADTHING_VERSION } from "./constants";
import { defaultErrorFormatter } from "./internal/error-formatter";
import {
buildPermissionsInfoHandler,
buildRequestHandler,
} from "./internal/handler";
import type { RouterWithConfig } from "./internal/handler";
import type { FileRouter } from "./internal/types";
import type { CreateBuilderOptions } from "./internal/upload-builder";
import { createBuilder } from "./internal/upload-builder";

export type { FileRouter } from "./internal/types";

export const createUploadthing = <TErrorShape extends Json>(
opts?: CreateBuilderOptions<TErrorShape>,
) =>
createBuilder<
{ req: undefined; res: undefined; event: H3Event },
TErrorShape
>(opts);

export const createH3EventHandler = <TRouter extends FileRouter>(
opts: RouterWithConfig<TRouter>,
) => {
const requestHandler = buildRequestHandler(opts);
const getBuildPerms = buildPermissionsInfoHandler<TRouter>(opts);

return defineEventHandler(async (event) => {
assertMethod(event, ["GET", "POST"]);
setHeaders(event, { "x-uploadthing-version": UPLOADTHING_VERSION });

// GET
if (event.method === "GET") {
setResponseStatus(event, 200);
juliusmarminge marked this conversation as resolved.
Show resolved Hide resolved
return getBuildPerms();
}

// POST
const response = await requestHandler({
req: {
url: getRequestURL(event).href,
headers: getRequestHeaders(event),
json: () => Promise.resolve(readBody(event)),
},
event,
});

if (response instanceof UploadThingError) {
setResponseStatus(event, getStatusCodeFromError(response));
const errorFormatter =
opts.router[Object.keys(opts.router)[0]]?._def.errorFormatter ??
defaultErrorFormatter;
return errorFormatter(response) as unknown;
}

if (response.status !== 200) {
// We messed up - this should never happen
setResponseStatus(event, 500);
return "An unknown error occurred";
}

setResponseStatus(event, 200);
juliusmarminge marked this conversation as resolved.
Show resolved Hide resolved
return response.body;
});
};
4 changes: 3 additions & 1 deletion packages/uploadthing/src/internal/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,14 +87,15 @@ export const buildRequestHandler = <TRouter extends FileRouter>(
return async (input: {
req: RequestLike;
res?: unknown;
event?: unknown;
}): Promise<
UploadThingError | { status: 200; body?: UploadThingResponse }
> => {
if (process.env.NODE_ENV === "development") {
console.log("[UT] UploadThing dev server is now running!");
}

const { req, res } = input;
const { req, res, event } = input;
const { router, config } = opts;
const preferredOrEnvSecret =
config?.uploadthingSecret ?? process.env.UPLOADTHING_SECRET;
Expand Down Expand Up @@ -206,6 +207,7 @@ export const buildRequestHandler = <TRouter extends FileRouter>(
req: req as any,
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
res: res as any,
event,
input: parsedInput,
});
} catch (error) {
Expand Down
9 changes: 5 additions & 4 deletions packages/uploadthing/src/internal/types.ts
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reminder for myself to look over these types, doesn't make sense having to add an any generic in a bunch of places like this...

Original file line number Diff line number Diff line change
Expand Up @@ -40,24 +40,25 @@ type ResolverOptions<TParams extends AnyParams> = {
file: UploadedFile;
};

export type AnyRuntime = "app" | "pages" | "web" | "express" | "fastify";
export type AnyRuntime = "app" | "pages" | "web" | "express" | "fastify" | "h3";

export type MiddlewareFnArgs<TRequest, TResponse> = {
export type MiddlewareFnArgs<TRequest, TResponse, TEvent> = {
req: TRequest;
res: TResponse;
event: TEvent;
};
export interface AnyParams {
_input: any;
_metadata: any; // imaginary field used to bind metadata return type to an Upload resolver
_middlewareArgs: MiddlewareFnArgs<any, any>;
_middlewareArgs: MiddlewareFnArgs<any, any, any>;
_errorShape: any;
_errorFn: any; // used for onUploadError
}

type MiddlewareFn<
TInput extends JSON | UnsetMarker,
TOutput extends Record<string, unknown>,
TArgs extends MiddlewareFnArgs<any, any>,
TArgs extends MiddlewareFnArgs<any, any, any>,
> = (
opts: TArgs & (TInput extends UnsetMarker ? {} : { input: TInput }),
) => MaybePromise<TOutput>;
Expand Down
Loading
Loading