@uploadista/server 0.0.10 → 0.0.12
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/ADVANCED_TYPE_SYSTEM.md +495 -0
- package/PLUGIN_TYPING.md +369 -0
- package/TYPE_SAFE_EXAMPLES.md +468 -0
- package/dist/index.cjs +2 -1
- package/dist/index.d.cts +639 -17
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +638 -16
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +7 -7
- package/src/__tests__/backward-compatibility.test.ts +285 -0
- package/src/core/__tests__/plugin-validation.test.ts +472 -0
- package/src/core/create-type-safe-server.ts +204 -0
- package/src/core/index.ts +3 -0
- package/src/core/plugin-types.ts +217 -0
- package/src/core/plugin-validation.ts +319 -0
- package/src/core/server.ts +231 -15
- package/src/core/types.ts +19 -6
- package/src/plugins-typing.ts +122 -40
- package/type-tests/plugin-types.test-d.ts +388 -0
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
# Type-Safe Plugin System Examples
|
|
2
|
+
|
|
3
|
+
This document demonstrates how to use the improved type-safe plugin system for the Uploadista server.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [Basic Concepts](#basic-concepts)
|
|
8
|
+
- [Simple Flow (No Plugins)](#simple-flow-no-plugins)
|
|
9
|
+
- [Flow with Single Plugin](#flow-with-single-plugin)
|
|
10
|
+
- [Flow with Multiple Plugins](#flow-with-multiple-plugins)
|
|
11
|
+
- [Compile-Time Validation](#compile-time-validation)
|
|
12
|
+
- [Mixed Flows (Some with Plugins, Some without)](#mixed-flows-some-with-plugins-some-without)
|
|
13
|
+
- [Migration from Untyped to Typed](#migration-from-untyped-to-typed)
|
|
14
|
+
|
|
15
|
+
## Basic Concepts
|
|
16
|
+
|
|
17
|
+
### Plugin Types
|
|
18
|
+
|
|
19
|
+
Each plugin has three related types:
|
|
20
|
+
|
|
21
|
+
1. **Service Tag** - The Context.Tag class (e.g., `ImagePlugin`)
|
|
22
|
+
2. **Service Shape** - The interface defining methods (e.g., `ImagePluginShape`)
|
|
23
|
+
3. **Layer Type** - The Effect Layer type (e.g., `ImagePluginLayer`)
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
// Defined in @uploadista/core/flow
|
|
27
|
+
export class ImagePlugin extends Context.Tag("ImagePlugin")<
|
|
28
|
+
ImagePlugin,
|
|
29
|
+
ImagePluginShape
|
|
30
|
+
>() {}
|
|
31
|
+
|
|
32
|
+
export type ImagePluginLayer = Layer.Layer<ImagePlugin, never, never>;
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Type-Safe Flow Function
|
|
36
|
+
|
|
37
|
+
Use `TypeSafeFlowFunction<TRequirements>` to declare plugin dependencies:
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
import type { TypeSafeFlowFunction } from "@uploadista/server";
|
|
41
|
+
import { ImagePlugin } from "@uploadista/core/flow";
|
|
42
|
+
|
|
43
|
+
// Flow that requires ImagePlugin
|
|
44
|
+
const myFlow: TypeSafeFlowFunction<ImagePlugin> = (flowId, clientId) =>
|
|
45
|
+
Effect.gen(function* () {
|
|
46
|
+
const imageService = yield* ImagePlugin;
|
|
47
|
+
// ...
|
|
48
|
+
});
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Simple Flow (No Plugins)
|
|
52
|
+
|
|
53
|
+
For flows that don't need any plugins:
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
import { createTypeSafeServer, defineSimpleFlow } from "@uploadista/server";
|
|
57
|
+
import { createFlow, createInputNode } from "@uploadista/core/flow";
|
|
58
|
+
import { z } from "zod";
|
|
59
|
+
|
|
60
|
+
// Define a simple flow with no plugin dependencies
|
|
61
|
+
const simpleUploadFlow = defineSimpleFlow((flowId, clientId) =>
|
|
62
|
+
Effect.gen(function* () {
|
|
63
|
+
const inputNode = yield* createInputNode("input");
|
|
64
|
+
|
|
65
|
+
return createFlow({
|
|
66
|
+
id: "simple-upload",
|
|
67
|
+
name: "Simple Upload",
|
|
68
|
+
nodes: [inputNode],
|
|
69
|
+
edges: [],
|
|
70
|
+
inputSchema: z.object({
|
|
71
|
+
input: z.object({
|
|
72
|
+
file: z.instanceof(File),
|
|
73
|
+
}),
|
|
74
|
+
}),
|
|
75
|
+
outputSchema: z.object({
|
|
76
|
+
fileUrl: z.string(),
|
|
77
|
+
}),
|
|
78
|
+
});
|
|
79
|
+
}),
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
// Create server with no plugins
|
|
83
|
+
const server = await createTypeSafeServer({
|
|
84
|
+
plugins: [] as const, // No plugins needed
|
|
85
|
+
flows: simpleUploadFlow,
|
|
86
|
+
adapter: honoAdapter({ /* ... */ }),
|
|
87
|
+
dataStore: { type: "s3", config: { bucket: "uploads" } },
|
|
88
|
+
kvStore: redisKvStore,
|
|
89
|
+
});
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Flow with Single Plugin
|
|
93
|
+
|
|
94
|
+
### Example: Image Processing Flow
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
import { createTypeSafeServer, defineFlow } from "@uploadista/server";
|
|
98
|
+
import { ImagePlugin, createFlow, createInputNode } from "@uploadista/core/flow";
|
|
99
|
+
import { createResizeNode } from "@uploadista/flow-images-nodes";
|
|
100
|
+
import { sharpImagePlugin } from "@uploadista/flow-images-sharp";
|
|
101
|
+
import { z } from "zod";
|
|
102
|
+
|
|
103
|
+
// Define flow that requires ImagePlugin
|
|
104
|
+
const imageResizeFlow = defineFlow<ImagePlugin>((flowId, clientId) =>
|
|
105
|
+
Effect.gen(function* () {
|
|
106
|
+
const inputNode = yield* createInputNode("input");
|
|
107
|
+
const resizeNode = yield* createResizeNode("resize", {
|
|
108
|
+
width: 800,
|
|
109
|
+
height: 600,
|
|
110
|
+
fit: "cover",
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
return createFlow({
|
|
114
|
+
id: "image-resize",
|
|
115
|
+
name: "Image Resize",
|
|
116
|
+
nodes: [inputNode, resizeNode],
|
|
117
|
+
edges: [
|
|
118
|
+
{ source: "input", target: "resize" },
|
|
119
|
+
],
|
|
120
|
+
inputSchema: z.object({
|
|
121
|
+
input: z.object({
|
|
122
|
+
file: z.instanceof(File),
|
|
123
|
+
}),
|
|
124
|
+
}),
|
|
125
|
+
outputSchema: z.object({
|
|
126
|
+
resizedUrl: z.string(),
|
|
127
|
+
}),
|
|
128
|
+
});
|
|
129
|
+
}),
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
// Create server with ImagePlugin
|
|
133
|
+
const server = await createTypeSafeServer({
|
|
134
|
+
plugins: [sharpImagePlugin] as const, // ✅ Provides ImagePlugin
|
|
135
|
+
flows: imageResizeFlow,
|
|
136
|
+
adapter: honoAdapter({ /* ... */ }),
|
|
137
|
+
dataStore: { type: "s3", config: { bucket: "uploads" } },
|
|
138
|
+
kvStore: redisKvStore,
|
|
139
|
+
});
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Flow with Multiple Plugins
|
|
143
|
+
|
|
144
|
+
### Example: Image Processing + ZIP Archive
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
import {
|
|
148
|
+
createTypeSafeServer,
|
|
149
|
+
defineFlow,
|
|
150
|
+
type TypeSafeFlowFunction,
|
|
151
|
+
} from "@uploadista/server";
|
|
152
|
+
import {
|
|
153
|
+
ImagePlugin,
|
|
154
|
+
ZipPlugin,
|
|
155
|
+
createFlow,
|
|
156
|
+
createInputNode,
|
|
157
|
+
} from "@uploadista/core/flow";
|
|
158
|
+
import { createResizeNode, createOptimizeNode } from "@uploadista/flow-images-nodes";
|
|
159
|
+
import { createZipNode } from "@uploadista/flow-zip-nodes";
|
|
160
|
+
import { sharpImagePlugin } from "@uploadista/flow-images-sharp";
|
|
161
|
+
import { jsZipPlugin } from "@uploadista/flow-zip-jszip";
|
|
162
|
+
import { z } from "zod";
|
|
163
|
+
|
|
164
|
+
// Flow requires both ImagePlugin AND ZipPlugin
|
|
165
|
+
const imageArchiveFlow: TypeSafeFlowFunction<ImagePlugin | ZipPlugin> = (
|
|
166
|
+
flowId,
|
|
167
|
+
clientId,
|
|
168
|
+
) =>
|
|
169
|
+
Effect.gen(function* () {
|
|
170
|
+
// Both plugins are available
|
|
171
|
+
const imageService = yield* ImagePlugin;
|
|
172
|
+
const zipService = yield* ZipPlugin;
|
|
173
|
+
|
|
174
|
+
const inputNode = yield* createInputNode("input");
|
|
175
|
+
const resizeNode = yield* createResizeNode("resize", {
|
|
176
|
+
width: 1920,
|
|
177
|
+
height: 1080,
|
|
178
|
+
fit: "cover",
|
|
179
|
+
});
|
|
180
|
+
const optimizeNode = yield* createOptimizeNode("optimize", {
|
|
181
|
+
quality: 85,
|
|
182
|
+
format: "webp",
|
|
183
|
+
});
|
|
184
|
+
const zipNode = yield* createZipNode("zip", {
|
|
185
|
+
zipName: "images.zip",
|
|
186
|
+
includeMetadata: true,
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
return createFlow({
|
|
190
|
+
id: "image-archive",
|
|
191
|
+
name: "Image Processing and Archive",
|
|
192
|
+
nodes: [inputNode, resizeNode, optimizeNode, zipNode],
|
|
193
|
+
edges: [
|
|
194
|
+
{ source: "input", target: "resize" },
|
|
195
|
+
{ source: "resize", target: "optimize" },
|
|
196
|
+
{ source: "optimize", target: "zip" },
|
|
197
|
+
],
|
|
198
|
+
inputSchema: z.object({
|
|
199
|
+
input: z.object({
|
|
200
|
+
files: z.array(z.instanceof(File)),
|
|
201
|
+
}),
|
|
202
|
+
}),
|
|
203
|
+
outputSchema: z.object({
|
|
204
|
+
archiveUrl: z.string(),
|
|
205
|
+
}),
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// Create server with both plugins
|
|
210
|
+
const server = await createTypeSafeServer({
|
|
211
|
+
plugins: [
|
|
212
|
+
sharpImagePlugin, // Provides ImagePlugin
|
|
213
|
+
jsZipPlugin, // Provides ZipPlugin
|
|
214
|
+
] as const, // ✅ Both requirements satisfied
|
|
215
|
+
flows: imageArchiveFlow,
|
|
216
|
+
adapter: honoAdapter({ /* ... */ }),
|
|
217
|
+
dataStore: { type: "s3", config: { bucket: "uploads" } },
|
|
218
|
+
kvStore: redisKvStore,
|
|
219
|
+
});
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## Compile-Time Validation
|
|
223
|
+
|
|
224
|
+
### ✅ Valid Configuration
|
|
225
|
+
|
|
226
|
+
```typescript
|
|
227
|
+
import { ImagePlugin } from "@uploadista/core/flow";
|
|
228
|
+
import { sharpImagePlugin } from "@uploadista/flow-images-sharp";
|
|
229
|
+
|
|
230
|
+
// TypeScript validates this successfully
|
|
231
|
+
const server = await createTypeSafeServer({
|
|
232
|
+
plugins: [sharpImagePlugin] as const,
|
|
233
|
+
flows: defineFlow<ImagePlugin>((flowId, clientId) =>
|
|
234
|
+
Effect.gen(function* () {
|
|
235
|
+
const imageService = yield* ImagePlugin; // ✅ Available
|
|
236
|
+
return createFlow({ /* ... */ });
|
|
237
|
+
}),
|
|
238
|
+
),
|
|
239
|
+
// ... rest of config
|
|
240
|
+
});
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### ❌ Invalid Configuration (Compile Error)
|
|
244
|
+
|
|
245
|
+
```typescript
|
|
246
|
+
import { ImagePlugin } from "@uploadista/core/flow";
|
|
247
|
+
|
|
248
|
+
// TypeScript ERROR: ImagePlugin required but not provided
|
|
249
|
+
const server = await createTypeSafeServer({
|
|
250
|
+
plugins: [] as const, // ❌ No plugins!
|
|
251
|
+
flows: defineFlow<ImagePlugin>((flowId, clientId) =>
|
|
252
|
+
Effect.gen(function* () {
|
|
253
|
+
const imageService = yield* ImagePlugin; // ❌ Not available
|
|
254
|
+
return createFlow({ /* ... */ });
|
|
255
|
+
}),
|
|
256
|
+
),
|
|
257
|
+
// ... rest of config
|
|
258
|
+
});
|
|
259
|
+
// Error: Type 'ImagePlugin' does not satisfy constraint 'never'
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### Error Messages
|
|
263
|
+
|
|
264
|
+
When plugins are missing, TypeScript shows helpful error:
|
|
265
|
+
|
|
266
|
+
```typescript
|
|
267
|
+
type Error = {
|
|
268
|
+
__error: "Missing required plugins";
|
|
269
|
+
__required: ImagePlugin | ZipPlugin; // What's needed
|
|
270
|
+
__provided: never; // What's provided
|
|
271
|
+
__missing: ImagePlugin | ZipPlugin; // What's missing
|
|
272
|
+
};
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
## Mixed Flows (Some with Plugins, Some without)
|
|
276
|
+
|
|
277
|
+
### Router Pattern
|
|
278
|
+
|
|
279
|
+
```typescript
|
|
280
|
+
import {
|
|
281
|
+
ImagePlugin,
|
|
282
|
+
ZipPlugin,
|
|
283
|
+
CredentialProvider,
|
|
284
|
+
} from "@uploadista/core/flow";
|
|
285
|
+
import {
|
|
286
|
+
defineFlow,
|
|
287
|
+
defineSimpleFlow,
|
|
288
|
+
createTypeSafeServer,
|
|
289
|
+
} from "@uploadista/server";
|
|
290
|
+
|
|
291
|
+
// Different flows with different requirements
|
|
292
|
+
const simpleFlow = defineSimpleFlow((flowId, clientId) => /* ... */);
|
|
293
|
+
const imageFlow = defineFlow<ImagePlugin>((flowId, clientId) => /* ... */);
|
|
294
|
+
const aiFlow = defineFlow<ImagePlugin | CredentialProvider>(
|
|
295
|
+
(flowId, clientId) => /* ... */,
|
|
296
|
+
);
|
|
297
|
+
const archiveFlow = defineFlow<ImagePlugin | ZipPlugin>(
|
|
298
|
+
(flowId, clientId) => /* ... */,
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
// Router function that combines all flows
|
|
302
|
+
// Requirements = union of all individual requirements
|
|
303
|
+
const flowRouter: TypeSafeFlowFunction<
|
|
304
|
+
ImagePlugin | ZipPlugin | CredentialProvider
|
|
305
|
+
> = (flowId, clientId) =>
|
|
306
|
+
Effect.gen(function* () {
|
|
307
|
+
switch (flowId) {
|
|
308
|
+
case "simple":
|
|
309
|
+
return yield* simpleFlow(flowId, clientId);
|
|
310
|
+
case "image":
|
|
311
|
+
return yield* imageFlow(flowId, clientId);
|
|
312
|
+
case "ai":
|
|
313
|
+
return yield* aiFlow(flowId, clientId);
|
|
314
|
+
case "archive":
|
|
315
|
+
return yield* archiveFlow(flowId, clientId);
|
|
316
|
+
default:
|
|
317
|
+
return yield* Effect.fail(
|
|
318
|
+
new UploadistaError({
|
|
319
|
+
code: "FLOW_NOT_FOUND",
|
|
320
|
+
message: `Flow ${flowId} not found`,
|
|
321
|
+
}),
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
// Server must provide ALL plugins used by ANY flow
|
|
327
|
+
const server = await createTypeSafeServer({
|
|
328
|
+
plugins: [
|
|
329
|
+
sharpImagePlugin, // For image flows
|
|
330
|
+
jsZipPlugin, // For archive flow
|
|
331
|
+
credentialProviderLive, // For AI flow
|
|
332
|
+
] as const,
|
|
333
|
+
flows: flowRouter,
|
|
334
|
+
// ... rest of config
|
|
335
|
+
});
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
## Migration from Untyped to Typed
|
|
339
|
+
|
|
340
|
+
### Before (Untyped)
|
|
341
|
+
|
|
342
|
+
```typescript
|
|
343
|
+
import { createUploadistaServer } from "@uploadista/server";
|
|
344
|
+
|
|
345
|
+
const server = await createUploadistaServer({
|
|
346
|
+
plugins: [sharpImagePlugin], // No type safety
|
|
347
|
+
flows: (flowId, clientId) =>
|
|
348
|
+
Effect.gen(function* () {
|
|
349
|
+
// Could forget to provide plugin - runtime error!
|
|
350
|
+
const imageService = yield* ImagePlugin;
|
|
351
|
+
return createFlow({ /* ... */ });
|
|
352
|
+
}),
|
|
353
|
+
// ...
|
|
354
|
+
});
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
### After (Type-Safe)
|
|
358
|
+
|
|
359
|
+
```typescript
|
|
360
|
+
import { createTypeSafeServer, defineFlow } from "@uploadista/server";
|
|
361
|
+
import { ImagePlugin } from "@uploadista/core/flow";
|
|
362
|
+
|
|
363
|
+
const imageFlow = defineFlow<ImagePlugin>((flowId, clientId) =>
|
|
364
|
+
Effect.gen(function* () {
|
|
365
|
+
// Type-checked at compile time!
|
|
366
|
+
const imageService = yield* ImagePlugin;
|
|
367
|
+
return createFlow({ /* ... */ });
|
|
368
|
+
}),
|
|
369
|
+
);
|
|
370
|
+
|
|
371
|
+
const server = await createTypeSafeServer({
|
|
372
|
+
plugins: [sharpImagePlugin] as const, // ✅ Type-safe
|
|
373
|
+
flows: imageFlow,
|
|
374
|
+
// ...
|
|
375
|
+
});
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
### Step-by-Step Migration
|
|
379
|
+
|
|
380
|
+
1. **Add explicit type to flow function**:
|
|
381
|
+
```typescript
|
|
382
|
+
- const myFlow = (flowId, clientId) => ...
|
|
383
|
+
+ const myFlow: TypeSafeFlowFunction<ImagePlugin> = (flowId, clientId) => ...
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
2. **Use `as const` for plugins array**:
|
|
387
|
+
```typescript
|
|
388
|
+
- plugins: [sharpImagePlugin]
|
|
389
|
+
+ plugins: [sharpImagePlugin] as const
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
3. **Replace `createUploadistaServer` with `createTypeSafeServer`**:
|
|
393
|
+
```typescript
|
|
394
|
+
- import { createUploadistaServer } from "@uploadista/server";
|
|
395
|
+
+ import { createTypeSafeServer } from "@uploadista/server";
|
|
396
|
+
|
|
397
|
+
- const server = await createUploadistaServer({
|
|
398
|
+
+ const server = await createTypeSafeServer({
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
4. **Fix any type errors** (missing plugins, wrong types, etc.)
|
|
402
|
+
|
|
403
|
+
## Benefits
|
|
404
|
+
|
|
405
|
+
### Type Safety
|
|
406
|
+
|
|
407
|
+
- ✅ **Compile-time validation** - Errors caught before runtime
|
|
408
|
+
- ✅ **Autocomplete** - IDE suggests available plugin services
|
|
409
|
+
- ✅ **Refactoring safety** - Type errors when removing required plugins
|
|
410
|
+
- ✅ **Documentation** - Types serve as inline documentation
|
|
411
|
+
|
|
412
|
+
### Before vs After
|
|
413
|
+
|
|
414
|
+
| Aspect | Before (Untyped) | After (Type-Safe) |
|
|
415
|
+
|--------|------------------|-------------------|
|
|
416
|
+
| Plugin validation | Runtime | Compile-time |
|
|
417
|
+
| IDE autocomplete | Limited | Full support |
|
|
418
|
+
| Error messages | Generic | Specific (shows missing plugins) |
|
|
419
|
+
| Refactoring | Risky | Safe |
|
|
420
|
+
| Documentation | External | In types |
|
|
421
|
+
|
|
422
|
+
## Best Practices
|
|
423
|
+
|
|
424
|
+
1. **Always use `as const`** for plugin arrays to preserve tuple types
|
|
425
|
+
2. **Use `defineFlow<T>`** helper for explicit type declarations
|
|
426
|
+
3. **Declare requirements at flow level**, not server level
|
|
427
|
+
4. **Group flows by plugin requirements** for better organization
|
|
428
|
+
5. **Start simple** - Begin with untyped, migrate to typed gradually
|
|
429
|
+
|
|
430
|
+
## Troubleshooting
|
|
431
|
+
|
|
432
|
+
### Problem: "Type 'X' does not satisfy constraint 'never'"
|
|
433
|
+
|
|
434
|
+
**Cause**: Flow requires plugin X, but it's not provided.
|
|
435
|
+
|
|
436
|
+
**Solution**: Add the missing plugin to the `plugins` array:
|
|
437
|
+
```typescript
|
|
438
|
+
plugins: [missingPlugin, ...otherPlugins] as const
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
### Problem: "Type instantiation is excessively deep"
|
|
442
|
+
|
|
443
|
+
**Cause**: Too many plugins or complex plugin combinations.
|
|
444
|
+
|
|
445
|
+
**Solution**: Use the untyped `createUploadistaServer` for very complex cases, or split into multiple servers.
|
|
446
|
+
|
|
447
|
+
### Problem: Lost autocomplete for plugin services
|
|
448
|
+
|
|
449
|
+
**Cause**: Missing `as const` on plugins array.
|
|
450
|
+
|
|
451
|
+
**Solution**: Always add `as const`:
|
|
452
|
+
```typescript
|
|
453
|
+
plugins: [plugin1, plugin2] as const
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
### Problem: Type errors in working code
|
|
457
|
+
|
|
458
|
+
**Cause**: Plugin tuple not properly inferred.
|
|
459
|
+
|
|
460
|
+
**Solution**: Explicitly annotate the type:
|
|
461
|
+
```typescript
|
|
462
|
+
import type { ImagePluginLayer, ZipPluginLayer } from "@uploadista/core/flow";
|
|
463
|
+
|
|
464
|
+
const plugins: readonly [ImagePluginLayer, ZipPluginLayer] = [
|
|
465
|
+
sharpImagePlugin,
|
|
466
|
+
jsZipPlugin,
|
|
467
|
+
] as const;
|
|
468
|
+
```
|
package/dist/index.cjs
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
const e=require(`./auth-B3XCQncE.cjs`);let t=require(`effect`),n=require(`@uploadista/core/flow`),r=require(`@uploadista/core/types`),i=require(`@uploadista/core/utils`),a=require(`@uploadista/event-broadcaster-memory`),o=require(`@uploadista/event-emitter-websocket`),s=require(`@uploadista/observability`),c=require(`@uploadista/core/upload`),l=require(`@uploadista/core/errors`);var u=class extends t.Context.Tag(`AuthCacheService`)(){};const d=(e={})=>{let n=e.maxSize??1e4,r=e.ttl??36e5,i=new Map,a=()=>{let e=Date.now();for(let[t,n]of i.entries())e-n.timestamp>r&&i.delete(t)},o=()=>{if(i.size<=n)return;let e=null,t=1/0;for(let[n,r]of i.entries())r.timestamp<t&&(t=r.timestamp,e=n);e&&i.delete(e)};return t.Layer.succeed(u,{set:(e,n)=>t.Effect.sync(()=>{i.size%100==0&&a(),i.set(e,{authContext:n,timestamp:Date.now()}),o()}),get:e=>t.Effect.sync(()=>{let t=i.get(e);return t?Date.now()-t.timestamp>r?(i.delete(e),null):t.authContext:null}),delete:e=>t.Effect.sync(()=>{i.delete(e)}),clear:()=>t.Effect.sync(()=>{i.clear()}),size:()=>t.Effect.sync(()=>i.size)})},f=t.Layer.succeed(u,{set:()=>t.Effect.void,get:()=>t.Effect.succeed(null),delete:()=>t.Effect.void,clear:()=>t.Effect.void,size:()=>t.Effect.succeed(0)}),p=e=>e.split(`/`).filter(Boolean),m=e=>{let t=p(e);return t[t.length-1]},h=(e,t)=>e.includes(`${t}/api/`),g=(e,t)=>e.replace(`${t}/api/`,``).split(`/`).filter(Boolean),_=e=>{let t=500,n=`UNKNOWN_ERROR`,r=`Internal server error`,i;if(typeof e==`object`&&e){let a=e;if(`code`in a&&typeof a.code==`string`&&(n=a.code),`message`in a&&typeof a.message==`string`?r=a.message:`body`in a&&typeof a.body==`string`&&(r=a.body),`details`in a&&(i=a.details),`status`in a&&typeof a.status==`number`)t=a.status;else if(`code`in a)switch(a.code){case`FILE_NOT_FOUND`:case`FLOW_JOB_NOT_FOUND`:case`UPLOAD_ID_NOT_FOUND`:t=404;break;case`FLOW_JOB_ERROR`:case`VALIDATION_ERROR`:case`INVALID_METADATA`:case`INVALID_LENGTH`:case`ABORTED`:case`INVALID_TERMINATION`:t=400;break;case`INVALID_OFFSET`:t=409;break;case`ERR_SIZE_EXCEEDED`:case`ERR_MAX_SIZE_EXCEEDED`:t=413;break;case`FILE_NO_LONGER_EXISTS`:t=410;break;case`MISSING_OFFSET`:case`INVALID_CONTENT_TYPE`:t=403;break;default:t=500}`message`in a&&a.message===`Invalid JSON body`&&(t=400,n=`VALIDATION_ERROR`)}let a={status:t,code:n,message:r};return i!==void 0&&(a.details=i),a},v=e=>e[e.length-2],y=e=>({jobId:e[e.length-3],nodeId:e[e.length-1]}),b=e=>({storageId:e.pop(),flowId:e.pop()}),x=({kvStore:e,eventEmitter:n,dataStore:i,bufferedDataStore:a,generateId:o})=>{let s=t.Layer.provide(r.uploadFileKvStore,e),l=t.Layer.provide(i,s),u=a?t.Layer.provide(a,s):t.Layer.empty,d=t.Layer.provide(r.uploadEventEmitter,n),f=t.Layer.mergeAll(l,s,d,...o?[o]:[],u);return t.Layer.provide(c.uploadServer,f)},S=({kvStore:e,eventEmitter:i,flowProvider:a,uploadServer:o})=>{let s=t.Layer.provide(r.flowJobKvStore,e),c=t.Layer.provide(r.flowEventEmitter,i),l=t.Layer.mergeAll(a,c,s,o);return t.Layer.provide(n.flowServer,l)};var C=class extends t.Context.Tag(`AuthContextService`)(){};const w=e=>t.Layer.succeed(C,{getClientId:()=>t.Effect.succeed(e?.clientId??null),getMetadata:()=>t.Effect.succeed(e?.metadata??{}),hasPermission:n=>t.Effect.succeed(e?.permissions?.includes(n)??!1),getAuthContext:()=>t.Effect.succeed(e)}),T=w(null),E=({flowId:e})=>t.Effect.gen(function*(){let r=yield*n.FlowServer,i=yield*(yield*C).getClientId();return i&&(yield*t.Effect.logInfo(`[Flow] Getting flow data: ${e}, client: ${i}`)),{status:200,body:yield*r.getFlowData(e,i)}}),D=({flowId:e,storageId:r,inputs:i})=>t.Effect.gen(function*(){let a=yield*n.FlowServer,o=yield*C,s=yield*u,c=yield*o.getClientId();c?(yield*t.Effect.logInfo(`[Flow] Executing flow: ${e}, storage: ${r}, client: ${c}`),yield*t.Effect.logInfo(JSON.stringify(i,null,2))):(yield*t.Effect.logInfo(`[Flow] Executing flow: ${e}, storage: ${r}`),yield*t.Effect.logInfo(`[Flow] Inputs: ${JSON.stringify(i,null,2)}`)),yield*t.Effect.logInfo(`[Flow] Calling flowServer.runFlow...`);let l=yield*a.runFlow({flowId:e,storageId:r,clientId:c,inputs:i}).pipe(t.Effect.tap(()=>t.Effect.logInfo(`[Flow] runFlow completed successfully`)),t.Effect.tapError(e=>t.Effect.logError(`[Flow] runFlow failed with error: ${e}`))),d=yield*o.getAuthContext();return d&&(yield*s.set(l.id,d)),yield*t.Effect.logInfo(`[Flow] Flow started with jobId: ${l.id}`),{status:200,body:l}}),O=({jobId:e})=>t.Effect.gen(function*(){let r=yield*n.FlowServer,i=yield*C,a=yield*u,o=yield*i.getClientId();if(!e)throw Error(`No job id`);o&&(yield*t.Effect.logInfo(`[Flow] Getting job status: ${e}, client: ${o}`));let s=yield*r.getJobStatus(e);return(s.status===`completed`||s.status===`failed`)&&(yield*a.delete(e),o&&(yield*t.Effect.logInfo(`[Flow] Flow ${s.status}, cleared auth cache: ${e}`))),{status:200,body:s}}),k=({jobId:e,nodeId:r,newData:i})=>t.Effect.gen(function*(){let a=yield*n.FlowServer,o=yield*C,s=yield*u,c=yield*o.getClientId();if(c||=(yield*s.get(e))?.clientId??null,c&&(yield*t.Effect.logInfo(`[Flow] Continuing flow: jobId=${e}, nodeId=${r}, client: ${c}`)),i===void 0)throw Error(`Missing newData`);let l=yield*a.resumeFlow({jobId:e,nodeId:r,newData:i,clientId:c});return(l.status===`completed`||l.status===`failed`)&&(yield*s.delete(e),c&&(yield*t.Effect.logInfo(`[Flow] Flow ${l.status}, cleared auth cache: ${e}`))),{status:200,body:l}}),A=({jobId:e})=>t.Effect.gen(function*(){let r=yield*n.FlowServer,i=yield*C,a=yield*u,o=yield*i.getClientId();o||=(yield*a.get(e))?.clientId??null,o&&(yield*t.Effect.logInfo(`[Flow] Pausing flow: jobId=${e}, client: ${o}`));let s=yield*r.pauseFlow(e,o);return o&&(yield*t.Effect.logInfo(`[Flow] Flow paused: ${e}, status: ${s.status}`)),{status:200,body:s}}),j=({jobId:e})=>t.Effect.gen(function*(){let r=yield*n.FlowServer,i=yield*C,a=yield*u;if(!e)throw Error(`No job id`);let o=yield*i.getClientId();o||=(yield*a.get(e))?.clientId??null,o&&(yield*t.Effect.logInfo(`[Flow] Cancelling flow: jobId=${e}, client: ${o}`));let s=yield*r.cancelFlow(e,o);return yield*a.delete(e),o&&(yield*t.Effect.logInfo(`[Flow] Flow cancelled, cleared auth cache: ${e}`)),{status:200,body:s}});var M=class extends Error{constructor(e,t=500,n=`INTERNAL_ERROR`){super(e),this.statusCode=t,this.errorCode=n,this.name=`AdapterError`}},N=class extends M{constructor(e){super(e,400,`VALIDATION_ERROR`),this.name=`ValidationError`}},P=class extends M{constructor(e){super(`${e} not found`,404,`NOT_FOUND`),this.name=`NotFoundError`}},F=class extends M{constructor(e){super(e,400,`BAD_REQUEST`),this.name=`BadRequestError`}};const I=e=>({error:e.message,code:e.errorCode,timestamp:new Date().toISOString()}),L=e=>{let t={error:e.body,code:e.code,timestamp:new Date().toISOString()};return e.details!==void 0&&(t.details=e.details),t},R=(e=`Internal server error`)=>({error:e,code:`INTERNAL_ERROR`,timestamp:new Date().toISOString()}),z=e=>t.Effect.gen(function*(){let n=yield*c.UploadServer,a=yield*C,o=yield*u,s=yield*a.getClientId();s&&(yield*t.Effect.logInfo(`[Upload] Creating upload for client: ${s}`));let l=yield*t.Effect.sync(()=>r.inputFileSchema.safeParse(e.data));if(!l.success)return yield*t.Effect.fail(new N(`Invalid input file schema`));if(l.data.checksumAlgorithm&&!(0,i.isSupportedAlgorithm)(l.data.checksumAlgorithm))return yield*t.Effect.fail(new N(`Unsupported checksum algorithm: ${l.data.checksumAlgorithm}. Supported algorithms: sha256`));let d=yield*n.createUpload(l.data,s),f=yield*a.getAuthContext();return f&&(yield*o.set(d.id,f)),s&&(yield*t.Effect.logInfo(`[Upload] Upload created: ${d.id} for client: ${s}`)),{status:200,body:d}}),B=({storageId:e})=>t.Effect.gen(function*(){let t=yield*c.UploadServer,n=yield*(yield*C).getClientId();return{status:200,body:{storageId:e,capabilities:yield*t.getCapabilities(e,n),timestamp:new Date().toISOString()}}}),V=({uploadId:e})=>t.Effect.gen(function*(){return{status:200,body:yield*(yield*c.UploadServer).getUpload(e)}}),H=e=>t.Effect.gen(function*(){let n=yield*c.UploadServer,r=yield*C,i=yield*u,a=yield*s.MetricsService,{uploadId:o,data:l}=e,d=yield*r.getClientId(),f=yield*r.getMetadata();if(!d){let e=yield*i.get(o);d=e?.clientId??null,f=e?.metadata??{}}d&&(yield*t.Effect.logInfo(`[Upload] Uploading chunk for upload: ${o}, client: ${d}`));let p=yield*n.uploadChunk(o,d,l);return p.size&&p.offset>=p.size&&(yield*i.delete(o),d&&(yield*t.Effect.logInfo(`[Upload] Upload completed, cleared auth cache: ${o}`)),d&&p.size?(yield*t.Effect.logInfo(`[Upload] Recording metrics for org: ${d}, size: ${p.size}`),yield*t.Effect.forkDaemon(a.recordUpload(d,p.size,f))):yield*t.Effect.logWarning(`[Upload] Cannot record metrics - missing organizationId or size`)),d&&(yield*t.Effect.logInfo(`[Upload] Chunk uploaded for upload: ${o}, client: ${d}`)),{status:200,body:p}}),U=e=>t.Effect.gen(function*(){switch(e.type){case`create-upload`:return yield*z(e);case`get-capabilities`:return yield*B(e);case`get-upload`:return yield*V(e);case`upload-chunk`:return yield*H(e);case`get-flow`:return yield*E(e);case`run-flow`:return yield*D(e);case`job-status`:return yield*O(e);case`resume-flow`:return yield*k(e);case`pause-flow`:return yield*A(e);case`cancel-flow`:return yield*j(e);case`not-found`:return{status:404,headers:{"Content-Type":`application/json`},body:{error:`Not found`}};case`bad-request`:return{status:400,body:{error:`Bad request`,message:e.message}};case`method-not-allowed`:return{status:405,headers:{"Content-Type":`application/json`},body:{error:`Method not allowed`}};case`unsupported-content-type`:return{status:415,headers:{"Content-Type":`application/json`},body:{error:`Unsupported content type`}}}}),W=async({flows:e,dataStore:c,kvStore:l,plugins:u=[],eventEmitter:f,eventBroadcaster:p=a.memoryEventBroadcaster,withTracing:m=!1,baseUrl:h=`uploadista`,generateId:g=i.GenerateIdLive,metricsLayer:v,bufferedDataStore:y,adapter:b,authCacheConfig:C})=>{let T=f??(0,o.webSocketEventEmitter)(p),E=h.endsWith(`/`)?h.slice(0,-1):h,D=t.Layer.effect(n.FlowProvider,t.Effect.succeed({getFlow:(t,n)=>e(t,n)}));if(!T)throw Error(`eventEmitter is required. Provide an event emitter layer in the configuration.`);let O=x({kvStore:l,eventEmitter:T,dataStore:await(0,r.createDataStoreLayer)(c),bufferedDataStore:y,generateId:g}),k=S({kvStore:l,eventEmitter:T,flowProvider:D,uploadServer:O}),A=d(C),j=v??s.NoOpMetricsServiceLive,M=t.Layer.mergeAll(O,k,j,A,...u),N=t.ManagedRuntime.make(M);return{handler:async e=>{let r=t.Effect.gen(function*(){let r=yield*b.extractRequest(e,{baseUrl:E}),i=null;if(b.runAuthMiddleware){let n=yield*b.runAuthMiddleware(e).pipe(t.Effect.timeout(`5 seconds`),t.Effect.catchAll(()=>(console.error(`Auth middleware timeout exceeded (5 seconds)`),t.Effect.succeed({_tag:`TimeoutError`}))),t.Effect.catchAllCause(e=>(console.error(`Auth middleware error:`,e),t.Effect.succeed({_tag:`AuthError`,error:e}))));if(n&&typeof n==`object`&&`_tag`in n&&n._tag===`TimeoutError`)return yield*b.sendResponse({status:503,headers:{"Content-Type":`application/json`},body:{error:`Authentication service unavailable`,message:`Authentication took too long to respond. Please try again.`}},e);if(n&&typeof n==`object`&&`_tag`in n&&n._tag===`AuthError`)return yield*b.sendResponse({status:500,headers:{"Content-Type":`application/json`},body:{error:`Internal Server Error`,message:`An error occurred during authentication`}},e);if(n===null)return yield*b.sendResponse({status:401,headers:{"Content-Type":`application/json`},body:{error:`Unauthorized`,message:`Invalid credentials`}},e);i=n}let a=w(i),o=[];if(b.extractWaitUntil){let r=b.extractWaitUntil(e);r&&o.push(t.Layer.succeed(n.FlowWaitUntil,r))}let s=t.Layer.mergeAll(a,A,j,...u,...o);if(r.type===`not-found`)return yield*b.sendResponse({type:`not-found`,status:404,headers:{"Content-Type":`application/json`},body:{error:`Not found`}},e);let c=yield*U(r).pipe(t.Effect.provide(s));return yield*b.sendResponse(c,e)}).pipe(t.Effect.catchAll(t=>{let n=_(t),r={code:n.code,message:n.message};n.details!==void 0&&(r.details=n.details);let i={status:n.status,headers:{"Content-Type":`application/json`},body:r};return b.sendResponse(i,e)}));return m?N.runPromise(r.pipe(t.Effect.provide(s.NodeSdkLive))):N.runPromise(r)},websocketHandler:await N.runPromise(b.webSocketHandler({baseUrl:E})),baseUrl:E,dispose:()=>N.dispose()}},G=(e,n,r)=>t.Effect.gen(function*(){if(!n){yield*t.Effect.sync(()=>{r.send(JSON.stringify({type:`error`,message:`Job ID is required for flow event subscription`,code:`MISSING_JOB_ID`}))});return}yield*e.subscribeToFlowEvents(n,r)}),K=(e,n)=>t.Effect.gen(function*(){n&&(yield*e.unsubscribeFromFlowEvents(n))}),q=(e,n,r)=>t.Effect.gen(function*(){if(!n){yield*t.Effect.sync(()=>{r.send(JSON.stringify({type:`error`,message:`Upload ID is required for upload event subscription`,code:`MISSING_UPLOAD_ID`}))});return}yield*e.subscribeToUploadEvents(n,r)}),J=(e,n)=>t.Effect.gen(function*(){n&&(yield*e.unsubscribeFromUploadEvents(n))}),Y=(e,n,r)=>{let{connection:i,isFlowRoute:a,isUploadRoute:o,jobId:s,uploadId:c,eventId:u}=e;return t.Effect.gen(function*(){a&&(yield*G(r,s,i)),o&&(yield*q(n,c,i)),i.send(JSON.stringify({type:`connection`,message:`Uploadista WebSocket connected`,id:u,jobId:s,uploadId:c,timestamp:new Date().toISOString()}))}).pipe(t.Effect.catchAll(e=>t.Effect.sync(()=>{console.error(`Error subscribing to events:`,e);let t=e instanceof l.UploadistaError?e.body:`Failed to subscribe to events`;i.send(JSON.stringify({type:`error`,message:t,code:e instanceof l.UploadistaError?e.code:`SUBSCRIPTION_ERROR`}))})))},X=(e,n)=>t.Effect.sync(()=>{try{JSON.parse(e).type===`ping`&&n.send(JSON.stringify({type:`pong`,timestamp:new Date().toISOString()}))}catch(e){console.error(`Error handling WebSocket message:`,e),n.send(JSON.stringify({type:`error`,message:`Invalid message format`}))}}),Z=(e,n,r)=>{let{isFlowRoute:i,isUploadRoute:a,jobId:o,uploadId:s}=e;return t.Effect.gen(function*(){i&&(yield*K(r,o)),a&&(yield*J(n,s))}).pipe(t.Effect.catchAll(e=>t.Effect.sync(()=>{console.error(`Error unsubscribing from events:`,e instanceof l.UploadistaError?e.body:e)})))},Q=(e,n)=>t.Effect.sync(()=>{console.error(`WebSocket error for event ${n}:`,e)});exports.AdapterError=M,exports.AuthCacheService=u,exports.AuthCacheServiceLive=d,exports.AuthContextService=C,exports.AuthContextServiceLive=w,exports.BadRequestError=F,exports.NoAuthCacheServiceLive=f,exports.NoAuthContextServiceLive=T,exports.NotFoundError=P,exports.ValidationError=N,exports.createErrorResponseBody=I,exports.createFlowServerLayer=S,exports.createGenericErrorResponseBody=R,exports.createUploadServerLayer=x,exports.createUploadistaErrorResponseBody=L,exports.createUploadistaServer=W,exports.extractFlowAndStorageId=b,exports.extractJobAndNodeId=y,exports.extractJobIdFromStatus=v,exports.getAuthCredentials=e.t,exports.getLastSegment=m,exports.getRouteSegments=g,exports.handleFlowError=_,exports.handleWebSocketClose=Z,exports.handleWebSocketError=Q,exports.handleWebSocketMessage=X,exports.handleWebSocketOpen=Y,exports.hasBasePath=h,exports.parseUrlSegments=p;
|
|
1
|
+
const e=require(`./auth-B3XCQncE.cjs`);let t=require(`effect`),n=require(`@uploadista/core/flow`),r=require(`@uploadista/core/types`),i=require(`@uploadista/core/utils`),a=require(`@uploadista/event-broadcaster-memory`),o=require(`@uploadista/event-emitter-websocket`),s=require(`@uploadista/observability`),c=require(`@uploadista/core/upload`),l=require(`@uploadista/core/errors`);var u=class extends t.Context.Tag(`AuthCacheService`)(){};const d=(e={})=>{let n=e.maxSize??1e4,r=e.ttl??36e5,i=new Map,a=()=>{let e=Date.now();for(let[t,n]of i.entries())e-n.timestamp>r&&i.delete(t)},o=()=>{if(i.size<=n)return;let e=null,t=1/0;for(let[n,r]of i.entries())r.timestamp<t&&(t=r.timestamp,e=n);e&&i.delete(e)};return t.Layer.succeed(u,{set:(e,n)=>t.Effect.sync(()=>{i.size%100==0&&a(),i.set(e,{authContext:n,timestamp:Date.now()}),o()}),get:e=>t.Effect.sync(()=>{let t=i.get(e);return t?Date.now()-t.timestamp>r?(i.delete(e),null):t.authContext:null}),delete:e=>t.Effect.sync(()=>{i.delete(e)}),clear:()=>t.Effect.sync(()=>{i.clear()}),size:()=>t.Effect.sync(()=>i.size)})},f=t.Layer.succeed(u,{set:()=>t.Effect.void,get:()=>t.Effect.succeed(null),delete:()=>t.Effect.void,clear:()=>t.Effect.void,size:()=>t.Effect.succeed(0)}),p=e=>e.split(`/`).filter(Boolean),m=e=>{let t=p(e);return t[t.length-1]},h=(e,t)=>e.includes(`${t}/api/`),g=(e,t)=>e.replace(`${t}/api/`,``).split(`/`).filter(Boolean),_=e=>{let t=500,n=`UNKNOWN_ERROR`,r=`Internal server error`,i;if(typeof e==`object`&&e){let a=e;if(`code`in a&&typeof a.code==`string`&&(n=a.code),`message`in a&&typeof a.message==`string`?r=a.message:`body`in a&&typeof a.body==`string`&&(r=a.body),`details`in a&&(i=a.details),`status`in a&&typeof a.status==`number`)t=a.status;else if(`code`in a)switch(a.code){case`FILE_NOT_FOUND`:case`FLOW_JOB_NOT_FOUND`:case`UPLOAD_ID_NOT_FOUND`:t=404;break;case`FLOW_JOB_ERROR`:case`VALIDATION_ERROR`:case`INVALID_METADATA`:case`INVALID_LENGTH`:case`ABORTED`:case`INVALID_TERMINATION`:t=400;break;case`INVALID_OFFSET`:t=409;break;case`ERR_SIZE_EXCEEDED`:case`ERR_MAX_SIZE_EXCEEDED`:t=413;break;case`FILE_NO_LONGER_EXISTS`:t=410;break;case`MISSING_OFFSET`:case`INVALID_CONTENT_TYPE`:t=403;break;default:t=500}`message`in a&&a.message===`Invalid JSON body`&&(t=400,n=`VALIDATION_ERROR`)}let a={status:t,code:n,message:r};return i!==void 0&&(a.details=i),a},v=e=>e[e.length-2],y=e=>({jobId:e[e.length-3],nodeId:e[e.length-1]}),b=e=>({storageId:e.pop(),flowId:e.pop()}),x=({kvStore:e,eventEmitter:n,dataStore:i,bufferedDataStore:a,generateId:o})=>{let s=t.Layer.provide(r.uploadFileKvStore,e),l=t.Layer.provide(i,s),u=a?t.Layer.provide(a,s):t.Layer.empty,d=t.Layer.provide(r.uploadEventEmitter,n),f=t.Layer.mergeAll(l,s,d,...o?[o]:[],u);return t.Layer.provide(c.uploadServer,f)},S=({kvStore:e,eventEmitter:i,flowProvider:a,uploadServer:o})=>{let s=t.Layer.provide(r.flowJobKvStore,e),c=t.Layer.provide(r.flowEventEmitter,i),l=t.Layer.mergeAll(a,c,s,o);return t.Layer.provide(n.flowServer,l)};var C=class extends t.Context.Tag(`AuthContextService`)(){};const w=e=>t.Layer.succeed(C,{getClientId:()=>t.Effect.succeed(e?.clientId??null),getMetadata:()=>t.Effect.succeed(e?.metadata??{}),hasPermission:n=>t.Effect.succeed(e?.permissions?.includes(n)??!1),getAuthContext:()=>t.Effect.succeed(e)}),T=w(null),E=({flowId:e})=>t.Effect.gen(function*(){let r=yield*n.FlowServer,i=yield*(yield*C).getClientId();return i&&(yield*t.Effect.logInfo(`[Flow] Getting flow data: ${e}, client: ${i}`)),{status:200,body:yield*r.getFlowData(e,i)}}),D=({flowId:e,storageId:r,inputs:i})=>t.Effect.gen(function*(){let a=yield*n.FlowServer,o=yield*C,s=yield*u,c=yield*o.getClientId();c?(yield*t.Effect.logInfo(`[Flow] Executing flow: ${e}, storage: ${r}, client: ${c}`),yield*t.Effect.logInfo(JSON.stringify(i,null,2))):(yield*t.Effect.logInfo(`[Flow] Executing flow: ${e}, storage: ${r}`),yield*t.Effect.logInfo(`[Flow] Inputs: ${JSON.stringify(i,null,2)}`)),yield*t.Effect.logInfo(`[Flow] Calling flowServer.runFlow...`);let l=yield*a.runFlow({flowId:e,storageId:r,clientId:c,inputs:i}).pipe(t.Effect.tap(()=>t.Effect.logInfo(`[Flow] runFlow completed successfully`)),t.Effect.tapError(e=>t.Effect.logError(`[Flow] runFlow failed with error: ${e}`))),d=yield*o.getAuthContext();return d&&(yield*s.set(l.id,d)),yield*t.Effect.logInfo(`[Flow] Flow started with jobId: ${l.id}`),{status:200,body:l}}),O=({jobId:e})=>t.Effect.gen(function*(){let r=yield*n.FlowServer,i=yield*C,a=yield*u,o=yield*i.getClientId();if(!e)throw Error(`No job id`);o&&(yield*t.Effect.logInfo(`[Flow] Getting job status: ${e}, client: ${o}`));let s=yield*r.getJobStatus(e);return(s.status===`completed`||s.status===`failed`)&&(yield*a.delete(e),o&&(yield*t.Effect.logInfo(`[Flow] Flow ${s.status}, cleared auth cache: ${e}`))),{status:200,body:s}}),k=({jobId:e,nodeId:r,newData:i})=>t.Effect.gen(function*(){let a=yield*n.FlowServer,o=yield*C,s=yield*u,c=yield*o.getClientId();if(c||=(yield*s.get(e))?.clientId??null,c&&(yield*t.Effect.logInfo(`[Flow] Continuing flow: jobId=${e}, nodeId=${r}, client: ${c}`)),i===void 0)throw Error(`Missing newData`);let l=yield*a.resumeFlow({jobId:e,nodeId:r,newData:i,clientId:c});return(l.status===`completed`||l.status===`failed`)&&(yield*s.delete(e),c&&(yield*t.Effect.logInfo(`[Flow] Flow ${l.status}, cleared auth cache: ${e}`))),{status:200,body:l}}),A=({jobId:e})=>t.Effect.gen(function*(){let r=yield*n.FlowServer,i=yield*C,a=yield*u,o=yield*i.getClientId();o||=(yield*a.get(e))?.clientId??null,o&&(yield*t.Effect.logInfo(`[Flow] Pausing flow: jobId=${e}, client: ${o}`));let s=yield*r.pauseFlow(e,o);return o&&(yield*t.Effect.logInfo(`[Flow] Flow paused: ${e}, status: ${s.status}`)),{status:200,body:s}}),j=({jobId:e})=>t.Effect.gen(function*(){let r=yield*n.FlowServer,i=yield*C,a=yield*u;if(!e)throw Error(`No job id`);let o=yield*i.getClientId();o||=(yield*a.get(e))?.clientId??null,o&&(yield*t.Effect.logInfo(`[Flow] Cancelling flow: jobId=${e}, client: ${o}`));let s=yield*r.cancelFlow(e,o);return yield*a.delete(e),o&&(yield*t.Effect.logInfo(`[Flow] Flow cancelled, cleared auth cache: ${e}`)),{status:200,body:s}});var M=class extends Error{constructor(e,t=500,n=`INTERNAL_ERROR`){super(e),this.statusCode=t,this.errorCode=n,this.name=`AdapterError`}},N=class extends M{constructor(e){super(e,400,`VALIDATION_ERROR`),this.name=`ValidationError`}},P=class extends M{constructor(e){super(`${e} not found`,404,`NOT_FOUND`),this.name=`NotFoundError`}},F=class extends M{constructor(e){super(e,400,`BAD_REQUEST`),this.name=`BadRequestError`}};const I=e=>({error:e.message,code:e.errorCode,timestamp:new Date().toISOString()}),L=e=>{let t={error:e.body,code:e.code,timestamp:new Date().toISOString()};return e.details!==void 0&&(t.details=e.details),t},R=(e=`Internal server error`)=>({error:e,code:`INTERNAL_ERROR`,timestamp:new Date().toISOString()}),ee=e=>t.Effect.gen(function*(){let n=yield*c.UploadServer,a=yield*C,o=yield*u,s=yield*a.getClientId();s&&(yield*t.Effect.logInfo(`[Upload] Creating upload for client: ${s}`));let l=yield*t.Effect.sync(()=>r.inputFileSchema.safeParse(e.data));if(!l.success)return yield*t.Effect.fail(new N(`Invalid input file schema`));if(l.data.checksumAlgorithm&&!(0,i.isSupportedAlgorithm)(l.data.checksumAlgorithm))return yield*t.Effect.fail(new N(`Unsupported checksum algorithm: ${l.data.checksumAlgorithm}. Supported algorithms: sha256`));let d=yield*n.createUpload(l.data,s),f=yield*a.getAuthContext();return f&&(yield*o.set(d.id,f)),s&&(yield*t.Effect.logInfo(`[Upload] Upload created: ${d.id} for client: ${s}`)),{status:200,body:d}}),z=({storageId:e})=>t.Effect.gen(function*(){let t=yield*c.UploadServer,n=yield*(yield*C).getClientId();return{status:200,body:{storageId:e,capabilities:yield*t.getCapabilities(e,n),timestamp:new Date().toISOString()}}}),B=({uploadId:e})=>t.Effect.gen(function*(){return{status:200,body:yield*(yield*c.UploadServer).getUpload(e)}}),V=e=>t.Effect.gen(function*(){let n=yield*c.UploadServer,r=yield*C,i=yield*u,a=yield*s.MetricsService,{uploadId:o,data:l}=e,d=yield*r.getClientId(),f=yield*r.getMetadata();if(!d){let e=yield*i.get(o);d=e?.clientId??null,f=e?.metadata??{}}d&&(yield*t.Effect.logInfo(`[Upload] Uploading chunk for upload: ${o}, client: ${d}`));let p=yield*n.uploadChunk(o,d,l);return p.size&&p.offset>=p.size&&(yield*i.delete(o),d&&(yield*t.Effect.logInfo(`[Upload] Upload completed, cleared auth cache: ${o}`)),d&&p.size?(yield*t.Effect.logInfo(`[Upload] Recording metrics for org: ${d}, size: ${p.size}`),yield*t.Effect.forkDaemon(a.recordUpload(d,p.size,f))):yield*t.Effect.logWarning(`[Upload] Cannot record metrics - missing organizationId or size`)),d&&(yield*t.Effect.logInfo(`[Upload] Chunk uploaded for upload: ${o}, client: ${d}`)),{status:200,body:p}}),H=e=>t.Effect.gen(function*(){switch(e.type){case`create-upload`:return yield*ee(e);case`get-capabilities`:return yield*z(e);case`get-upload`:return yield*B(e);case`upload-chunk`:return yield*V(e);case`get-flow`:return yield*E(e);case`run-flow`:return yield*D(e);case`job-status`:return yield*O(e);case`resume-flow`:return yield*k(e);case`pause-flow`:return yield*A(e);case`cancel-flow`:return yield*j(e);case`not-found`:return{status:404,headers:{"Content-Type":`application/json`},body:{error:`Not found`}};case`bad-request`:return{status:400,body:{error:`Bad request`,message:e.message}};case`method-not-allowed`:return{status:405,headers:{"Content-Type":`application/json`},body:{error:`Method not allowed`}};case`unsupported-content-type`:return{status:415,headers:{"Content-Type":`application/json`},body:{error:`Unsupported content type`}}}}),U=async({flows:e,dataStore:c,kvStore:l,plugins:u=[],eventEmitter:f,eventBroadcaster:p=a.memoryEventBroadcaster,withTracing:m=!1,baseUrl:h=`uploadista`,generateId:g=i.GenerateIdLive,metricsLayer:v,bufferedDataStore:y,adapter:b,authCacheConfig:C})=>{let T=f??(0,o.webSocketEventEmitter)(p),E=h.endsWith(`/`)?h.slice(0,-1):h,D=t.Layer.effect(n.FlowProvider,t.Effect.succeed({getFlow:(t,n)=>e(t,n)}));if(!T)throw Error(`eventEmitter is required. Provide an event emitter layer in the configuration.`);let O=x({kvStore:l,eventEmitter:T,dataStore:await(0,r.createDataStoreLayer)(c),bufferedDataStore:y,generateId:g}),k=S({kvStore:l,eventEmitter:T,flowProvider:D,uploadServer:O}),A=d(C),j=v??s.NoOpMetricsServiceLive,M=t.Layer.mergeAll(O,k,j,A,...u),N=t.ManagedRuntime.make(M);return{handler:async e=>{let r=t.Effect.gen(function*(){let r=yield*b.extractRequest(e,{baseUrl:E}),i=null;if(b.runAuthMiddleware){let n=yield*b.runAuthMiddleware(e).pipe(t.Effect.timeout(`5 seconds`),t.Effect.catchAll(()=>(console.error(`Auth middleware timeout exceeded (5 seconds)`),t.Effect.succeed({_tag:`TimeoutError`}))),t.Effect.catchAllCause(e=>(console.error(`Auth middleware error:`,e),t.Effect.succeed({_tag:`AuthError`,error:e}))));if(n&&typeof n==`object`&&`_tag`in n&&n._tag===`TimeoutError`)return yield*b.sendResponse({status:503,headers:{"Content-Type":`application/json`},body:{error:`Authentication service unavailable`,message:`Authentication took too long to respond. Please try again.`}},e);if(n&&typeof n==`object`&&`_tag`in n&&n._tag===`AuthError`)return yield*b.sendResponse({status:500,headers:{"Content-Type":`application/json`},body:{error:`Internal Server Error`,message:`An error occurred during authentication`}},e);if(n===null)return yield*b.sendResponse({status:401,headers:{"Content-Type":`application/json`},body:{error:`Unauthorized`,message:`Invalid credentials`}},e);i=n}let a=w(i),o=[];if(b.extractWaitUntil){let r=b.extractWaitUntil(e);r&&o.push(t.Layer.succeed(n.FlowWaitUntil,r))}let s=t.Layer.mergeAll(a,A,j,...u,...o);if(r.type===`not-found`)return yield*b.sendResponse({type:`not-found`,status:404,headers:{"Content-Type":`application/json`},body:{error:`Not found`}},e);let c=yield*H(r).pipe(t.Effect.provide(s));return yield*b.sendResponse(c,e)}).pipe(t.Effect.catchAll(t=>{let n=_(t),r={code:n.code,message:n.message};n.details!==void 0&&(r.details=n.details);let i={status:n.status,headers:{"Content-Type":`application/json`},body:r};return b.sendResponse(i,e)}));return m?N.runPromise(r.pipe(t.Effect.provide(s.NodeSdkLive))):N.runPromise(r)},websocketHandler:await N.runPromise(b.webSocketHandler({baseUrl:E})),baseUrl:E,dispose:()=>N.dispose()}};async function W(e){return U(e)}function G(e){return e}function K(e){return e}const q={ImagePlugin:{packageName:`@uploadista/flow-images-sharp`,variableName:`sharpImagePlugin`},ImageAiPlugin:{packageName:`@uploadista/flow-images-replicate`,variableName:`replicateImagePlugin`},ZipPlugin:{packageName:`@uploadista/flow-utility-zipjs`,variableName:`zipPlugin`},CredentialProvider:{packageName:`@uploadista/core`,variableName:`credentialProviderLayer`}};function J(e){try{let t=e;if(t._tag)return t._tag;if(t.constructor?.name)return t.constructor.name;if(t.context?.services){let e=Array.from(t.context.services.keys());if(e.length>0){let t=e[0];if(t.key)return t.key}}return null}catch{return null}}function Y(e){return e.map(e=>J(e)).filter(e=>e!==null)}function X(e){let{plugins:t,expectedServices:n=[]}=e,r=Y(t),i=n.filter(e=>!r.includes(e));return i.length===0?{success:!0}:{success:!1,required:n,provided:r,missing:i,suggestions:i.map(e=>{let t=q[e];return t?{name:e,packageName:t.packageName,importStatement:`import { ${t.variableName} } from '${t.packageName}';`}:null}).filter(e=>e!==null)}}function Z(e){let t=[`Server initialization failed: Missing required plugins`,``,`Required: ${e.required.join(`, `)}`,`Provided: ${e.provided.length>0?e.provided.join(`, `):`(none)`}`,`Missing: ${e.missing.join(`, `)}`,``];if(e.suggestions.length>0){t.push(`Add the missing plugins to your configuration:`),t.push(``);for(let n of e.suggestions)t.push(` ${n.importStatement}`);t.push(``),t.push(` const server = await createUploadistaServer({`),t.push(` plugins: [${[...e.provided,...e.missing.map(e=>q[e]?.variableName||e)].join(`, `)}],`),t.push(` // ...`),t.push(` });`)}else t.push(`Note: Could not determine package names for missing plugins.`),t.push(`Please ensure all required plugin layers are provided.`);return t.join(`
|
|
2
|
+
`)}function Q(e){return t.Effect.sync(()=>{let t=X(e);if(!t.success){let e=Z(t);throw Error(e)}})}function te(e){let t=X(e);if(!t.success){let e=Z(t);throw Error(e)}}const ne=(e,n,r)=>t.Effect.gen(function*(){if(!n){yield*t.Effect.sync(()=>{r.send(JSON.stringify({type:`error`,message:`Job ID is required for flow event subscription`,code:`MISSING_JOB_ID`}))});return}yield*e.subscribeToFlowEvents(n,r)}),re=(e,n)=>t.Effect.gen(function*(){n&&(yield*e.unsubscribeFromFlowEvents(n))}),$=(e,n,r)=>t.Effect.gen(function*(){if(!n){yield*t.Effect.sync(()=>{r.send(JSON.stringify({type:`error`,message:`Upload ID is required for upload event subscription`,code:`MISSING_UPLOAD_ID`}))});return}yield*e.subscribeToUploadEvents(n,r)}),ie=(e,n)=>t.Effect.gen(function*(){n&&(yield*e.unsubscribeFromUploadEvents(n))}),ae=(e,n,r)=>{let{connection:i,isFlowRoute:a,isUploadRoute:o,jobId:s,uploadId:c,eventId:u}=e;return t.Effect.gen(function*(){a&&(yield*ne(r,s,i)),o&&(yield*$(n,c,i)),i.send(JSON.stringify({type:`connection`,message:`Uploadista WebSocket connected`,id:u,jobId:s,uploadId:c,timestamp:new Date().toISOString()}))}).pipe(t.Effect.catchAll(e=>t.Effect.sync(()=>{console.error(`Error subscribing to events:`,e);let t=e instanceof l.UploadistaError?e.body:`Failed to subscribe to events`;i.send(JSON.stringify({type:`error`,message:t,code:e instanceof l.UploadistaError?e.code:`SUBSCRIPTION_ERROR`}))})))},oe=(e,n)=>t.Effect.sync(()=>{try{JSON.parse(e).type===`ping`&&n.send(JSON.stringify({type:`pong`,timestamp:new Date().toISOString()}))}catch(e){console.error(`Error handling WebSocket message:`,e),n.send(JSON.stringify({type:`error`,message:`Invalid message format`}))}}),se=(e,n,r)=>{let{isFlowRoute:i,isUploadRoute:a,jobId:o,uploadId:s}=e;return t.Effect.gen(function*(){i&&(yield*re(r,o)),a&&(yield*ie(n,s))}).pipe(t.Effect.catchAll(e=>t.Effect.sync(()=>{console.error(`Error unsubscribing from events:`,e instanceof l.UploadistaError?e.body:e)})))},ce=(e,n)=>t.Effect.sync(()=>{console.error(`WebSocket error for event ${n}:`,e)});exports.AdapterError=M,exports.AuthCacheService=u,exports.AuthCacheServiceLive=d,exports.AuthContextService=C,exports.AuthContextServiceLive=w,exports.BadRequestError=F,exports.NoAuthCacheServiceLive=f,exports.NoAuthContextServiceLive=T,exports.NotFoundError=P,exports.ValidationError=N,exports.createErrorResponseBody=I,exports.createFlowServerLayer=S,exports.createGenericErrorResponseBody=R,exports.createTypeSafeServer=W,exports.createUploadServerLayer=x,exports.createUploadistaErrorResponseBody=L,exports.createUploadistaServer=U,exports.defineFlow=G,exports.defineSimpleFlow=K,exports.extractFlowAndStorageId=b,exports.extractJobAndNodeId=y,exports.extractJobIdFromStatus=v,exports.extractServiceIdentifiers=Y,exports.formatPluginValidationError=Z,exports.getAuthCredentials=e.t,exports.getLastSegment=m,exports.getRouteSegments=g,exports.handleFlowError=_,exports.handleWebSocketClose=se,exports.handleWebSocketError=ce,exports.handleWebSocketMessage=oe,exports.handleWebSocketOpen=ae,exports.hasBasePath=h,exports.parseUrlSegments=p,exports.validatePluginRequirements=X,exports.validatePluginRequirementsEffect=Q,exports.validatePluginsOrThrow=te;
|