@uploadista/adapters-express 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/.turbo/turbo-build.log +5 -0
  2. package/.turbo/turbo-check.log +5 -0
  3. package/LICENSE +21 -0
  4. package/README.md +456 -0
  5. package/USAGE.md +164 -0
  6. package/dist/adapter-layer.d.ts +22 -0
  7. package/dist/adapter-layer.d.ts.map +1 -0
  8. package/dist/adapter-layer.js +3 -0
  9. package/dist/error-types.d.ts +24 -0
  10. package/dist/error-types.d.ts.map +1 -0
  11. package/dist/error-types.js +65 -0
  12. package/dist/flow-adapter.d.ts +19 -0
  13. package/dist/flow-adapter.d.ts.map +1 -0
  14. package/dist/flow-adapter.js +80 -0
  15. package/dist/flow-http-handlers.d.ts +9 -0
  16. package/dist/flow-http-handlers.d.ts.map +1 -0
  17. package/dist/flow-http-handlers.js +133 -0
  18. package/dist/http-handlers.d.ts +7 -0
  19. package/dist/http-handlers.d.ts.map +1 -0
  20. package/dist/http-handlers.js +78 -0
  21. package/dist/index.d.ts +4 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.js +2 -0
  24. package/dist/upload-http-handlers.d.ts +9 -0
  25. package/dist/upload-http-handlers.d.ts.map +1 -0
  26. package/dist/upload-http-handlers.js +113 -0
  27. package/dist/uploadista-adapter-layer.d.ts +24 -0
  28. package/dist/uploadista-adapter-layer.d.ts.map +1 -0
  29. package/dist/uploadista-adapter-layer.js +4 -0
  30. package/dist/uploadista-adapter.d.ts +78 -0
  31. package/dist/uploadista-adapter.d.ts.map +1 -0
  32. package/dist/uploadista-adapter.js +297 -0
  33. package/dist/uploadista-websocket-handler.d.ts +9 -0
  34. package/dist/uploadista-websocket-handler.d.ts.map +1 -0
  35. package/dist/uploadista-websocket-handler.js +132 -0
  36. package/dist/websocket-handler.d.ts +8 -0
  37. package/dist/websocket-handler.d.ts.map +1 -0
  38. package/dist/websocket-handler.js +82 -0
  39. package/package.json +40 -0
  40. package/src/error-types.ts +103 -0
  41. package/src/flow-http-handlers.ts +184 -0
  42. package/src/index.ts +14 -0
  43. package/src/upload-http-handlers.ts +186 -0
  44. package/src/uploadista-adapter-layer.ts +32 -0
  45. package/src/uploadista-adapter.ts +626 -0
  46. package/src/uploadista-websocket-handler.ts +209 -0
  47. package/tsconfig.json +11 -0
  48. package/tsconfig.tsbuildinfo +1 -0
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@uploadista/adapters-express",
3
+ "type": "module",
4
+ "version": "0.0.3",
5
+ "description": "Express adapter for Uploadista",
6
+ "license": "MIT",
7
+ "author": "Uploadista",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js",
12
+ "default": "./dist/index.js"
13
+ }
14
+ },
15
+ "dependencies": {
16
+ "effect": "3.18.4",
17
+ "zod": "4.1.12",
18
+ "@uploadista/core": "0.0.3",
19
+ "@uploadista/observability": "0.0.3",
20
+ "@uploadista/server": "0.0.3",
21
+ "@uploadista/event-broadcaster-memory": "0.0.3",
22
+ "@uploadista/event-emitter-websocket": "0.0.3"
23
+ },
24
+ "devDependencies": {
25
+ "@types/express": "^5.0.0",
26
+ "@types/node": "24.8.1",
27
+ "typescript": "5.9.3",
28
+ "@uploadista/typescript-config": "0.0.3"
29
+ },
30
+ "peerDependencies": {
31
+ "express": "^4.0.0 || ^5.0.0"
32
+ },
33
+ "scripts": {
34
+ "build": "tsc -b",
35
+ "format": "biome format --write ./src",
36
+ "lint": "biome lint --write ./src",
37
+ "check": "biome check --write ./src",
38
+ "clean": "rimraf -rf dist && rimraf -rf .turbo && rimraf tsconfig.tsbuildinfo"
39
+ }
40
+ }
@@ -0,0 +1,103 @@
1
+ import type { UploadistaError } from "@uploadista/core/errors";
2
+ import {
3
+ AdapterError,
4
+ BadRequestError as BaseBadRequestError,
5
+ NotFoundError as BaseNotFoundError,
6
+ ValidationError as BaseValidationError,
7
+ createErrorResponseBody,
8
+ createGenericErrorResponseBody,
9
+ createUploadistaErrorResponseBody,
10
+ } from "@uploadista/server";
11
+ import { Effect } from "effect";
12
+ import type { Response } from "express";
13
+
14
+ // Re-export shared error types for backward compatibility
15
+ export {
16
+ AdapterError as ExpressAdapterError,
17
+ BaseValidationError as ValidationError,
18
+ BaseNotFoundError as NotFoundError,
19
+ BaseBadRequestError as BadRequestError,
20
+ };
21
+
22
+ /**
23
+ * Sends error response using Express Response object
24
+ */
25
+ export const sendErrorResponse = (res: Response, error: AdapterError): void => {
26
+ res.status(error.statusCode).json(createErrorResponseBody(error));
27
+ };
28
+
29
+ /**
30
+ * Sends UploadistaError response using Express Response object
31
+ */
32
+ export const sendUploadistaErrorResponse = (
33
+ res: Response,
34
+ error: UploadistaError,
35
+ ): void => {
36
+ res.status(error.status).json(createUploadistaErrorResponseBody(error));
37
+ };
38
+
39
+ /**
40
+ * Sends generic error response using Express Response object
41
+ */
42
+ export const sendGenericErrorResponse = (
43
+ res: Response,
44
+ message = "Internal server error",
45
+ ): void => {
46
+ res.status(500).json(createGenericErrorResponseBody(message));
47
+ };
48
+
49
+ /**
50
+ * Universal error handler that sends error response via Express Response.
51
+ * Handles AdapterError, UploadistaError, and unknown errors.
52
+ * This is the recommended way to handle errors in HTTP handlers.
53
+ */
54
+ export const handleErrorResponse = (res: Response) => (error: unknown) => {
55
+ console.error(error);
56
+
57
+ // Handle known adapter errors
58
+ if (error instanceof AdapterError) {
59
+ return Effect.sync(() => sendErrorResponse(res, error));
60
+ }
61
+
62
+ // Handle UploadistaError
63
+ if (
64
+ typeof error === "object" &&
65
+ error !== null &&
66
+ "code" in error &&
67
+ "status" in error &&
68
+ "body" in error
69
+ ) {
70
+ return Effect.sync(() =>
71
+ sendUploadistaErrorResponse(res, error as UploadistaError),
72
+ );
73
+ }
74
+
75
+ // Handle unknown errors - try to extract what we can
76
+ let message = "Internal server error";
77
+ let code = "UNKNOWN_ERROR";
78
+ let status = 500;
79
+
80
+ if (typeof error === "object" && error !== null) {
81
+ const errorObj = error as Record<string, unknown>;
82
+
83
+ if ("message" in errorObj && typeof errorObj.message === "string") {
84
+ message = errorObj.message;
85
+ }
86
+
87
+ if ("code" in errorObj && typeof errorObj.code === "string") {
88
+ code = errorObj.code;
89
+ }
90
+
91
+ if ("status" in errorObj && typeof errorObj.status === "number") {
92
+ status = errorObj.status;
93
+ }
94
+ }
95
+
96
+ return Effect.sync(() => {
97
+ res.status(status).json({
98
+ error: message,
99
+ code,
100
+ timestamp: new Date().toISOString(),
101
+ });
102
+ });
103
+ };
@@ -0,0 +1,184 @@
1
+ import type { FlowServerShape } from "@uploadista/core/flow";
2
+ import {
3
+ AuthCacheService,
4
+ AuthContextService,
5
+ getLastSegment,
6
+ } from "@uploadista/server";
7
+ import { Effect } from "effect";
8
+ import type { Request, Response } from "express";
9
+ import { handleErrorResponse } from "./error-types";
10
+
11
+ export const handleFlowGet = (
12
+ req: Request,
13
+ res: Response,
14
+ flowServer: FlowServerShape,
15
+ ) => {
16
+ return Effect.gen(function* () {
17
+ // Access auth context if available
18
+ const authService = yield* AuthContextService;
19
+ const clientId = yield* authService.getClientId();
20
+
21
+ const url = new URL(req.url, `http://${req.get("host")}`);
22
+ const id = getLastSegment(url.pathname);
23
+ if (!id) {
24
+ res.status(400).json({ error: "No id" });
25
+ return;
26
+ }
27
+
28
+ const flowData = yield* flowServer.getFlowData(id, clientId);
29
+
30
+ res.status(200).json(flowData);
31
+ }).pipe(Effect.catchAll(handleErrorResponse(res)));
32
+ };
33
+
34
+ export const handleFlowPost = <TRequirements = never>(
35
+ req: Request,
36
+ res: Response,
37
+ flowServer: FlowServerShape,
38
+ ) => {
39
+ return Effect.gen(function* () {
40
+ const authService = yield* AuthContextService;
41
+ const authCache = yield* AuthCacheService;
42
+ const clientId = yield* authService.getClientId();
43
+
44
+ const urlSegments = req.url.split("/");
45
+ const storageId = urlSegments.pop();
46
+ const flowId = urlSegments.pop();
47
+
48
+ if (!flowId) {
49
+ res.status(400).json({ error: "No id" });
50
+ return;
51
+ }
52
+ if (!storageId) {
53
+ res.status(400).json({ error: "No storage id" });
54
+ return;
55
+ }
56
+
57
+ const params = req.body;
58
+
59
+ if (clientId) {
60
+ console.log(
61
+ `[Flow] Executing flow: ${flowId}, storage: ${storageId}, client: ${clientId}`,
62
+ );
63
+ console.log(JSON.stringify(params, null, 2));
64
+ } else {
65
+ console.log(`Flow execution params: ${flowId} ${storageId}`);
66
+ console.log(JSON.stringify(params, null, 2));
67
+ }
68
+
69
+ // Run flow returns immediately with jobId
70
+ const result = yield* flowServer.runFlow<TRequirements>({
71
+ flowId,
72
+ storageId,
73
+ clientId,
74
+ inputs: params.inputs,
75
+ });
76
+
77
+ // Cache auth context for subsequent flow operations (continue, status)
78
+ const authContext = yield* authService.getAuthContext();
79
+ if (authContext) {
80
+ yield* authCache.set(result.id, authContext);
81
+ }
82
+
83
+ if (clientId) {
84
+ console.log(
85
+ `[Flow] Flow started with jobId: ${result.id}, client: ${clientId}`,
86
+ );
87
+ }
88
+
89
+ res.status(200).json(result);
90
+ }).pipe(Effect.catchAll(handleErrorResponse(res)));
91
+ };
92
+
93
+ export const handleJobStatus = (
94
+ req: Request,
95
+ res: Response,
96
+ flowServer: FlowServerShape,
97
+ ): Effect.Effect<void> => {
98
+ return Effect.gen(function* () {
99
+ const urlSegments = req.url.split("/");
100
+ const jobId = urlSegments[urlSegments.length - 2]; // .../jobs/:jobId/status
101
+
102
+ if (!jobId) {
103
+ res.status(400).json({ error: "No job id" });
104
+ return;
105
+ }
106
+
107
+ const result = yield* flowServer.getJobStatus(jobId);
108
+
109
+ res.status(200).json(result);
110
+ }).pipe(Effect.catchAll(handleErrorResponse(res)));
111
+ };
112
+
113
+ export const handleContinueFlow = <TRequirements = never>(
114
+ req: Request,
115
+ res: Response,
116
+ flowServer: FlowServerShape,
117
+ ) => {
118
+ return Effect.gen(function* () {
119
+ const authService = yield* AuthContextService;
120
+ const clientId = yield* authService.getClientId();
121
+
122
+ const url = new URL(req.url, `http://${req.get("host")}`);
123
+ const urlSegments = url.pathname.split("/");
124
+ const jobId = urlSegments[urlSegments.length - 3]; // .../jobs/:jobId/continue/:nodeId
125
+ const nodeId = urlSegments[urlSegments.length - 1]; // .../jobs/:jobId/continue/:nodeId
126
+
127
+ if (!jobId) {
128
+ console.error("No job id");
129
+ res.status(400).json({ error: "No job id" });
130
+ return;
131
+ }
132
+
133
+ if (!nodeId) {
134
+ console.error("No node id");
135
+ res.status(400).json({ error: "No node id" });
136
+ return;
137
+ }
138
+
139
+ const contentType = req.get("Content-Type");
140
+
141
+ let newData: unknown;
142
+
143
+ // Handle different content types
144
+ if (contentType?.includes("application/octet-stream")) {
145
+ // For streaming data, convert Node.js Readable to web ReadableStream
146
+ newData = new ReadableStream({
147
+ start(controller) {
148
+ req.on("data", (chunk) => {
149
+ controller.enqueue(chunk);
150
+ });
151
+ req.on("end", () => {
152
+ controller.close();
153
+ });
154
+ req.on("error", (error) => {
155
+ controller.error(error);
156
+ });
157
+ },
158
+ });
159
+ } else if (contentType?.includes("application/json")) {
160
+ // For JSON data, use the parsed body
161
+ const body = req.body;
162
+
163
+ if (body.newData === undefined) {
164
+ console.error("Missing newData");
165
+ res.status(400).json({ error: "Missing newData" });
166
+ return;
167
+ }
168
+
169
+ newData = body.newData;
170
+ } else {
171
+ res.status(415).json({ error: "Unsupported Content-Type" });
172
+ return;
173
+ }
174
+
175
+ const result = yield* flowServer.continueFlow<TRequirements>({
176
+ jobId,
177
+ nodeId,
178
+ newData,
179
+ clientId,
180
+ });
181
+
182
+ res.status(200).json(result);
183
+ }).pipe(Effect.catchAll(handleErrorResponse(res)));
184
+ };
package/src/index.ts ADDED
@@ -0,0 +1,14 @@
1
+ export * from "./uploadista-adapter";
2
+
3
+ // Export types and utilities for WebSocket integration
4
+ export type {
5
+ ExpressWebSocketHandler,
6
+ WebSocketConnection,
7
+ WebSocketHandlers,
8
+ } from "./uploadista-adapter-layer";
9
+ export {
10
+ createUploadistaWebSocketHandler,
11
+ createWebSocketCloseHandler,
12
+ createWebSocketErrorHandler,
13
+ createWebSocketMessageHandler,
14
+ } from "./uploadista-websocket-handler";
@@ -0,0 +1,186 @@
1
+ import { inputFileSchema } from "@uploadista/core/types";
2
+ import type { UploadServerShape } from "@uploadista/core/upload";
3
+ import { MetricsService } from "@uploadista/observability";
4
+ import { AuthCacheService, AuthContextService } from "@uploadista/server";
5
+ import { Effect } from "effect";
6
+ import type { Request, Response } from "express";
7
+ import {
8
+ BadRequestError,
9
+ handleErrorResponse,
10
+ ValidationError,
11
+ } from "./error-types";
12
+
13
+ export const handleUploadPost = (
14
+ req: Request,
15
+ res: Response,
16
+ server: UploadServerShape,
17
+ ) =>
18
+ Effect.gen(function* () {
19
+ // Access auth context if available
20
+ const authService = yield* AuthContextService;
21
+ const authCache = yield* AuthCacheService;
22
+ const clientId = yield* authService.getClientId();
23
+
24
+ if (clientId) {
25
+ console.log(`[Upload] Creating upload for client: ${clientId}`);
26
+ }
27
+
28
+ const json = req.body;
29
+
30
+ if (!json) {
31
+ return yield* Effect.fail(new BadRequestError("Invalid JSON payload"));
32
+ }
33
+
34
+ const parsedInputFile = yield* Effect.sync(() =>
35
+ inputFileSchema.safeParse(json),
36
+ );
37
+
38
+ if (!parsedInputFile.success) {
39
+ return yield* Effect.fail(
40
+ new ValidationError("Invalid input file schema"),
41
+ );
42
+ }
43
+
44
+ const fileCreated = yield* server.createUpload(
45
+ parsedInputFile.data,
46
+ clientId,
47
+ );
48
+
49
+ // Cache auth context for subsequent chunk uploads
50
+ const authContext = yield* authService.getAuthContext();
51
+ if (authContext) {
52
+ yield* authCache.set(fileCreated.id, authContext);
53
+ }
54
+
55
+ if (clientId) {
56
+ console.log(
57
+ `[Upload] Upload created: ${fileCreated.id} for client: ${clientId}`,
58
+ );
59
+ }
60
+
61
+ res.status(200).json(fileCreated);
62
+ }).pipe(Effect.catchAll(handleErrorResponse(res)));
63
+
64
+ export const handleUploadGet = (
65
+ req: Request,
66
+ res: Response,
67
+ server: UploadServerShape,
68
+ ) =>
69
+ Effect.gen(function* () {
70
+ const authService = yield* AuthContextService;
71
+ const clientId = yield* authService.getClientId();
72
+
73
+ const url = new URL(req.url, `http://${req.get("host")}`);
74
+ const pathSegments = url.pathname.split("/").filter(Boolean);
75
+ const lastSegment = pathSegments[pathSegments.length - 1];
76
+
77
+ if (lastSegment === "capabilities") {
78
+ const storageId =
79
+ url.searchParams.get("storageId") ||
80
+ pathSegments[pathSegments.length - 2];
81
+
82
+ if (!storageId) {
83
+ return yield* Effect.fail(
84
+ new BadRequestError("storageId is required for capabilities"),
85
+ );
86
+ }
87
+
88
+ const capabilities = yield* server.getCapabilities(storageId, clientId);
89
+
90
+ res.status(200).json({
91
+ storageId,
92
+ capabilities,
93
+ timestamp: new Date().toISOString(),
94
+ });
95
+ return;
96
+ }
97
+
98
+ if (!lastSegment) {
99
+ return yield* Effect.fail(new BadRequestError("Upload ID is required"));
100
+ }
101
+
102
+ const fileResult = yield* server.getUpload(lastSegment);
103
+
104
+ res.status(200).json(fileResult);
105
+ }).pipe(Effect.catchAll(handleErrorResponse(res)));
106
+
107
+ export const handleUploadPatch = (
108
+ req: Request,
109
+ res: Response,
110
+ server: UploadServerShape,
111
+ ) =>
112
+ Effect.gen(function* () {
113
+ // Try to get auth from current request or cached auth
114
+ const authService = yield* AuthContextService;
115
+ const authCache = yield* AuthCacheService;
116
+ const metricsService = yield* MetricsService;
117
+
118
+ const uploadId = req.url.split("/").pop();
119
+ if (!uploadId) {
120
+ return yield* Effect.fail(new BadRequestError("Upload ID is required"));
121
+ }
122
+
123
+ // Try current auth first, fallback to cached auth
124
+ let clientId = yield* authService.getClientId();
125
+ let authMetadata = yield* authService.getMetadata();
126
+ if (!clientId) {
127
+ const cachedAuth = yield* authCache.get(uploadId);
128
+ clientId = cachedAuth?.clientId ?? null;
129
+ authMetadata = cachedAuth?.metadata ?? {};
130
+ }
131
+
132
+ if (clientId) {
133
+ console.log(
134
+ `[Upload] Uploading chunk for upload: ${uploadId}, client: ${clientId}`,
135
+ );
136
+ }
137
+
138
+ // Convert Node.js Readable stream to web ReadableStream
139
+ const body = new ReadableStream({
140
+ start(controller) {
141
+ req.on("data", (chunk) => {
142
+ controller.enqueue(chunk);
143
+ });
144
+ req.on("end", () => {
145
+ controller.close();
146
+ });
147
+ req.on("error", (error) => {
148
+ controller.error(error);
149
+ });
150
+ },
151
+ });
152
+
153
+ const fileResult = yield* server.uploadChunk(uploadId, clientId, body);
154
+
155
+ // Clear cache and record metrics if upload is complete
156
+ if (fileResult.size && fileResult.offset >= fileResult.size) {
157
+ yield* authCache.delete(uploadId);
158
+ if (clientId) {
159
+ console.log(
160
+ `[Upload] Upload completed, cleared auth cache: ${uploadId}`,
161
+ );
162
+ }
163
+
164
+ // Record upload metrics if we have organization ID
165
+ if (clientId && fileResult.size) {
166
+ console.log(
167
+ `[Upload] Recording metrics for org: ${clientId}, size: ${fileResult.size}`,
168
+ );
169
+ yield* Effect.forkDaemon(
170
+ metricsService.recordUpload(clientId, fileResult.size, authMetadata),
171
+ );
172
+ } else {
173
+ console.warn(
174
+ `[Upload] Cannot record metrics - missing organizationId or size`,
175
+ );
176
+ }
177
+ }
178
+
179
+ if (clientId) {
180
+ console.log(
181
+ `[Upload] Chunk uploaded for upload: ${uploadId}, client: ${clientId}`,
182
+ );
183
+ }
184
+
185
+ res.status(200).json(fileResult);
186
+ }).pipe(Effect.catchAll(handleErrorResponse(res)));
@@ -0,0 +1,32 @@
1
+ import type { IncomingMessage } from "node:http";
2
+ import { type Effect, Context as EffectContext } from "effect";
3
+ import type { Request, Response } from "express";
4
+
5
+ export interface WebSocketConnection {
6
+ id: string;
7
+ send: (data: string) => void;
8
+ close: (code?: number, reason?: string) => void;
9
+ readyState: number;
10
+ }
11
+
12
+ export type WebSocketHandlers = {
13
+ onMessage: (message: string) => void;
14
+ onClose: () => void;
15
+ onError: (error: Error) => void;
16
+ };
17
+
18
+ export type ExpressWebSocketHandler = (
19
+ req: IncomingMessage,
20
+ connection: WebSocketConnection,
21
+ ) => WebSocketHandlers;
22
+
23
+ // Define the Uploadista adapter service interface
24
+ export type ExpressUploadistaAdapterServiceShape = {
25
+ handler: (req: Request, res: Response) => Effect.Effect<void, never, never>;
26
+ websocketHandler: ExpressWebSocketHandler;
27
+ };
28
+
29
+ // Context Tag for the Uploadista adapter service
30
+ export class ExpressUploadistaAdapterService extends EffectContext.Tag(
31
+ "ExpressUploadistaAdapterService",
32
+ )<ExpressUploadistaAdapterService, ExpressUploadistaAdapterServiceShape>() {}