@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.
- package/.turbo/turbo-build.log +5 -0
- package/.turbo/turbo-check.log +5 -0
- package/LICENSE +21 -0
- package/README.md +456 -0
- package/USAGE.md +164 -0
- package/dist/adapter-layer.d.ts +22 -0
- package/dist/adapter-layer.d.ts.map +1 -0
- package/dist/adapter-layer.js +3 -0
- package/dist/error-types.d.ts +24 -0
- package/dist/error-types.d.ts.map +1 -0
- package/dist/error-types.js +65 -0
- package/dist/flow-adapter.d.ts +19 -0
- package/dist/flow-adapter.d.ts.map +1 -0
- package/dist/flow-adapter.js +80 -0
- package/dist/flow-http-handlers.d.ts +9 -0
- package/dist/flow-http-handlers.d.ts.map +1 -0
- package/dist/flow-http-handlers.js +133 -0
- package/dist/http-handlers.d.ts +7 -0
- package/dist/http-handlers.d.ts.map +1 -0
- package/dist/http-handlers.js +78 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/upload-http-handlers.d.ts +9 -0
- package/dist/upload-http-handlers.d.ts.map +1 -0
- package/dist/upload-http-handlers.js +113 -0
- package/dist/uploadista-adapter-layer.d.ts +24 -0
- package/dist/uploadista-adapter-layer.d.ts.map +1 -0
- package/dist/uploadista-adapter-layer.js +4 -0
- package/dist/uploadista-adapter.d.ts +78 -0
- package/dist/uploadista-adapter.d.ts.map +1 -0
- package/dist/uploadista-adapter.js +297 -0
- package/dist/uploadista-websocket-handler.d.ts +9 -0
- package/dist/uploadista-websocket-handler.d.ts.map +1 -0
- package/dist/uploadista-websocket-handler.js +132 -0
- package/dist/websocket-handler.d.ts +8 -0
- package/dist/websocket-handler.d.ts.map +1 -0
- package/dist/websocket-handler.js +82 -0
- package/package.json +40 -0
- package/src/error-types.ts +103 -0
- package/src/flow-http-handlers.ts +184 -0
- package/src/index.ts +14 -0
- package/src/upload-http-handlers.ts +186 -0
- package/src/uploadista-adapter-layer.ts +32 -0
- package/src/uploadista-adapter.ts +626 -0
- package/src/uploadista-websocket-handler.ts +209 -0
- package/tsconfig.json +11 -0
- 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>() {}
|