@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/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 uploadista
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
# @uploadista/adapters-express
|
|
2
|
+
|
|
3
|
+
Uploadista adapter for Express - Run upload servers on Node.js with Express.
|
|
4
|
+
|
|
5
|
+
Provides a complete file upload and flow processing server for Express with manual WebSocket setup, authentication middleware, and support for standard Node.js hosting (Heroku, Railway, VPS, etc.).
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Node.js Compatible** - Run on any Node.js 18+ environment
|
|
10
|
+
- **Express Middleware** - Integrates with Express request/response patterns
|
|
11
|
+
- **WebSocket Support** - Use `ws` package for real-time progress
|
|
12
|
+
- **Authentication** - Flexible middleware for JWT or custom auth
|
|
13
|
+
- **Multi-Cloud Storage** - S3, Azure, GCS, or filesystem
|
|
14
|
+
- **Redis Support** - Distributed deployments with Redis KV store
|
|
15
|
+
- **TypeScript** - Full type safety with comprehensive JSDoc
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install @uploadista/adapters-express express ws
|
|
21
|
+
# or
|
|
22
|
+
pnpm add @uploadista/adapters-express express ws
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Requirements
|
|
26
|
+
|
|
27
|
+
- Node.js 18+
|
|
28
|
+
- Express 4.0 or 5.0+
|
|
29
|
+
- TypeScript 5.0+ (optional but recommended)
|
|
30
|
+
|
|
31
|
+
## Quick Start
|
|
32
|
+
|
|
33
|
+
### 1. Basic Express Server
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
import express from "express";
|
|
37
|
+
import { createExpressUploadistaAdapter } from "@uploadista/adapters-express";
|
|
38
|
+
import { redisKvStore } from "@uploadista/kv-store-redis";
|
|
39
|
+
import { s3DataStore } from "@uploadista/data-store-s3";
|
|
40
|
+
import { webSocketEventEmitter } from "@uploadista/event-emitter-websocket";
|
|
41
|
+
import { memoryEventBroadcaster } from "@uploadista/event-broadcaster-memory";
|
|
42
|
+
|
|
43
|
+
const app = express();
|
|
44
|
+
|
|
45
|
+
// Create adapter
|
|
46
|
+
const adapter = await createExpressUploadistaAdapter({
|
|
47
|
+
baseUrl: "uploadista",
|
|
48
|
+
dataStore: s3DataStore,
|
|
49
|
+
kvStore: redisKvStore,
|
|
50
|
+
eventEmitter: webSocketEventEmitter,
|
|
51
|
+
eventBroadcaster: memoryEventBroadcaster,
|
|
52
|
+
flows: (flowId, clientId) => createFlowsEffect(flowId, clientId),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Mount HTTP handler
|
|
56
|
+
app.use(`/${adapter.baseUrl}`, (req, res) => {
|
|
57
|
+
adapter.handler(req, res);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// WebSocket server
|
|
61
|
+
import http from "http";
|
|
62
|
+
import WebSocket from "ws";
|
|
63
|
+
|
|
64
|
+
const server = http.createServer(app);
|
|
65
|
+
const wss = new WebSocket.Server({ server });
|
|
66
|
+
|
|
67
|
+
wss.on("connection", (ws, req) => {
|
|
68
|
+
adapter.websocketConnectionHandler(ws, req);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
server.listen(3000, () => {
|
|
72
|
+
console.log("Server running on http://localhost:3000");
|
|
73
|
+
});
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### 2. Add Authentication
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
const adapter = await createExpressUploadistaAdapter({
|
|
80
|
+
baseUrl: "uploadista",
|
|
81
|
+
dataStore: s3DataStore,
|
|
82
|
+
kvStore: redisKvStore,
|
|
83
|
+
authMiddleware: async (req, res) => {
|
|
84
|
+
const token = req.headers.authorization?.split(" ")[1];
|
|
85
|
+
if (!token) return null;
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
const payload = await verifyToken(token);
|
|
89
|
+
return {
|
|
90
|
+
clientId: payload.sub,
|
|
91
|
+
permissions: payload.permissions,
|
|
92
|
+
};
|
|
93
|
+
} catch {
|
|
94
|
+
res.status(401).json({ error: "Unauthorized" });
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
authCacheConfig: { maxSize: 5000, ttl: 3600000 },
|
|
99
|
+
});
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### 3. Express Middleware Setup
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
import express, { Request, Response, NextFunction } from "express";
|
|
106
|
+
|
|
107
|
+
const app = express();
|
|
108
|
+
|
|
109
|
+
// Body parser for JSON
|
|
110
|
+
app.use(express.json({ limit: "50mb" }));
|
|
111
|
+
|
|
112
|
+
// CORS
|
|
113
|
+
app.use(cors({
|
|
114
|
+
origin: process.env.ALLOWED_ORIGINS?.split(","),
|
|
115
|
+
allowedHeaders: ["Content-Type", "Authorization"],
|
|
116
|
+
}));
|
|
117
|
+
|
|
118
|
+
// Logging
|
|
119
|
+
app.use((req, res, next) => {
|
|
120
|
+
console.log(`${req.method} ${req.path}`);
|
|
121
|
+
next();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// Mount adapter
|
|
125
|
+
app.use(`/${adapter.baseUrl}`, (req: Request, res: Response) => {
|
|
126
|
+
adapter.handler(req, res);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// Error handler
|
|
130
|
+
app.use((err: any, req: Request, res: Response, next: NextFunction) => {
|
|
131
|
+
console.error(err);
|
|
132
|
+
res.status(500).json({ error: "Internal server error" });
|
|
133
|
+
});
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Configuration
|
|
137
|
+
|
|
138
|
+
### `ExpressUploadistaAdapterOptions`
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
type ExpressUploadistaAdapterOptions = {
|
|
142
|
+
// Required
|
|
143
|
+
flows: (flowId: string, clientId: string | null) =>
|
|
144
|
+
Effect.Effect<unknown, unknown, unknown>;
|
|
145
|
+
dataStore: Layer.Layer<UploadFileDataStores, never, UploadFileKVStore>;
|
|
146
|
+
kvStore: Layer.Layer<BaseKvStoreService>;
|
|
147
|
+
|
|
148
|
+
// Optional
|
|
149
|
+
baseUrl?: string; // Default: "uploadista"
|
|
150
|
+
eventEmitter?: Layer.Layer<BaseEventEmitterService>;
|
|
151
|
+
eventBroadcaster?: Layer.Layer<any>;
|
|
152
|
+
generateId?: Layer.Layer<GenerateId>;
|
|
153
|
+
authMiddleware?: (
|
|
154
|
+
req: IncomingMessage,
|
|
155
|
+
res: ServerResponse,
|
|
156
|
+
) => Promise<AuthResult>;
|
|
157
|
+
authCacheConfig?: AuthCacheConfig;
|
|
158
|
+
bufferedDataStore?: Layer.Layer<UploadFileDataStore>;
|
|
159
|
+
};
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## API Routes
|
|
163
|
+
|
|
164
|
+
Routes are available at `/{baseUrl}/api/`:
|
|
165
|
+
|
|
166
|
+
```
|
|
167
|
+
POST /uploadista/api/upload
|
|
168
|
+
Create new upload
|
|
169
|
+
|
|
170
|
+
GET /uploadista/api/upload/:uploadId
|
|
171
|
+
Get upload status
|
|
172
|
+
|
|
173
|
+
PATCH /uploadista/api/upload/:uploadId
|
|
174
|
+
Upload chunk
|
|
175
|
+
|
|
176
|
+
POST /uploadista/api/flow/:flowId/:storageId
|
|
177
|
+
Execute flow
|
|
178
|
+
|
|
179
|
+
GET /uploadista/api/jobs/:jobId/status
|
|
180
|
+
Get job status
|
|
181
|
+
|
|
182
|
+
PATCH /uploadista/api/jobs/:jobId/continue/:nodeId
|
|
183
|
+
Continue flow
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## WebSocket Integration
|
|
187
|
+
|
|
188
|
+
### Using `ws` Package
|
|
189
|
+
|
|
190
|
+
```typescript
|
|
191
|
+
import WebSocket from "ws";
|
|
192
|
+
import http from "http";
|
|
193
|
+
|
|
194
|
+
const server = http.createServer(app);
|
|
195
|
+
const wss = new WebSocket.Server({ server });
|
|
196
|
+
|
|
197
|
+
// Handle WebSocket connections
|
|
198
|
+
wss.on("connection", (ws, req) => {
|
|
199
|
+
// Pass to adapter
|
|
200
|
+
adapter.websocketConnectionHandler(ws, req);
|
|
201
|
+
|
|
202
|
+
ws.on("message", (data) => {
|
|
203
|
+
// Adapter handles messages internally
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
ws.on("close", () => {
|
|
207
|
+
console.log("Client disconnected");
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
server.listen(3000);
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### Using Socket.io (Alternative)
|
|
215
|
+
|
|
216
|
+
```typescript
|
|
217
|
+
import { Server } from "socket.io";
|
|
218
|
+
|
|
219
|
+
const io = new Server(server, {
|
|
220
|
+
cors: { origin: "*" },
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
io.on("connection", (socket) => {
|
|
224
|
+
socket.on("subscribe", (channels) => {
|
|
225
|
+
channels.forEach((ch) => socket.join(ch));
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
socket.on("disconnect", () => {
|
|
229
|
+
console.log("Client disconnected");
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
// Emit events from adapter
|
|
234
|
+
const eventEmitter = /* ... */;
|
|
235
|
+
eventEmitter.on("upload:progress", (data) => {
|
|
236
|
+
io.emit(`upload:${data.uploadId}`, data);
|
|
237
|
+
});
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
## Complete Server Example
|
|
241
|
+
|
|
242
|
+
```typescript
|
|
243
|
+
import express, { Express, Request, Response } from "express";
|
|
244
|
+
import http from "http";
|
|
245
|
+
import WebSocket from "ws";
|
|
246
|
+
import cors from "cors";
|
|
247
|
+
import { createExpressUploadistaAdapter } from "@uploadista/adapters-express";
|
|
248
|
+
import { redisKvStore } from "@uploadista/kv-store-redis";
|
|
249
|
+
import { s3DataStore } from "@uploadista/data-store-s3";
|
|
250
|
+
import { webSocketEventEmitter } from "@uploadista/event-emitter-websocket";
|
|
251
|
+
import { memoryEventBroadcaster } from "@uploadista/event-broadcaster-memory";
|
|
252
|
+
import { verify } from "jsonwebtoken";
|
|
253
|
+
|
|
254
|
+
const app: Express = express();
|
|
255
|
+
|
|
256
|
+
// Middleware
|
|
257
|
+
app.use(express.json({ limit: "50mb" }));
|
|
258
|
+
app.use(cors());
|
|
259
|
+
|
|
260
|
+
// Health check
|
|
261
|
+
app.get("/health", (req: Request, res: Response) => {
|
|
262
|
+
res.json({ status: "ok" });
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// Create adapter
|
|
266
|
+
const adapter = await createExpressUploadistaAdapter({
|
|
267
|
+
baseUrl: "uploadista",
|
|
268
|
+
dataStore: s3DataStore,
|
|
269
|
+
kvStore: redisKvStore,
|
|
270
|
+
eventEmitter: webSocketEventEmitter,
|
|
271
|
+
eventBroadcaster: memoryEventBroadcaster,
|
|
272
|
+
flows: createFlowsEffect,
|
|
273
|
+
authMiddleware: async (req, res) => {
|
|
274
|
+
const token = req.headers.authorization?.split(" ")[1];
|
|
275
|
+
if (!token) return null;
|
|
276
|
+
|
|
277
|
+
try {
|
|
278
|
+
const payload = verify(token, process.env.JWT_SECRET!);
|
|
279
|
+
return {
|
|
280
|
+
clientId: (payload as any).sub,
|
|
281
|
+
permissions: (payload as any).permissions || [],
|
|
282
|
+
};
|
|
283
|
+
} catch {
|
|
284
|
+
res.statusCode = 401;
|
|
285
|
+
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
},
|
|
289
|
+
authCacheConfig: { maxSize: 5000, ttl: 3600000 },
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
// Mount adapter
|
|
293
|
+
app.use(`/${adapter.baseUrl}`, (req: Request, res: Response) => {
|
|
294
|
+
adapter.handler(req, res);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
// Error handler
|
|
298
|
+
app.use((err: any, req: Request, res: Response) => {
|
|
299
|
+
console.error(err);
|
|
300
|
+
res.status(500).json({ error: "Internal server error" });
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
// HTTP + WebSocket server
|
|
304
|
+
const server = http.createServer(app);
|
|
305
|
+
const wss = new WebSocket.Server({ server });
|
|
306
|
+
|
|
307
|
+
wss.on("connection", (ws, req) => {
|
|
308
|
+
adapter.websocketConnectionHandler(ws, req);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
const PORT = process.env.PORT || 3000;
|
|
312
|
+
server.listen(PORT, () => {
|
|
313
|
+
console.log(`Server running on http://localhost:${PORT}`);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
export default server;
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
## Environment Configuration
|
|
320
|
+
|
|
321
|
+
### .env File
|
|
322
|
+
|
|
323
|
+
```env
|
|
324
|
+
NODE_ENV=production
|
|
325
|
+
PORT=3000
|
|
326
|
+
|
|
327
|
+
# AWS S3
|
|
328
|
+
AWS_ACCESS_KEY_ID=your-key
|
|
329
|
+
AWS_SECRET_ACCESS_KEY=your-secret
|
|
330
|
+
AWS_REGION=us-east-1
|
|
331
|
+
S3_BUCKET=uploads-prod
|
|
332
|
+
|
|
333
|
+
# Redis (for KV store and events)
|
|
334
|
+
REDIS_URL=redis://localhost:6379
|
|
335
|
+
|
|
336
|
+
# JWT Authentication
|
|
337
|
+
JWT_SECRET=your-jwt-secret
|
|
338
|
+
|
|
339
|
+
# CORS
|
|
340
|
+
ALLOWED_ORIGINS=https://app.example.com,https://dashboard.example.com
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
## Docker Deployment
|
|
344
|
+
|
|
345
|
+
### Dockerfile
|
|
346
|
+
|
|
347
|
+
```dockerfile
|
|
348
|
+
FROM node:20-alpine
|
|
349
|
+
WORKDIR /app
|
|
350
|
+
|
|
351
|
+
COPY package*.json ./
|
|
352
|
+
RUN npm ci --only=production
|
|
353
|
+
|
|
354
|
+
COPY dist ./dist
|
|
355
|
+
|
|
356
|
+
ENV NODE_ENV=production
|
|
357
|
+
EXPOSE 3000
|
|
358
|
+
|
|
359
|
+
CMD ["node", "dist/server.js"]
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
### docker-compose.yml
|
|
363
|
+
|
|
364
|
+
```yaml
|
|
365
|
+
version: "3.8"
|
|
366
|
+
services:
|
|
367
|
+
app:
|
|
368
|
+
build: .
|
|
369
|
+
ports:
|
|
370
|
+
- "3000:3000"
|
|
371
|
+
environment:
|
|
372
|
+
REDIS_URL: redis://redis:6379
|
|
373
|
+
JWT_SECRET: ${JWT_SECRET}
|
|
374
|
+
AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID}
|
|
375
|
+
AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY}
|
|
376
|
+
depends_on:
|
|
377
|
+
- redis
|
|
378
|
+
|
|
379
|
+
redis:
|
|
380
|
+
image: redis:7-alpine
|
|
381
|
+
ports:
|
|
382
|
+
- "6379:6379"
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
## Request Examples
|
|
386
|
+
|
|
387
|
+
### Create Upload
|
|
388
|
+
|
|
389
|
+
```bash
|
|
390
|
+
curl -X POST http://localhost:3000/uploadista/api/upload \
|
|
391
|
+
-H "Content-Type: application/json" \
|
|
392
|
+
-H "Authorization: Bearer TOKEN" \
|
|
393
|
+
-d '{
|
|
394
|
+
"filename": "document.pdf",
|
|
395
|
+
"size": 5242880,
|
|
396
|
+
"metadata": {"type": "document"}
|
|
397
|
+
}'
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
### Upload Chunk
|
|
401
|
+
|
|
402
|
+
```bash
|
|
403
|
+
curl -X PATCH http://localhost:3000/uploadista/api/upload/upload-123 \
|
|
404
|
+
-H "Content-Range: bytes 0-1048575/5242880" \
|
|
405
|
+
-H "Authorization: Bearer TOKEN" \
|
|
406
|
+
--data-binary @chunk.bin
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
## Error Codes
|
|
410
|
+
|
|
411
|
+
- `400 VALIDATION_ERROR` - Invalid request
|
|
412
|
+
- `404 NOT_FOUND` - Upload/flow not found
|
|
413
|
+
- `409 CONFLICT` - Invalid chunk offset
|
|
414
|
+
- `413 PAYLOAD_TOO_LARGE` - File too large
|
|
415
|
+
- `500 INTERNAL_ERROR` - Server error
|
|
416
|
+
|
|
417
|
+
## Performance Tips
|
|
418
|
+
|
|
419
|
+
1. Use clustering for multiple CPU cores
|
|
420
|
+
2. Enable Redis for distributed deployments
|
|
421
|
+
3. Configure appropriate chunk sizes
|
|
422
|
+
4. Use reverse proxy (nginx) for load balancing
|
|
423
|
+
5. Monitor memory and disk usage
|
|
424
|
+
|
|
425
|
+
## Deployment Options
|
|
426
|
+
|
|
427
|
+
- **VPS**: Deploy with PM2 or systemd
|
|
428
|
+
- **Heroku**: Use Procfile and Redis add-on
|
|
429
|
+
- **Railway**: Direct GitHub integration
|
|
430
|
+
- **Docker**: Container deployment
|
|
431
|
+
- **AWS**: ECS, Lambda (with custom runtime)
|
|
432
|
+
- **DigitalOcean**: App Platform or VPS
|
|
433
|
+
|
|
434
|
+
## Related Packages
|
|
435
|
+
|
|
436
|
+
- **[@uploadista/server](../server/)** - Core server utilities
|
|
437
|
+
- **[@uploadista/adapters-hono](../adapters-hono/)** - Hono adapter
|
|
438
|
+
- **[@uploadista/adapters-fastify](../adapters-fastify/)** - Fastify adapter
|
|
439
|
+
- **[@uploadista/core](../../core/)** - Core engine
|
|
440
|
+
- **[@uploadista/kv-store-redis](../../kv-stores/redis/)** - Redis KV store
|
|
441
|
+
- **[@uploadista/data-store-s3](../../data-stores/s3/)** - AWS S3 storage
|
|
442
|
+
|
|
443
|
+
## Troubleshooting
|
|
444
|
+
|
|
445
|
+
### WebSocket Connection Refused
|
|
446
|
+
Ensure `ws` server is created and `websocketConnectionHandler` is called on connection.
|
|
447
|
+
|
|
448
|
+
### Memory Leaks
|
|
449
|
+
Check WebSocket connections are properly closed. Use `nodejs --inspect` for profiling.
|
|
450
|
+
|
|
451
|
+
### Slow Uploads
|
|
452
|
+
Use Redis for distributed deployments. Increase chunk size. Monitor network throughput.
|
|
453
|
+
|
|
454
|
+
## License
|
|
455
|
+
|
|
456
|
+
MIT
|
package/USAGE.md
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# Express Adapter for Uploadista
|
|
2
|
+
|
|
3
|
+
This package provides an Express.js adapter for the Uploadista upload server, following the same patterns as the Hono adapter but adapted for Express.js applications.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @uploadista/adapters-express express
|
|
9
|
+
# or
|
|
10
|
+
pnpm add @uploadista/adapters-express express
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Basic Usage
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import express from 'express';
|
|
17
|
+
import { createExpressUploadAdapter } from '@uploadista/adapters-express';
|
|
18
|
+
import { Layer } from 'effect';
|
|
19
|
+
|
|
20
|
+
// Set up your layers (same as Hono adapter)
|
|
21
|
+
const userLayer = Layer.mergeAll(
|
|
22
|
+
dataStoreLayer,
|
|
23
|
+
kvStoreLayer,
|
|
24
|
+
eventEmitterLayer,
|
|
25
|
+
generateIdLayer
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
// Create the adapter
|
|
29
|
+
const uploadAdapter = await createExpressUploadAdapter({
|
|
30
|
+
kvStore: kvStoreLayer,
|
|
31
|
+
dataStore: dataStoreLayer,
|
|
32
|
+
eventEmitter: eventEmitterLayer,
|
|
33
|
+
withTracing: true,
|
|
34
|
+
enableWebSockets: true
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const app = express();
|
|
38
|
+
|
|
39
|
+
// Apply JSON middleware for POST requests
|
|
40
|
+
app.use(express.json());
|
|
41
|
+
|
|
42
|
+
// HTTP endpoints - Express style
|
|
43
|
+
app.use('/api/upload', uploadAdapter.handler);
|
|
44
|
+
|
|
45
|
+
app.listen(3000, () => {
|
|
46
|
+
console.log('Express server running on port 3000');
|
|
47
|
+
});
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## WebSocket Integration
|
|
51
|
+
|
|
52
|
+
Unlike Hono which has built-in WebSocket support, Express requires an external WebSocket library. Here's how to integrate with the popular `ws` library:
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
import express from 'express';
|
|
56
|
+
import { createServer } from 'http';
|
|
57
|
+
import WebSocket from 'ws';
|
|
58
|
+
import { createExpressUploadAdapter } from '@uploadista/adapters-express';
|
|
59
|
+
|
|
60
|
+
const app = express();
|
|
61
|
+
const server = createServer(app);
|
|
62
|
+
|
|
63
|
+
// Create the upload adapter
|
|
64
|
+
const uploadAdapter = await createExpressUploadAdapter({
|
|
65
|
+
kvStore: kvStoreLayer,
|
|
66
|
+
dataStore: dataStoreLayer,
|
|
67
|
+
eventEmitter: eventEmitterLayer,
|
|
68
|
+
enableWebSockets: true
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// HTTP endpoints
|
|
72
|
+
app.use('/api/upload', uploadAdapter.handler);
|
|
73
|
+
|
|
74
|
+
// WebSocket server setup
|
|
75
|
+
const wss = new WebSocket.Server({
|
|
76
|
+
server,
|
|
77
|
+
path: '/ws/upload'
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
wss.on('connection', (ws, req) => {
|
|
81
|
+
const connection = {
|
|
82
|
+
id: `conn_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`,
|
|
83
|
+
send: (data: string) => ws.send(data),
|
|
84
|
+
close: (code?: number, reason?: string) => ws.close(code, reason),
|
|
85
|
+
readyState: ws.readyState
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// Handle WebSocket connection with Uploadista
|
|
89
|
+
uploadAdapter.websocketHandler(req, connection);
|
|
90
|
+
|
|
91
|
+
// Handle incoming messages
|
|
92
|
+
ws.on('message', (message) => {
|
|
93
|
+
// You can add custom message handling here
|
|
94
|
+
console.log('WebSocket message:', message.toString());
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Handle connection close
|
|
98
|
+
ws.on('close', () => {
|
|
99
|
+
console.log('WebSocket connection closed');
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
server.listen(3000, () => {
|
|
104
|
+
console.log('Express server with WebSocket support running on port 3000');
|
|
105
|
+
});
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Effect-native API
|
|
109
|
+
|
|
110
|
+
For Effect-based applications, you can use the Effect-native API:
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
import { Effect } from 'effect';
|
|
114
|
+
import { createExpressUploadServer } from '@uploadista/adapters-express';
|
|
115
|
+
|
|
116
|
+
const serverEffect = createExpressUploadServer({
|
|
117
|
+
kvStore: kvStoreLayer,
|
|
118
|
+
dataStore: dataStoreLayer,
|
|
119
|
+
eventEmitter: eventEmitterLayer,
|
|
120
|
+
enableWebSockets: true
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// Use within Effect context
|
|
124
|
+
const program = Effect.gen(function* () {
|
|
125
|
+
const server = yield* serverEffect;
|
|
126
|
+
|
|
127
|
+
// server.handler, server.upload, and server.websocketHandler
|
|
128
|
+
// are available as Effect-returning functions
|
|
129
|
+
});
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## API Reference
|
|
133
|
+
|
|
134
|
+
### Types
|
|
135
|
+
|
|
136
|
+
- `ExpressUploadAdapter`: Promise-based adapter for standard Express apps
|
|
137
|
+
- `ExpressUploadServer`: Effect-native server for Effect-based applications
|
|
138
|
+
- `WebSocketConnection`: Interface for WebSocket connection abstraction
|
|
139
|
+
- `ExpressWebSocketHandler`: Type for WebSocket handler functions
|
|
140
|
+
|
|
141
|
+
### Functions
|
|
142
|
+
|
|
143
|
+
- `createExpressUploadAdapter(options)`: Creates a Promise-based adapter
|
|
144
|
+
- `createExpressUploadServer(options)`: Creates an Effect-native server
|
|
145
|
+
- WebSocket utilities: `createWebSocketHandler`, `createWebSocketMessageHandler`, etc.
|
|
146
|
+
|
|
147
|
+
## Differences from Hono Adapter
|
|
148
|
+
|
|
149
|
+
1. **WebSocket Handling**: Express requires external WebSocket library integration
|
|
150
|
+
2. **Middleware**: Uses Express middleware patterns instead of Hono's context pattern
|
|
151
|
+
3. **Request/Response**: Works with Express's `req`/`res` objects
|
|
152
|
+
4. **Error Handling**: Integrates with Express error handling middleware
|
|
153
|
+
|
|
154
|
+
## Configuration Options
|
|
155
|
+
|
|
156
|
+
Same as the Hono adapter:
|
|
157
|
+
|
|
158
|
+
- `kvStore`: Key-value store layer
|
|
159
|
+
- `dataStore`: Data store layer (depends on kvStore)
|
|
160
|
+
- `eventEmitter`: Event emitter layer
|
|
161
|
+
- `webSocketManager`: Optional WebSocket manager layer
|
|
162
|
+
- `generateId`: Optional ID generation layer
|
|
163
|
+
- `withTracing`: Enable OpenTelemetry tracing
|
|
164
|
+
- `enableWebSockets`: Enable WebSocket support
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { IncomingMessage } from "node:http";
|
|
2
|
+
import type { UploadistaError } from "@uploadista/core/errors";
|
|
3
|
+
import type { InputFile, UploadFile } from "@uploadista/core/types";
|
|
4
|
+
import { type Effect, Context as EffectContext } from "effect";
|
|
5
|
+
import type { Request, Response } from "express";
|
|
6
|
+
export interface WebSocketConnection {
|
|
7
|
+
id: string;
|
|
8
|
+
send: (data: string) => void;
|
|
9
|
+
close: (code?: number, reason?: string) => void;
|
|
10
|
+
readyState: number;
|
|
11
|
+
}
|
|
12
|
+
export type ExpressWebSocketHandler = (req: IncomingMessage, connection: WebSocketConnection) => void;
|
|
13
|
+
export type ExpressAdapterServiceShape = {
|
|
14
|
+
handler: (req: Request, res: Response) => Effect.Effect<void>;
|
|
15
|
+
upload: (file: InputFile, stream: ReadableStream) => Effect.Effect<UploadFile, UploadistaError>;
|
|
16
|
+
websocketHandler: ExpressWebSocketHandler;
|
|
17
|
+
};
|
|
18
|
+
declare const ExpressAdapterService_base: EffectContext.TagClass<ExpressAdapterService, "ExpressAdapterService", ExpressAdapterServiceShape>;
|
|
19
|
+
export declare class ExpressAdapterService extends ExpressAdapterService_base {
|
|
20
|
+
}
|
|
21
|
+
export {};
|
|
22
|
+
//# sourceMappingURL=adapter-layer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adapter-layer.d.ts","sourceRoot":"","sources":["../src/adapter-layer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AACjD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC/D,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACpE,OAAO,EAAE,KAAK,MAAM,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,QAAQ,CAAC;AAC/D,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAEjD,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7B,KAAK,EAAE,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IAChD,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,MAAM,uBAAuB,GAAG,CACpC,GAAG,EAAE,eAAe,EACpB,UAAU,EAAE,mBAAmB,KAC5B,IAAI,CAAC;AAEV,MAAM,MAAM,0BAA0B,GAAG;IACvC,OAAO,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IAC9D,MAAM,EAAE,CACN,IAAI,EAAE,SAAS,EACf,MAAM,EAAE,cAAc,KACnB,MAAM,CAAC,MAAM,CAAC,UAAU,EAAE,eAAe,CAAC,CAAC;IAChD,gBAAgB,EAAE,uBAAuB,CAAC;CAC3C,CAAC;;AAEF,qBAAa,qBAAsB,SAAQ,0BAEW;CAAG"}
|