@uploadista/adapters-express 0.0.7 → 0.0.9

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.
@@ -1,642 +0,0 @@
1
- import type { IncomingMessage } from "node:http";
2
- import type {
3
- EventBroadcasterService,
4
- UploadFileDataStores,
5
- UploadistaError,
6
- } from "@uploadista/core";
7
- import { type Flow, FlowProvider, FlowServer } from "@uploadista/core/flow";
8
- import {
9
- type BaseEventEmitterService,
10
- type BaseKvStoreService,
11
- createDataStoreLayer,
12
- type DataStoreConfig,
13
- type UploadFileDataStore,
14
- type UploadFileKVStore,
15
- } from "@uploadista/core/types";
16
- import { UploadServer } from "@uploadista/core/upload";
17
- import { type GenerateId, GenerateIdLive } from "@uploadista/core/utils";
18
- import { memoryEventBroadcaster } from "@uploadista/event-broadcaster-memory";
19
- import { webSocketEventEmitter } from "@uploadista/event-emitter-websocket";
20
- import {
21
- type MetricsService,
22
- NodeSdkLive,
23
- NoOpMetricsServiceLive,
24
- } from "@uploadista/observability";
25
- import {
26
- type AuthCacheConfig,
27
- AuthCacheServiceLive,
28
- type AuthContext,
29
- AuthContextServiceLive,
30
- type AuthResult,
31
- createFlowServerLayer,
32
- createUploadServerLayer,
33
- type FlowRequirementsOf,
34
- } from "@uploadista/server";
35
- import { Effect, Layer } from "effect";
36
- import type { Request, Response } from "express";
37
- import type { z } from "zod";
38
- import {
39
- handleCancelFlow,
40
- handleFlowGet,
41
- handleFlowPost,
42
- handleJobStatus,
43
- handlePauseFlow,
44
- handleResumeFlow,
45
- } from "./flow-http-handlers";
46
- import {
47
- handleUploadGet,
48
- handleUploadPatch,
49
- handleUploadPost,
50
- } from "./upload-http-handlers";
51
- import {
52
- ExpressUploadistaAdapterService,
53
- type ExpressUploadistaAdapterServiceShape,
54
- type WebSocketConnection,
55
- type WebSocketHandlers,
56
- } from "./uploadista-adapter-layer";
57
- import { createUploadistaWebSocketHandler } from "./uploadista-websocket-handler";
58
-
59
- export type ExpressUploadistaAdapterOptions<
60
- TFlows extends (
61
- flowId: string,
62
- clientId: string | null,
63
- ) => Effect.Effect<
64
- Flow<z.ZodSchema<unknown>, z.ZodSchema<unknown>, unknown>,
65
- UploadistaError,
66
- unknown
67
- // biome-ignore lint/suspicious/noExplicitAny: Generic type constraint allows any flow function type
68
- > = any,
69
- // biome-ignore lint/suspicious/noExplicitAny: Permissive constraint allows plugin tuples, validation via PluginAssertion
70
- TPlugins extends readonly Layer.Layer<any, never, never>[] = Layer.Layer<
71
- any,
72
- never,
73
- never
74
- >[],
75
- > = {
76
- // Flow configuration
77
- flows: TFlows;
78
- plugins?: TPlugins;
79
-
80
- dataStore: DataStoreConfig;
81
- bufferedDataStore?: Layer.Layer<
82
- UploadFileDataStore,
83
- never,
84
- UploadFileKVStore
85
- >;
86
-
87
- // Shared configuration
88
- baseUrl?: string;
89
- kvStore: Layer.Layer<BaseKvStoreService>;
90
- eventEmitter?: Layer.Layer<BaseEventEmitterService>;
91
- eventBroadcaster?: Layer.Layer<EventBroadcasterService>;
92
- generateId?: Layer.Layer<GenerateId>;
93
- withTracing?: boolean;
94
-
95
- // Authentication
96
- authMiddleware?: (req: Request, res: Response) => Promise<AuthResult>;
97
- authCacheConfig?: AuthCacheConfig;
98
-
99
- // Metrics
100
- metricsLayer?: Layer.Layer<MetricsService, never, never>;
101
- };
102
-
103
- export type InternalExpressUploadistaAdapterOptions<
104
- TRequirements = never,
105
- TPlugins extends readonly Layer.Layer<TRequirements, never, never>[] = [],
106
- > = {
107
- // Flow configuration
108
- flowProvider: Layer.Layer<FlowProvider>;
109
- plugins?: TPlugins;
110
-
111
- // Upload configuration
112
- dataStore: Layer.Layer<UploadFileDataStores, never, UploadFileKVStore>;
113
- bufferedDataStore?: Layer.Layer<
114
- UploadFileDataStore,
115
- never,
116
- UploadFileKVStore
117
- >;
118
- // Shared configuration
119
- baseUrl: string;
120
- kvStore: Layer.Layer<BaseKvStoreService>;
121
- eventEmitter: Layer.Layer<BaseEventEmitterService>;
122
- generateId?: Layer.Layer<GenerateId>;
123
- withTracing?: boolean;
124
-
125
- // Authentication
126
- authMiddleware?: (req: Request, res: Response) => Promise<AuthResult>;
127
- authCacheConfig?: AuthCacheConfig;
128
-
129
- // Metrics
130
- metricsLayer?: Layer.Layer<MetricsService, never, never>;
131
- };
132
-
133
- export type ExpressUploadistaAdapter = {
134
- baseUrl: string;
135
- handler: (
136
- req: Request,
137
- res: Response,
138
- next?: (error?: Error) => void,
139
- ) => void;
140
- websocketHandler: (
141
- req: IncomingMessage,
142
- connection: WebSocketConnection,
143
- ) => WebSocketHandlers;
144
- websocketConnectionHandler: (ws: WebSocket, req: IncomingMessage) => void;
145
- };
146
-
147
- // WebSocket type from ws package
148
- interface WebSocket {
149
- readyState: number;
150
- OPEN: number;
151
- send: (data: string) => void;
152
- close: (code?: number, reason?: string) => void;
153
- on: (event: string, handler: (...args: any[]) => void) => void;
154
- }
155
-
156
- // Effect-native API
157
- export type ExpressUploadistaServer = {
158
- handler: (req: Request, res: Response) => Effect.Effect<void, never, never>;
159
- uploadServer: Layer.Layer<UploadServer>;
160
- flowServer: Layer.Layer<FlowServer>;
161
- websocketHandler: (
162
- req: IncomingMessage,
163
- connection: WebSocketConnection,
164
- ) => WebSocketHandlers;
165
- };
166
-
167
- // Effect-based service factory for creating the unified adapter layer
168
- const createExpressUploadistaAdapterServiceLayer = (
169
- baseUrl: string,
170
- authMiddleware?: (req: Request, res: Response) => Promise<AuthResult>,
171
- authCacheConfig?: AuthCacheConfig,
172
- metricsLayer?: Layer.Layer<MetricsService, never, never>,
173
- ) =>
174
- Layer.effect(
175
- ExpressUploadistaAdapterService,
176
- Effect.gen(function* () {
177
- const uploadServer = yield* UploadServer;
178
- const flowServer = yield* FlowServer;
179
-
180
- // Create auth cache layer (always present, even if auth is not enabled)
181
- const authCacheLayer = AuthCacheServiceLive(authCacheConfig);
182
-
183
- return {
184
- handler: (req: Request, res: Response) =>
185
- Effect.gen(function* () {
186
- // Call auth middleware if configured and create auth context layer
187
- let authContext: AuthContext | null = null;
188
- if (authMiddleware) {
189
- // Run auth middleware with timeout protection (5 seconds default)
190
- const authMiddlewareWithTimeout = Effect.tryPromise({
191
- try: () => authMiddleware(req, res),
192
- catch: (error) => {
193
- console.error("Auth middleware error:", error);
194
- return { _tag: "AuthError" as const, error };
195
- },
196
- }).pipe(
197
- Effect.timeout("5 seconds"),
198
- Effect.catchAll((error) => {
199
- // Check if timeout occurred
200
- if (error && typeof error === "object" && "_tag" in error) {
201
- if (error._tag === "TimeoutException") {
202
- console.error(
203
- "Auth middleware timeout exceeded (5 seconds)",
204
- );
205
- return Effect.succeed({
206
- _tag: "TimeoutError" as const,
207
- } as const);
208
- }
209
- }
210
- return Effect.succeed(null);
211
- }),
212
- );
213
-
214
- const authResult:
215
- | AuthContext
216
- | null
217
- | { _tag: "TimeoutError" }
218
- | { _tag: "AuthError"; error: unknown } =
219
- yield* authMiddlewareWithTimeout;
220
-
221
- // If auth middleware timed out, return 503 Service Unavailable
222
- if (
223
- authResult &&
224
- typeof authResult === "object" &&
225
- "_tag" in authResult &&
226
- authResult._tag === "TimeoutError"
227
- ) {
228
- res.status(503).json({
229
- error: "Authentication service unavailable",
230
- message:
231
- "Authentication took too long to respond. Please try again.",
232
- });
233
- return;
234
- }
235
-
236
- // If auth middleware returned null, authentication failed
237
- if (authResult === null) {
238
- res.status(401).json({
239
- error: "Unauthorized",
240
- message: "Invalid credentials",
241
- });
242
- return;
243
- }
244
-
245
- // Check for error marker (shouldn't happen after catchAll, but for type safety)
246
- if (
247
- authResult &&
248
- typeof authResult === "object" &&
249
- "_tag" in authResult &&
250
- authResult._tag === "AuthError"
251
- ) {
252
- res.status(500).json({
253
- error: "Internal Server Error",
254
- message: "An error occurred during authentication",
255
- });
256
- return;
257
- }
258
-
259
- authContext = authResult;
260
- }
261
-
262
- // Create auth context layer for this request
263
- const authContextLayer = AuthContextServiceLive(authContext);
264
-
265
- // Combine auth context, auth cache, and metrics layers
266
- // Always provide a metrics layer (either real or no-op) to satisfy type requirements
267
- const authLayer = Layer.mergeAll(
268
- authContextLayer,
269
- authCacheLayer,
270
- metricsLayer ?? NoOpMetricsServiceLive,
271
- );
272
-
273
- const url = new URL(req.url, `http://${req.get("host")}`);
274
-
275
- // Check for uploadista/api/ prefix
276
- if (!url.pathname.includes(`${baseUrl}/api/`)) {
277
- res.status(404).json({ error: "Not found" });
278
- return;
279
- }
280
-
281
- // Remove the prefix and get the actual route segments
282
- const routeSegments = url.pathname
283
- .replace(`${baseUrl}/api/`, "")
284
- .split("/")
285
- .filter(Boolean);
286
-
287
- // Parse JSON body for routes that need it
288
- const needsJsonBody =
289
- (routeSegments.includes("upload") && req.method === "POST") ||
290
- (routeSegments.includes("flow") && req.method === "POST") ||
291
- (routeSegments.includes("resume") &&
292
- req.get("Content-Type")?.includes("application/json"));
293
-
294
- if (needsJsonBody && !req.body) {
295
- // Manually parse JSON body
296
- yield* Effect.tryPromise({
297
- try: async () => {
298
- const chunks: Buffer[] = [];
299
- for await (const chunk of req) {
300
- chunks.push(chunk as Buffer);
301
- }
302
- const body = Buffer.concat(chunks).toString();
303
- req.body = JSON.parse(body);
304
- },
305
- catch: (error) => error,
306
- });
307
- }
308
-
309
- // Route based on path
310
- if (routeSegments.includes("upload")) {
311
- // Upload API routes - these now create jobs behind the scenes
312
- switch (req.method) {
313
- case "POST":
314
- return yield* handleUploadPost(req, res, uploadServer).pipe(
315
- Effect.provide(authLayer),
316
- );
317
- case "GET":
318
- return yield* handleUploadGet(req, res, uploadServer).pipe(
319
- Effect.provide(authLayer),
320
- );
321
- case "PATCH":
322
- return yield* handleUploadPatch(req, res, uploadServer).pipe(
323
- Effect.provide(authLayer),
324
- );
325
- default:
326
- res.status(405).json({ error: "Method not allowed" });
327
- return;
328
- }
329
- } else if (routeSegments.includes("flow")) {
330
- // Flow API routes
331
- switch (req.method) {
332
- case "GET":
333
- return yield* handleFlowGet(req, res, flowServer).pipe(
334
- Effect.provide(authLayer),
335
- );
336
- case "POST":
337
- return yield* handleFlowPost<never>(
338
- req,
339
- res,
340
- flowServer,
341
- ).pipe(Effect.provide(authLayer));
342
- default:
343
- res.status(405).json({ error: "Method not allowed" });
344
- return;
345
- }
346
- } else if (routeSegments.includes("jobs")) {
347
- // Unified job status routes
348
- if (req.method === "GET" && url.pathname.endsWith("/status")) {
349
- return yield* handleJobStatus(req, res, flowServer).pipe(
350
- Effect.provide(authLayer),
351
- );
352
- } else if (
353
- req.method === "PATCH" &&
354
- routeSegments.includes("resume")
355
- ) {
356
- return yield* handleResumeFlow<never>(
357
- req,
358
- res,
359
- flowServer,
360
- ).pipe(Effect.provide(authLayer));
361
- } else if (
362
- req.method === "POST" &&
363
- url.pathname.endsWith("/pause")
364
- ) {
365
- return yield* handlePauseFlow(req, res, flowServer).pipe(
366
- Effect.provide(authLayer),
367
- );
368
- } else if (
369
- req.method === "POST" &&
370
- url.pathname.endsWith("/cancel")
371
- ) {
372
- return yield* handleCancelFlow(req, res, flowServer).pipe(
373
- Effect.provide(authLayer),
374
- );
375
- }
376
- res.status(405).json({ error: "Method not allowed" });
377
- return;
378
- } else {
379
- res.status(404).json({ error: "Not found" });
380
- return;
381
- }
382
- }).pipe(
383
- Effect.catchAll(() =>
384
- Effect.sync(() => {
385
- res.status(500).json({ error: "Internal server error" });
386
- }),
387
- ),
388
- ),
389
-
390
- websocketHandler: createUploadistaWebSocketHandler(
391
- baseUrl,
392
- uploadServer,
393
- flowServer,
394
- ),
395
- } satisfies ExpressUploadistaAdapterServiceShape;
396
- }),
397
- );
398
-
399
- /**
400
- * Creates an Effect-native unified Express server - combining upload and flow capabilities
401
- */
402
- export const createExpressUploadistaServer = <TRequirements = UploadServer>({
403
- baseUrl,
404
- flowProvider,
405
- eventEmitter,
406
- dataStore,
407
- bufferedDataStore,
408
- kvStore,
409
- generateId = GenerateIdLive,
410
- authMiddleware,
411
- authCacheConfig,
412
- metricsLayer,
413
- }: InternalExpressUploadistaAdapterOptions<TRequirements>): Effect.Effect<ExpressUploadistaServer> => {
414
- // Set up upload server dependencies using shared utility
415
- const uploadServerLayer = createUploadServerLayer({
416
- kvStore,
417
- eventEmitter,
418
- dataStore,
419
- bufferedDataStore,
420
- generateId,
421
- });
422
-
423
- // Set up flow server dependencies using shared utility
424
- const flowServerLayer = createFlowServerLayer({
425
- kvStore,
426
- eventEmitter,
427
- flowProvider,
428
- uploadServer: uploadServerLayer,
429
- });
430
-
431
- // Set up adapter
432
- const adapterLayer = Layer.provide(
433
- createExpressUploadistaAdapterServiceLayer(
434
- baseUrl,
435
- authMiddleware,
436
- authCacheConfig,
437
- metricsLayer,
438
- ),
439
- Layer.mergeAll(uploadServerLayer, flowServerLayer),
440
- );
441
-
442
- return Effect.gen(function* () {
443
- const adapterService = yield* ExpressUploadistaAdapterService;
444
-
445
- return {
446
- handler: (req: Request, res: Response) =>
447
- adapterService.handler(req, res),
448
- websocketHandler: (
449
- req: IncomingMessage,
450
- connection: WebSocketConnection,
451
- ) => adapterService.websocketHandler(req, connection),
452
- uploadServer: uploadServerLayer,
453
- flowServer: flowServerLayer,
454
- } satisfies ExpressUploadistaServer;
455
- }).pipe(Effect.provide(adapterLayer));
456
- };
457
-
458
- const runProgram = <A, E>(
459
- effect: Effect.Effect<A, E>,
460
- withTracing: boolean,
461
- ) => {
462
- if (withTracing) {
463
- return Effect.runPromise(effect.pipe(Effect.provide(NodeSdkLive)));
464
- }
465
- return Effect.runPromise(effect);
466
- };
467
-
468
- /**
469
- * Creates a Promise-based Express adapter for compatibility with existing Express applications
470
- * This wraps the Effect-native version with Promise conversion and caches the server instance
471
- */
472
- export const createInternalExpressUploadistaAdapter = async <
473
- TRequirements = UploadServer,
474
- TPlugins extends readonly Layer.Layer<TRequirements, never, never>[] = [],
475
- >({
476
- baseUrl,
477
- flowProvider,
478
- plugins,
479
- eventEmitter,
480
- dataStore,
481
- bufferedDataStore,
482
- kvStore,
483
- withTracing = false,
484
- authMiddleware,
485
- authCacheConfig,
486
- metricsLayer,
487
- }: InternalExpressUploadistaAdapterOptions<
488
- TRequirements,
489
- TPlugins
490
- >): Promise<ExpressUploadistaAdapter> => {
491
- // Create and cache the Effect server instance
492
- const uploadistaServer = await Effect.runPromise(
493
- createExpressUploadistaServer<TRequirements>({
494
- baseUrl,
495
- flowProvider,
496
- eventEmitter,
497
- dataStore,
498
- bufferedDataStore,
499
- kvStore,
500
- authMiddleware,
501
- authCacheConfig,
502
- metricsLayer,
503
- }),
504
- );
505
-
506
- // Merge all plugin layers so we can provide them when running handlers
507
- const pluginLayers = Layer.mergeAll(
508
- uploadistaServer.uploadServer,
509
- ...(plugins ?? []),
510
- ) as Layer.Layer<UploadServer | TRequirements, never, never>;
511
-
512
- return {
513
- baseUrl,
514
- handler: (req: Request, res: Response, next) => {
515
- runProgram(
516
- uploadistaServer.handler(req, res).pipe(Effect.provide(pluginLayers)),
517
- withTracing,
518
- ).catch((error) => {
519
- console.error("Express adapter error:", error);
520
- if (next) next(error);
521
- });
522
- },
523
- websocketHandler: (req: IncomingMessage, connection: WebSocketConnection) =>
524
- uploadistaServer.websocketHandler(req, connection),
525
- websocketConnectionHandler: (ws: WebSocket, req: IncomingMessage) => {
526
- // Filter to only handle uploadista WebSocket paths
527
- if (!req.url?.startsWith(`/${baseUrl}/ws/`)) {
528
- ws.close(1008, "Invalid WebSocket path");
529
- return;
530
- }
531
-
532
- console.log(`📡 WebSocket connected: ${req.url}`);
533
-
534
- const connection: WebSocketConnection = {
535
- id: `conn_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`,
536
- send: (data: string) => {
537
- if (ws.readyState === ws.OPEN) {
538
- ws.send(data);
539
- }
540
- },
541
- close: (code?: number, reason?: string) => ws.close(code, reason),
542
- readyState: ws.readyState,
543
- };
544
-
545
- // Initialize WebSocket handler with Uploadista and get event handlers
546
- const handlers = uploadistaServer.websocketHandler(req, connection);
547
-
548
- // Attach event handlers
549
- ws.on("message", (message: Buffer) => {
550
- handlers.onMessage(message.toString());
551
- });
552
-
553
- ws.on("close", () => {
554
- handlers.onClose();
555
- });
556
-
557
- ws.on("error", (error: Error) => {
558
- handlers.onError(error);
559
- });
560
- },
561
- } satisfies ExpressUploadistaAdapter;
562
- };
563
-
564
- /**
565
- * Creates a Promise-based Uploadista Express adapter for compatibility
566
- *
567
- * Note: Ensure that the plugins array provides all services required by your flows.
568
- * Missing plugin services will result in runtime errors during flow execution.
569
- */
570
- export const createExpressUploadistaAdapter = async <
571
- TFlows extends (
572
- flowId: string,
573
- clientId: string | null,
574
- ) => Effect.Effect<
575
- Flow<z.ZodSchema<unknown>, z.ZodSchema<unknown>, unknown>,
576
- UploadistaError,
577
- unknown
578
- // biome-ignore lint/suspicious/noExplicitAny: Generic type constraint allows any flow function type
579
- > = any,
580
- // biome-ignore lint/suspicious/noExplicitAny: Permissive constraint allows plugin tuples, validation done at runtime
581
- TPlugins extends readonly Layer.Layer<any, never, never>[] = Layer.Layer<
582
- any,
583
- never,
584
- never
585
- >[],
586
- >({
587
- baseUrl = "uploadista",
588
- flows,
589
- // Default to an empty plugin list while preserving the generic type
590
- plugins = [] as unknown as TPlugins,
591
- eventEmitter,
592
- eventBroadcaster = memoryEventBroadcaster,
593
- dataStore,
594
- bufferedDataStore,
595
- kvStore,
596
- generateId = GenerateIdLive,
597
- authMiddleware,
598
- authCacheConfig,
599
- metricsLayer,
600
- }: ExpressUploadistaAdapterOptions<
601
- TFlows,
602
- TPlugins
603
- >): Promise<ExpressUploadistaAdapter> => {
604
- type FlowReq = FlowRequirementsOf<TFlows>;
605
- // Create a simplified flow provider that uses the flows function directly
606
- const createFlowProvider = Effect.succeed({
607
- getFlow: (flowId: string, clientId: string | null) => {
608
- // The flows function returns an Effect with TRequirements context,
609
- // but the FlowProvider interface expects no context.
610
- // We cast this to match the interface - the requirements will be provided
611
- // at the layer level when the flow adapter is created.
612
- return flows(flowId, clientId) as Effect.Effect<
613
- Flow<z.ZodSchema<unknown>, z.ZodSchema<unknown>, FlowReq>,
614
- UploadistaError
615
- >;
616
- },
617
- });
618
-
619
- // Create the flow provider layer that provides the requirements
620
- const flowProvider = Layer.effect(FlowProvider, createFlowProvider);
621
-
622
- // Default eventEmitter to webSocketEventEmitter with the provided eventBroadcaster
623
- const finalEventEmitter =
624
- eventEmitter ?? webSocketEventEmitter(eventBroadcaster);
625
-
626
- // Convert DataStoreConfig to Layer
627
- const dataStoreLayer = await createDataStoreLayer(dataStore);
628
-
629
- return createInternalExpressUploadistaAdapter<FlowReq, TPlugins>({
630
- baseUrl,
631
- flowProvider,
632
- plugins,
633
- eventEmitter: finalEventEmitter,
634
- dataStore: dataStoreLayer,
635
- bufferedDataStore,
636
- kvStore,
637
- generateId,
638
- authMiddleware,
639
- authCacheConfig,
640
- metricsLayer,
641
- });
642
- };