@uploadista/server 0.0.9 → 0.0.11
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 +644 -21
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +643 -20
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +14 -12
- 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
package/PLUGIN_TYPING.md
ADDED
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
# Plugin Typing System
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
The Uploadista server has a sophisticated typing system that allows flows to depend on plugins while maintaining type safety. This document explains how the typing works and how to use it effectively.
|
|
6
|
+
|
|
7
|
+
## Core Concepts
|
|
8
|
+
|
|
9
|
+
### Flow Requirements
|
|
10
|
+
|
|
11
|
+
Flows can have dependencies on services (like `ImagePlugin`) that are provided through Effect's dependency injection system. The `Flow` type has a third generic parameter `TRequirements` that captures these dependencies:
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
type Flow<
|
|
15
|
+
TFlowInputSchema extends z.ZodSchema<any>,
|
|
16
|
+
TFlowOutputSchema extends z.ZodSchema<any>,
|
|
17
|
+
TRequirements // Services that flow nodes need
|
|
18
|
+
>
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### Plugin Layers
|
|
22
|
+
|
|
23
|
+
Plugins are Effect layers that provide services to flows. They are typed as:
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
Layer.Layer<TOutput, TError, TRequirements>
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Where:
|
|
30
|
+
- `TOutput`: The service(s) this plugin provides
|
|
31
|
+
- `TError`: Errors the plugin can produce (usually `never`)
|
|
32
|
+
- `TRequirements`: Services this plugin needs (can be `never` or other services)
|
|
33
|
+
|
|
34
|
+
## Type Parameters
|
|
35
|
+
|
|
36
|
+
The `createUploadistaServer` function has these key type parameters:
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
createUploadistaServer<
|
|
40
|
+
TContext, // Framework-specific context
|
|
41
|
+
TResponse, // Framework-specific response
|
|
42
|
+
TWebSocket, // WebSocket handler type
|
|
43
|
+
TFlows, // Flow function type
|
|
44
|
+
TPlugins // Plugin layers tuple
|
|
45
|
+
>
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### TFlows
|
|
49
|
+
|
|
50
|
+
The flows function type that returns Effects producing Flows:
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
TFlows extends (
|
|
54
|
+
flowId: string,
|
|
55
|
+
clientId: string | null,
|
|
56
|
+
) => Effect.Effect<
|
|
57
|
+
Flow<z.ZodSchema<unknown>, z.ZodSchema<unknown>, any>,
|
|
58
|
+
UploadistaError,
|
|
59
|
+
any // Flow requirements - can be ImagePlugin, etc.
|
|
60
|
+
>
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The `any` here allows flows to have any requirements, which will be satisfied by plugins.
|
|
64
|
+
|
|
65
|
+
### TPlugins
|
|
66
|
+
|
|
67
|
+
A readonly tuple of plugin layers:
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
TPlugins extends readonly Layer.Layer<any, never, any>[]
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The type parameters are:
|
|
74
|
+
- First `any`: Plugins can provide any services
|
|
75
|
+
- `never`: Plugins should not error
|
|
76
|
+
- Second `any`: Plugins can have requirements (satisfied by other plugins or at runtime)
|
|
77
|
+
|
|
78
|
+
## Usage Examples
|
|
79
|
+
|
|
80
|
+
### Basic Flow Without Plugins
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
const server = await createUploadistaServer({
|
|
84
|
+
flows: (flowId) => Effect.succeed(basicFlow),
|
|
85
|
+
dataStore: { type: "s3", config: { bucket: "uploads" } },
|
|
86
|
+
kvStore: redisKvStore,
|
|
87
|
+
adapter: honoAdapter({ /* ... */ })
|
|
88
|
+
});
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Flow With Image Plugin
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
import { sharpImagePlugin } from "@uploadista/flow-images-sharp";
|
|
95
|
+
|
|
96
|
+
// Create a flow that uses ImagePlugin
|
|
97
|
+
const imageFlow = Effect.gen(function* () {
|
|
98
|
+
const imageService = yield* ImagePlugin;
|
|
99
|
+
|
|
100
|
+
return createFlow({
|
|
101
|
+
id: "resize-flow",
|
|
102
|
+
nodes: [
|
|
103
|
+
yield* createResizeNode("resize", {
|
|
104
|
+
width: 800,
|
|
105
|
+
height: 600,
|
|
106
|
+
fit: "cover"
|
|
107
|
+
})
|
|
108
|
+
],
|
|
109
|
+
// ... edges, schemas
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Create server with plugin
|
|
114
|
+
const server = await createUploadistaServer({
|
|
115
|
+
flows: (flowId) => {
|
|
116
|
+
if (flowId === "resize") return imageFlow;
|
|
117
|
+
return Effect.fail(new UploadistaError({
|
|
118
|
+
code: "FLOW_NOT_FOUND"
|
|
119
|
+
}));
|
|
120
|
+
},
|
|
121
|
+
plugins: [sharpImagePlugin], // Provides ImagePlugin
|
|
122
|
+
dataStore: { type: "s3", config: { bucket: "uploads" } },
|
|
123
|
+
kvStore: redisKvStore,
|
|
124
|
+
adapter: honoAdapter({ /* ... */ })
|
|
125
|
+
});
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Multiple Plugins
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
const server = await createUploadistaServer({
|
|
132
|
+
flows: getFlowById,
|
|
133
|
+
plugins: [
|
|
134
|
+
sharpImagePlugin, // Provides ImagePlugin
|
|
135
|
+
virusScanPlugin, // Provides VirusScanService
|
|
136
|
+
customProcessingPlugin // Provides CustomProcessingService
|
|
137
|
+
] as const, // 'as const' preserves tuple type
|
|
138
|
+
dataStore: { type: "s3", config: { bucket: "uploads" } },
|
|
139
|
+
kvStore: redisKvStore,
|
|
140
|
+
adapter: honoAdapter({ /* ... */ })
|
|
141
|
+
});
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Implementation Details
|
|
145
|
+
|
|
146
|
+
### Layer Composition
|
|
147
|
+
|
|
148
|
+
The server merges all layers together:
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
const serverLayer = Layer.mergeAll(
|
|
152
|
+
uploadServerLayer, // Provides UploadServer
|
|
153
|
+
flowServerLayer, // Provides FlowServer
|
|
154
|
+
metricsLayer, // Provides MetricsService
|
|
155
|
+
authCacheLayer, // Provides AuthCacheService
|
|
156
|
+
...plugins // Provides plugin services (ImagePlugin, etc.)
|
|
157
|
+
);
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Type Assertions
|
|
161
|
+
|
|
162
|
+
Due to the dynamic nature of plugins, we use type assertions to bridge Effect's strict typing with the plugin system:
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
const serverLayer = serverLayerRaw as unknown as Layer.Layer<any, never, any>;
|
|
166
|
+
const managedRuntime = ManagedRuntime.make(serverLayer as any);
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
These assertions are necessary because:
|
|
170
|
+
1. Plugin requirements are determined at runtime based on which plugins are provided
|
|
171
|
+
2. TypeScript cannot statically verify all possible plugin combinations
|
|
172
|
+
3. The Effect runtime handles requirement resolution dynamically
|
|
173
|
+
|
|
174
|
+
### Request Context Layer
|
|
175
|
+
|
|
176
|
+
For each request, plugin services are merged with request-specific context:
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
const requestContextLayer = Layer.mergeAll(
|
|
180
|
+
authContextLayer, // Auth for this request
|
|
181
|
+
authCacheLayer, // Shared auth cache
|
|
182
|
+
effectiveMetricsLayer, // Metrics service
|
|
183
|
+
...plugins, // All plugin services
|
|
184
|
+
...waitUntilLayers // Cloudflare waitUntil if available
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
// Flow execution with all dependencies
|
|
188
|
+
const response = yield* handleUploadistaRequest(uploadistaRequest)
|
|
189
|
+
.pipe(Effect.provide(requestContextLayer));
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
This ensures that:
|
|
193
|
+
- Flow nodes have access to all plugin services (ImagePlugin, etc.)
|
|
194
|
+
- Each request has isolated auth context
|
|
195
|
+
- Metrics and caching are shared across requests
|
|
196
|
+
|
|
197
|
+
## Best Practices
|
|
198
|
+
|
|
199
|
+
### 1. Use `as const` for Plugin Arrays
|
|
200
|
+
|
|
201
|
+
```typescript
|
|
202
|
+
const plugins = [
|
|
203
|
+
imagePlugin,
|
|
204
|
+
scanPlugin
|
|
205
|
+
] as const;
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
This preserves the tuple type and enables better type inference.
|
|
209
|
+
|
|
210
|
+
### 2. Type Flow Requirements Explicitly
|
|
211
|
+
|
|
212
|
+
When creating flows, explicitly type the requirements:
|
|
213
|
+
|
|
214
|
+
```typescript
|
|
215
|
+
const myFlow: Effect.Effect<
|
|
216
|
+
Flow<InputSchema, OutputSchema, ImagePlugin>,
|
|
217
|
+
UploadistaError,
|
|
218
|
+
never
|
|
219
|
+
> = Effect.gen(function* () {
|
|
220
|
+
const imageService = yield* ImagePlugin;
|
|
221
|
+
// ...
|
|
222
|
+
});
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### 3. Document Plugin Dependencies
|
|
226
|
+
|
|
227
|
+
Always document which plugins a flow requires:
|
|
228
|
+
|
|
229
|
+
```typescript
|
|
230
|
+
/**
|
|
231
|
+
* Image processing flow
|
|
232
|
+
*
|
|
233
|
+
* @requires ImagePlugin - For resize and optimize operations
|
|
234
|
+
* @requires MetricsService - For performance tracking
|
|
235
|
+
*/
|
|
236
|
+
export const imageProcessingFlow = /* ... */;
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### 4. Provide Plugin Implementations
|
|
240
|
+
|
|
241
|
+
When creating a server, ensure all required plugins are provided:
|
|
242
|
+
|
|
243
|
+
```typescript
|
|
244
|
+
// ❌ Bad: Flow requires ImagePlugin but none provided
|
|
245
|
+
createUploadistaServer({
|
|
246
|
+
flows: flowThatNeedsImagePlugin,
|
|
247
|
+
plugins: [] // Missing ImagePlugin!
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// ✅ Good: All requirements satisfied
|
|
251
|
+
createUploadistaServer({
|
|
252
|
+
flows: flowThatNeedsImagePlugin,
|
|
253
|
+
plugins: [sharpImagePlugin] // Provides ImagePlugin
|
|
254
|
+
});
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
## Advanced: Custom Plugins
|
|
258
|
+
|
|
259
|
+
### Creating a Plugin
|
|
260
|
+
|
|
261
|
+
```typescript
|
|
262
|
+
import { Context, Layer, Effect } from "effect";
|
|
263
|
+
|
|
264
|
+
// 1. Define the service interface
|
|
265
|
+
export interface CustomService {
|
|
266
|
+
process: (data: Uint8Array) => Effect.Effect<Uint8Array, Error>;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// 2. Create the service tag
|
|
270
|
+
export class CustomService extends Context.Tag("CustomService")<
|
|
271
|
+
CustomService,
|
|
272
|
+
CustomService
|
|
273
|
+
>() {}
|
|
274
|
+
|
|
275
|
+
// 3. Create the implementation layer
|
|
276
|
+
export const customServiceLive = Layer.succeed(
|
|
277
|
+
CustomService,
|
|
278
|
+
{
|
|
279
|
+
process: (data) => Effect.sync(() => {
|
|
280
|
+
// Your processing logic
|
|
281
|
+
return data;
|
|
282
|
+
})
|
|
283
|
+
}
|
|
284
|
+
);
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
### Using Custom Plugin in Flow
|
|
288
|
+
|
|
289
|
+
```typescript
|
|
290
|
+
const flowWithCustomPlugin = Effect.gen(function* () {
|
|
291
|
+
const customService = yield* CustomService;
|
|
292
|
+
|
|
293
|
+
return createFlow({
|
|
294
|
+
nodes: [
|
|
295
|
+
yield* createTransformNode({
|
|
296
|
+
id: "custom",
|
|
297
|
+
name: "Custom Processing",
|
|
298
|
+
transform: (input) => customService.process(input)
|
|
299
|
+
})
|
|
300
|
+
],
|
|
301
|
+
// ...
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
### Adding to Server
|
|
307
|
+
|
|
308
|
+
```typescript
|
|
309
|
+
const server = await createUploadistaServer({
|
|
310
|
+
flows: () => flowWithCustomPlugin,
|
|
311
|
+
plugins: [customServiceLive],
|
|
312
|
+
// ...
|
|
313
|
+
});
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
## Troubleshooting
|
|
317
|
+
|
|
318
|
+
### Flow Requirements Not Satisfied
|
|
319
|
+
|
|
320
|
+
**Error**: "Service not found" at runtime
|
|
321
|
+
|
|
322
|
+
**Solution**: Ensure the plugin providing the required service is in the `plugins` array
|
|
323
|
+
|
|
324
|
+
```typescript
|
|
325
|
+
// Add the missing plugin
|
|
326
|
+
plugins: [sharpImagePlugin] // Provides ImagePlugin
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
### Type Errors with Plugins
|
|
330
|
+
|
|
331
|
+
**Error**: Type mismatch when passing plugins
|
|
332
|
+
|
|
333
|
+
**Solution**: Use `as const` and ensure plugin types match `Layer.Layer<any, never, any>`
|
|
334
|
+
|
|
335
|
+
```typescript
|
|
336
|
+
const plugins = [myPlugin] as const;
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
### Runtime Layer Composition Errors
|
|
340
|
+
|
|
341
|
+
**Error**: Layer requirements not satisfied
|
|
342
|
+
|
|
343
|
+
**Solution**: Check that plugin dependencies form a valid dependency graph (no circular dependencies)
|
|
344
|
+
|
|
345
|
+
```typescript
|
|
346
|
+
// ❌ Bad: Circular dependency
|
|
347
|
+
const pluginA = Layer.effect(ServiceA,
|
|
348
|
+
Effect.gen(function* () {
|
|
349
|
+
yield* ServiceB; // Depends on B
|
|
350
|
+
return { /* ... */ };
|
|
351
|
+
})
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
const pluginB = Layer.effect(ServiceB,
|
|
355
|
+
Effect.gen(function* () {
|
|
356
|
+
yield* ServiceA; // Depends on A - circular!
|
|
357
|
+
return { /* ... */ };
|
|
358
|
+
})
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
// ✅ Good: Linear dependency
|
|
362
|
+
const pluginA = Layer.succeed(ServiceA, { /* ... */ });
|
|
363
|
+
const pluginB = Layer.effect(ServiceB,
|
|
364
|
+
Effect.gen(function* () {
|
|
365
|
+
yield* ServiceA; // Only B depends on A
|
|
366
|
+
return { /* ... */ };
|
|
367
|
+
})
|
|
368
|
+
);
|
|
369
|
+
```
|