Skip to content

Commit

Permalink
feat: @effect/platform adapter (#861)
Browse files Browse the repository at this point in the history
Co-authored-by: Davletov Almir <davletovalmir@gmail.com>
Co-authored-by: Mark R. Florkowski <mark.florkowski@gmail.com>
  • Loading branch information
3 people authored Jun 24, 2024
1 parent 0862a6d commit 78c755d
Show file tree
Hide file tree
Showing 9 changed files with 284 additions and 83 deletions.
5 changes: 5 additions & 0 deletions .changeset/five-phones-hammer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"uploadthing": minor
---

feat: `@effect/platform` adapter
43 changes: 41 additions & 2 deletions docs/src/pages/api-reference/server.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ The helper function to create an UploadThing instance. MAKE SURE YOU IMPORT IT
FROM THE RIGHT PLACE. The export name ensures your file routes' `middleware`
functions are typed correctly.

<Tabs items={["Next App Router", "Next Pages Dir", "SolidJS", "Express", "Fastify", "H3"]}>
<Tabs items={["Next App Router", "Next Pages Dir", "SolidJS", "Effect Platform","Express", "Fastify", "H3"]}>
<Tab>
```ts
import { createUploadthing, type FileRouter } from "uploadthing/next";
Expand Down Expand Up @@ -51,6 +51,22 @@ functions are typed correctly.
})
```

</Tab>

<Tab>
```ts
import { createUploadthing, type FileRouter } from "uploadthing/effect-platform";

const f = createUploadthing();
export const uploadRouter = { ... };

// ...
f({ ... })
.middleware(({ req }) => {
// ^? req: HttpServer.request.ServerRequest
})
```

</Tab>

<Tab>
Expand Down Expand Up @@ -305,7 +321,7 @@ function, although there are some extra configuration options available.

> The names of the exported `createRouteHandler` is different prior to `v6.3`.
<Tabs items={["Next App Router", "Next Pages Dir", "SolidJS", "Express", "Fastify", "H3"]}>
<Tabs items={["Next App Router", "Next Pages Dir", "SolidJS", "Effect Platform", "Express", "Fastify", "H3"]}>
<Tab>
```ts
import { createRouteHandler } from "uploadthing/next";
Expand Down Expand Up @@ -341,6 +357,29 @@ function, although there are some extra configuration options available.
});
```

</Tab>
<Tab>
```ts
import * as Http from "@effect/platform/HttpServer";
import { createRouteHandler } from "uploadthing/effect-platform";
import { uploadRouter } from "~/server/uploadthing.ts";

const router = Http.router.empty.pipe(
/** Your other routes */
Http.router.mount("/api/uploadthing", createRouteHandler({
router: uploadRouter,
config: { ... },
})),
);

const app = router.pipe(
Http.server.serve(Http.middleware.logger),
Http.server.withLogAddress,
);

// ...
```

</Tab>

<Tab>
Expand Down
6 changes: 3 additions & 3 deletions examples/backend-adapters/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@
"dev:fastify": "NODE_ENV=development tsx watch src/fastify.ts",
"dev:hono": "NODE_ENV=development tsx watch src/hono.ts",
"dev:h3": "NODE_ENV=development listhen -w src/h3.ts",
"dev:effect": "NODE_ENV=development bun run src/effect.ts"
"dev:effect": "NODE_ENV=development tsx watch src/effect-platform.ts"
},
"dependencies": {
"@effect/platform": "^0.57.1",
"@effect/platform-bun": "^0.36.10",
"@effect/platform": "^0.57.2",
"@effect/platform-node": "^0.51.11",
"@elysiajs/cors": "^0.8.0",
"@fastify/cors": "^9.0.1",
"@hono/node-server": "^1.8.2",
Expand Down
61 changes: 61 additions & 0 deletions examples/backend-adapters/server/src/effect-platform.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import "dotenv/config";

import { createServer } from "node:http";
import { NodeHttpServer, NodeRuntime } from "@effect/platform-node";
import * as Http from "@effect/platform/HttpServer";
import { Config, Effect, Layer } from "effect";

import { createRouteHandler } from "uploadthing/effect-platform";

import { uploadRouter } from "./router";

const uploadthingRouter = createRouteHandler({
router: uploadRouter,
});

/**
* Simple CORS middleware that allows everything
* Adjust to your needs.
*/
const cors = Http.middleware.make((app) =>
Effect.gen(function* () {
const req = yield* Http.request.ServerRequest;

const corsHeaders = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "*",
"Access-Control-Allow-Headers": "*",
};

if (req.method === "OPTIONS") {
return Http.response.empty({
status: 204,
headers: Http.headers.fromInput(corsHeaders),
});
}

const response = yield* app;

return response.pipe(Http.response.setHeaders(corsHeaders));
}),
);

const router = Http.router.empty.pipe(
Http.router.get("/api", Http.response.text("Hello from Effect")),
Http.router.mount("/api/uploadthing", uploadthingRouter),
);

const app = router.pipe(
cors,
Http.server.serve(Http.middleware.logger),
Http.server.withLogAddress,
);

const Port = Config.integer("PORT").pipe(Config.withDefault(3000));
const ServerLive = Layer.unwrapEffect(
Effect.map(Port, (port) =>
NodeHttpServer.server.layer(() => createServer(), { port }),
),
);

NodeRuntime.runMain(Layer.launch(Layer.provide(app, ServerLive)));
46 changes: 0 additions & 46 deletions examples/backend-adapters/server/src/effect.ts

This file was deleted.

16 changes: 16 additions & 0 deletions packages/uploadthing/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,16 @@
"default": "./next-legacy/index.cjs"
}
},
"./effect-platform": {
"import": {
"types": "./effect-platform/index.d.ts",
"default": "./effect-platform/index.js"
},
"require": {
"types": "./effect-platform/index.d.cts",
"default": "./effect-platform/index.cjs"
}
},
"./tw": {
"import": {
"types": "./tw/index.d.ts",
Expand Down Expand Up @@ -106,6 +116,7 @@
},
"files": [
"client",
"effect-platform",
"express",
"fastify",
"h3",
Expand Down Expand Up @@ -137,6 +148,7 @@
"std-env": "^3.7.0"
},
"devDependencies": {
"@effect/platform": "^0.57.2",
"@types/body-parser": "^1.19.5",
"@types/express": "^4.17.21",
"@types/express-serve-static-core": "^4.17.43",
Expand All @@ -161,13 +173,17 @@
"zod": "^3.23.8"
},
"peerDependencies": {
"@effect/platform": "*",
"express": "*",
"fastify": "*",
"h3": "*",
"next": "*",
"tailwindcss": "*"
},
"peerDependenciesMeta": {
"@effect/platform": {
"optional": true
},
"next": {
"optional": true
},
Expand Down
104 changes: 104 additions & 0 deletions packages/uploadthing/src/effect-platform.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import * as Http from "@effect/platform/HttpServer";
import * as Effect from "effect/Effect";

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

import { UPLOADTHING_VERSION } from "./internal/constants";
import { formatError } from "./internal/error-formatter";
import {
buildPermissionsInfoHandler,
buildRequestHandler,
} from "./internal/handler";
import { incompatibleNodeGuard } from "./internal/incompat-node-guard";
import { toWebRequest } from "./internal/to-web-request";
import type { FileRouter, RouteHandlerOptions } from "./internal/types";
import type { CreateBuilderOptions } from "./internal/upload-builder";
import { createBuilder } from "./internal/upload-builder";

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

type MiddlewareArgs = {
req: Http.request.ServerRequest;
res: undefined;
event: undefined;
};

export const createUploadthing = <TErrorShape extends Json>(
opts?: CreateBuilderOptions<TErrorShape>,
) => createBuilder<MiddlewareArgs, TErrorShape>(opts);

export const createRouteHandler = <TRouter extends FileRouter>(
opts: RouteHandlerOptions<TRouter>,
): Http.router.Router<Http.body.BodyError, never> => {
incompatibleNodeGuard();

const requestHandler = buildRequestHandler<TRouter, MiddlewareArgs>(
opts,
"effect-platform",
);
const getBuildPerms = buildPermissionsInfoHandler<TRouter>(opts);

const appendUploadThingResponseHeaders = Http.middleware.make(
Effect.map(
Http.response.setHeader("x-uploadthing-version", UPLOADTHING_VERSION),
),
);

return Http.router.empty.pipe(
Http.router.get("/", Http.response.json(getBuildPerms())),
Http.router.post(
"/",
Effect.flatMap(Http.request.ServerRequest, (req) =>
requestHandler({
/**
* TODO: Redo this to be more cross-platform
* This should handle WinterCG and Node.js runtimes,
* unsure about others...
* Perhaps we can use `Http.request.ServerRequest` internally?
*/
req: Effect.if(req.source instanceof Request, {
onTrue: () => Effect.succeed(req.source as Request),
onFalse: () =>
req.json.pipe(
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
Effect.flatMap((body) => toWebRequest(req.source as any, body)),
Effect.catchTags({
RequestError: (error) =>
new UploadThingError({
code: "BAD_REQUEST",
message: "INVALID_JSON",
cause: error,
}),
}),
),
}),
middlewareArgs: { req, res: undefined, event: undefined },
}).pipe(
Effect.provideService(FetchContext, {
fetch: opts.config?.fetch ?? globalThis.fetch,
baseHeaders: {
"x-uploadthing-version": UPLOADTHING_VERSION,
// These are filled in later in `parseAndValidateRequest`
"x-uploadthing-api-key": undefined,
"x-uploadthing-be-adapter": undefined,
"x-uploadthing-fe-package": undefined,
},
}),
Effect.andThen((response) => Http.response.json(response.body)),
Effect.catchTag("UploadThingError", (error) =>
Http.response.json(formatError(error, opts.router), {
status: getStatusCodeFromError(error),
}),
),
),
),
),
Http.router.use(appendUploadThingResponseHeaders),
);
};
1 change: 1 addition & 0 deletions packages/uploadthing/turbo.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"build": {
"outputs": [
"client/**",
"effect-platform/**",
"express/**",
"fastify/**",
"h3/**",
Expand Down
Loading

0 comments on commit 78c755d

Please sign in to comment.