@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,388 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type tests for plugin-types.ts
|
|
3
|
+
*
|
|
4
|
+
* These tests verify that plugin type utilities correctly validate plugin
|
|
5
|
+
* requirements and generate appropriate error messages at compile time.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Effect, Layer } from "effect";
|
|
9
|
+
import { expectType } from "tsd";
|
|
10
|
+
import type {
|
|
11
|
+
ExtractFlowPluginRequirements,
|
|
12
|
+
InferFlowRequirements,
|
|
13
|
+
PluginServices,
|
|
14
|
+
PluginTuple,
|
|
15
|
+
TypeSafeFlowFunction,
|
|
16
|
+
ValidatePlugins,
|
|
17
|
+
} from "../src/core/plugin-types";
|
|
18
|
+
|
|
19
|
+
// ============================================================================
|
|
20
|
+
// Test Services and Layers
|
|
21
|
+
// ============================================================================
|
|
22
|
+
|
|
23
|
+
class ImagePlugin {
|
|
24
|
+
readonly _tag = "ImagePlugin";
|
|
25
|
+
resize(data: Buffer, width: number): Buffer {
|
|
26
|
+
return data;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
class ZipPlugin {
|
|
31
|
+
readonly _tag = "ZipPlugin";
|
|
32
|
+
compress(files: Buffer[]): Buffer {
|
|
33
|
+
return Buffer.from([]);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
class VideoPlugin {
|
|
38
|
+
readonly _tag = "VideoPlugin";
|
|
39
|
+
transcode(data: Buffer): Buffer {
|
|
40
|
+
return data;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Create test layers
|
|
45
|
+
const imageLayer = Layer.succeed(ImagePlugin, new ImagePlugin());
|
|
46
|
+
const zipLayer = Layer.succeed(ZipPlugin, new ZipPlugin());
|
|
47
|
+
const videoLayer = Layer.succeed(VideoPlugin, new VideoPlugin());
|
|
48
|
+
|
|
49
|
+
// ============================================================================
|
|
50
|
+
// PluginTuple Tests
|
|
51
|
+
// ============================================================================
|
|
52
|
+
|
|
53
|
+
// Test: Should accept valid plugin tuples
|
|
54
|
+
expectType<PluginTuple>([imageLayer] as const);
|
|
55
|
+
expectType<PluginTuple>([imageLayer, zipLayer] as const);
|
|
56
|
+
expectType<PluginTuple>([imageLayer, zipLayer, videoLayer] as const);
|
|
57
|
+
|
|
58
|
+
// Test: Should accept empty tuple
|
|
59
|
+
expectType<PluginTuple>([] as const);
|
|
60
|
+
|
|
61
|
+
// ============================================================================
|
|
62
|
+
// PluginServices Tests
|
|
63
|
+
// ============================================================================
|
|
64
|
+
|
|
65
|
+
// Test: Should extract union of services from plugin tuple
|
|
66
|
+
expectType<ImagePlugin>({} as PluginServices<[typeof imageLayer]>);
|
|
67
|
+
|
|
68
|
+
expectType<ImagePlugin | ZipPlugin>(
|
|
69
|
+
{} as PluginServices<[typeof imageLayer, typeof zipLayer]>,
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
expectType<ImagePlugin | ZipPlugin | VideoPlugin>(
|
|
73
|
+
{} as PluginServices<[typeof imageLayer, typeof zipLayer, typeof videoLayer]>,
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
// Test: Should return never for empty tuple
|
|
77
|
+
expectType<never>({} as PluginServices<[]>);
|
|
78
|
+
|
|
79
|
+
// ============================================================================
|
|
80
|
+
// TypeSafeFlowFunction Tests
|
|
81
|
+
// ============================================================================
|
|
82
|
+
|
|
83
|
+
// Test: Should accept flow functions with matching requirements
|
|
84
|
+
const imageFlowFn: TypeSafeFlowFunction<ImagePlugin> = (
|
|
85
|
+
flowId: string,
|
|
86
|
+
clientId: string,
|
|
87
|
+
) =>
|
|
88
|
+
Effect.gen(function* () {
|
|
89
|
+
const image = yield* ImagePlugin;
|
|
90
|
+
return {} as any;
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
expectType<TypeSafeFlowFunction<ImagePlugin>>(imageFlowFn);
|
|
94
|
+
|
|
95
|
+
const multiPluginFlowFn: TypeSafeFlowFunction<ImagePlugin | ZipPlugin> = (
|
|
96
|
+
flowId: string,
|
|
97
|
+
clientId: string,
|
|
98
|
+
) =>
|
|
99
|
+
Effect.gen(function* () {
|
|
100
|
+
const image = yield* ImagePlugin;
|
|
101
|
+
const zip = yield* ZipPlugin;
|
|
102
|
+
return {} as any;
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
expectType<TypeSafeFlowFunction<ImagePlugin | ZipPlugin>>(multiPluginFlowFn);
|
|
106
|
+
|
|
107
|
+
// Test: Should accept flow with no requirements
|
|
108
|
+
const noPluginFlowFn: TypeSafeFlowFunction<never> = (
|
|
109
|
+
flowId: string,
|
|
110
|
+
clientId: string,
|
|
111
|
+
) => Effect.succeed({} as any);
|
|
112
|
+
|
|
113
|
+
expectType<TypeSafeFlowFunction<never>>(noPluginFlowFn);
|
|
114
|
+
|
|
115
|
+
// ============================================================================
|
|
116
|
+
// ValidatePlugins Tests - Success Cases
|
|
117
|
+
// ============================================================================
|
|
118
|
+
|
|
119
|
+
// Test: Should return true when all requirements are satisfied
|
|
120
|
+
type ValidSinglePlugin = ValidatePlugins<[typeof imageLayer], ImagePlugin>;
|
|
121
|
+
expectType<true>({} as ValidSinglePlugin);
|
|
122
|
+
|
|
123
|
+
type ValidMultiplePlugins = ValidatePlugins<
|
|
124
|
+
[typeof imageLayer, typeof zipLayer],
|
|
125
|
+
ImagePlugin | ZipPlugin
|
|
126
|
+
>;
|
|
127
|
+
expectType<true>({} as ValidMultiplePlugins);
|
|
128
|
+
|
|
129
|
+
// Test: Should return true when plugins provide more than required
|
|
130
|
+
type ValidExtraPlugins = ValidatePlugins<
|
|
131
|
+
[typeof imageLayer, typeof zipLayer, typeof videoLayer],
|
|
132
|
+
ImagePlugin | ZipPlugin
|
|
133
|
+
>;
|
|
134
|
+
expectType<true>({} as ValidExtraPlugins);
|
|
135
|
+
|
|
136
|
+
// Test: Should return true when no requirements
|
|
137
|
+
type ValidNoRequirements = ValidatePlugins<[typeof imageLayer], never>;
|
|
138
|
+
expectType<true>({} as ValidNoRequirements);
|
|
139
|
+
|
|
140
|
+
type ValidEmptyPluginsNoRequirements = ValidatePlugins<[], never>;
|
|
141
|
+
expectType<true>({} as ValidEmptyPluginsNoRequirements);
|
|
142
|
+
|
|
143
|
+
// ============================================================================
|
|
144
|
+
// ValidatePlugins Tests - Error Cases
|
|
145
|
+
// ============================================================================
|
|
146
|
+
|
|
147
|
+
// Test: Should return error object when plugins are missing
|
|
148
|
+
type MissingSinglePlugin = ValidatePlugins<[], ImagePlugin>;
|
|
149
|
+
|
|
150
|
+
// Should NOT be true
|
|
151
|
+
type MissingSinglePluginIsTrue = MissingSinglePlugin extends true
|
|
152
|
+
? true
|
|
153
|
+
: false;
|
|
154
|
+
expectType<false>({} as MissingSinglePluginIsTrue);
|
|
155
|
+
|
|
156
|
+
// Should have error properties
|
|
157
|
+
expectType<"MISSING_REQUIRED_PLUGINS">(
|
|
158
|
+
{} as MissingSinglePlugin extends { readonly __error: infer E } ? E : never,
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
expectType<"Missing required plugins. Check __missing field for details.">(
|
|
162
|
+
{} as MissingSinglePlugin extends { readonly __message: infer M } ? M : never,
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
expectType<ImagePlugin>(
|
|
166
|
+
{} as MissingSinglePlugin extends { readonly __required: infer R }
|
|
167
|
+
? R
|
|
168
|
+
: never,
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
expectType<never>(
|
|
172
|
+
{} as MissingSinglePlugin extends { readonly __provided: infer P }
|
|
173
|
+
? P
|
|
174
|
+
: never,
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
expectType<ImagePlugin>(
|
|
178
|
+
{} as MissingSinglePlugin extends { readonly __missing: infer M } ? M : never,
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
expectType<"Add the missing plugins to your server configuration's plugins array.">(
|
|
182
|
+
{} as MissingSinglePlugin extends { readonly __hint: infer H } ? H : never,
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
// Test: Should show partial missing plugins
|
|
186
|
+
type PartiallyMissingPlugins = ValidatePlugins<
|
|
187
|
+
[typeof imageLayer],
|
|
188
|
+
ImagePlugin | ZipPlugin
|
|
189
|
+
>;
|
|
190
|
+
|
|
191
|
+
expectType<ZipPlugin>(
|
|
192
|
+
{} as PartiallyMissingPlugins extends { readonly __missing: infer M }
|
|
193
|
+
? M
|
|
194
|
+
: never,
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
expectType<ImagePlugin>(
|
|
198
|
+
{} as PartiallyMissingPlugins extends { readonly __provided: infer P }
|
|
199
|
+
? P
|
|
200
|
+
: never,
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
// Test: Should show all missing plugins
|
|
204
|
+
type AllMissingPlugins = ValidatePlugins<
|
|
205
|
+
[],
|
|
206
|
+
ImagePlugin | ZipPlugin | VideoPlugin
|
|
207
|
+
>;
|
|
208
|
+
|
|
209
|
+
expectType<ImagePlugin | ZipPlugin | VideoPlugin>(
|
|
210
|
+
{} as AllMissingPlugins extends { readonly __missing: infer M } ? M : never,
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
// ============================================================================
|
|
214
|
+
// ExtractFlowPluginRequirements Tests
|
|
215
|
+
// ============================================================================
|
|
216
|
+
|
|
217
|
+
// Test: Should extract requirements from flow function
|
|
218
|
+
type ImageFlowRequirements = ExtractFlowPluginRequirements<typeof imageFlowFn>;
|
|
219
|
+
expectType<ImagePlugin>({} as ImageFlowRequirements);
|
|
220
|
+
|
|
221
|
+
type MultiPluginFlowRequirements = ExtractFlowPluginRequirements<
|
|
222
|
+
typeof multiPluginFlowFn
|
|
223
|
+
>;
|
|
224
|
+
expectType<ImagePlugin | ZipPlugin>({} as MultiPluginFlowRequirements);
|
|
225
|
+
|
|
226
|
+
type NoPluginFlowRequirements = ExtractFlowPluginRequirements<
|
|
227
|
+
typeof noPluginFlowFn
|
|
228
|
+
>;
|
|
229
|
+
expectType<never>({} as NoPluginFlowRequirements);
|
|
230
|
+
|
|
231
|
+
// ============================================================================
|
|
232
|
+
// InferFlowRequirements Tests
|
|
233
|
+
// ============================================================================
|
|
234
|
+
|
|
235
|
+
// Test: Should infer requirements from flow function type
|
|
236
|
+
type InferredImageFlow = InferFlowRequirements<typeof imageFlowFn>;
|
|
237
|
+
expectType<ImagePlugin>({} as InferredImageFlow);
|
|
238
|
+
|
|
239
|
+
type InferredMultiFlow = InferFlowRequirements<typeof multiPluginFlowFn>;
|
|
240
|
+
expectType<ImagePlugin | ZipPlugin>({} as InferredMultiFlow);
|
|
241
|
+
|
|
242
|
+
type InferredNoPluginFlow = InferFlowRequirements<typeof noPluginFlowFn>;
|
|
243
|
+
expectType<never>({} as InferredNoPluginFlow);
|
|
244
|
+
|
|
245
|
+
// ============================================================================
|
|
246
|
+
// Integration Tests - Realistic Usage Scenarios
|
|
247
|
+
// ============================================================================
|
|
248
|
+
|
|
249
|
+
// Scenario 1: Valid server configuration
|
|
250
|
+
const validPlugins = [imageLayer, zipLayer] as const;
|
|
251
|
+
const validFlow: TypeSafeFlowFunction<ImagePlugin | ZipPlugin> = (
|
|
252
|
+
flowId,
|
|
253
|
+
clientId,
|
|
254
|
+
) =>
|
|
255
|
+
Effect.gen(function* () {
|
|
256
|
+
yield* ImagePlugin;
|
|
257
|
+
yield* ZipPlugin;
|
|
258
|
+
return {} as any;
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
type ValidConfig = ValidatePlugins<
|
|
262
|
+
typeof validPlugins,
|
|
263
|
+
ExtractFlowPluginRequirements<typeof validFlow>
|
|
264
|
+
>;
|
|
265
|
+
expectType<true>({} as ValidConfig);
|
|
266
|
+
|
|
267
|
+
// Scenario 2: Invalid server configuration - missing plugin
|
|
268
|
+
const incompletePlugins = [imageLayer] as const;
|
|
269
|
+
const multiPluginFlow: TypeSafeFlowFunction<ImagePlugin | ZipPlugin> = (
|
|
270
|
+
flowId,
|
|
271
|
+
clientId,
|
|
272
|
+
) =>
|
|
273
|
+
Effect.gen(function* () {
|
|
274
|
+
yield* ImagePlugin;
|
|
275
|
+
yield* ZipPlugin;
|
|
276
|
+
return {} as any;
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
type InvalidConfig = ValidatePlugins<
|
|
280
|
+
typeof incompletePlugins,
|
|
281
|
+
ExtractFlowPluginRequirements<typeof multiPluginFlow>
|
|
282
|
+
>;
|
|
283
|
+
|
|
284
|
+
// Should fail validation
|
|
285
|
+
type InvalidConfigIsTrue = InvalidConfig extends true ? true : false;
|
|
286
|
+
expectType<false>({} as InvalidConfigIsTrue);
|
|
287
|
+
|
|
288
|
+
// Should show ZipPlugin as missing
|
|
289
|
+
expectType<ZipPlugin>(
|
|
290
|
+
{} as InvalidConfig extends { readonly __missing: infer M } ? M : never,
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
// Scenario 3: Valid configuration with extra plugins
|
|
294
|
+
const extraPlugins = [imageLayer, zipLayer, videoLayer] as const;
|
|
295
|
+
const simpleFlow: TypeSafeFlowFunction<ImagePlugin> = (flowId, clientId) =>
|
|
296
|
+
Effect.gen(function* () {
|
|
297
|
+
yield* ImagePlugin;
|
|
298
|
+
return {} as any;
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
type ExtraPluginsConfig = ValidatePlugins<
|
|
302
|
+
typeof extraPlugins,
|
|
303
|
+
ExtractFlowPluginRequirements<typeof simpleFlow>
|
|
304
|
+
>;
|
|
305
|
+
expectType<true>({} as ExtraPluginsConfig);
|
|
306
|
+
|
|
307
|
+
// Scenario 4: No plugins, no requirements - valid
|
|
308
|
+
const noPlugins = [] as const;
|
|
309
|
+
const noRequirementsFlow: TypeSafeFlowFunction<never> = (flowId, clientId) =>
|
|
310
|
+
Effect.succeed({} as any);
|
|
311
|
+
|
|
312
|
+
type NoPluginsConfig = ValidatePlugins<
|
|
313
|
+
typeof noPlugins,
|
|
314
|
+
ExtractFlowPluginRequirements<typeof noRequirementsFlow>
|
|
315
|
+
>;
|
|
316
|
+
expectType<true>({} as NoPluginsConfig);
|
|
317
|
+
|
|
318
|
+
// ============================================================================
|
|
319
|
+
// Error Message Quality Tests
|
|
320
|
+
// ============================================================================
|
|
321
|
+
|
|
322
|
+
// Test: Error messages should be descriptive and actionable
|
|
323
|
+
type ErrorWithGoodMessages = ValidatePlugins<[], ImagePlugin | ZipPlugin>;
|
|
324
|
+
|
|
325
|
+
// Verify all required error properties exist
|
|
326
|
+
type HasError = ErrorWithGoodMessages extends {
|
|
327
|
+
readonly __error: string;
|
|
328
|
+
readonly __message: string;
|
|
329
|
+
readonly __required: unknown;
|
|
330
|
+
readonly __provided: unknown;
|
|
331
|
+
readonly __missing: unknown;
|
|
332
|
+
readonly __hint: string;
|
|
333
|
+
}
|
|
334
|
+
? true
|
|
335
|
+
: false;
|
|
336
|
+
expectType<true>({} as HasError);
|
|
337
|
+
|
|
338
|
+
// Verify error code is specific
|
|
339
|
+
type ErrorCode = ErrorWithGoodMessages extends {
|
|
340
|
+
readonly __error: "MISSING_REQUIRED_PLUGINS";
|
|
341
|
+
}
|
|
342
|
+
? true
|
|
343
|
+
: false;
|
|
344
|
+
expectType<true>({} as ErrorCode);
|
|
345
|
+
|
|
346
|
+
// Verify message is helpful
|
|
347
|
+
type HasHelpfulMessage = ErrorWithGoodMessages extends {
|
|
348
|
+
readonly __message: "Missing required plugins. Check __missing field for details.";
|
|
349
|
+
}
|
|
350
|
+
? true
|
|
351
|
+
: false;
|
|
352
|
+
expectType<true>({} as HasHelpfulMessage);
|
|
353
|
+
|
|
354
|
+
// Verify hint is actionable
|
|
355
|
+
type HasActionableHint = ErrorWithGoodMessages extends {
|
|
356
|
+
readonly __hint: "Add the missing plugins to your server configuration's plugins array.";
|
|
357
|
+
}
|
|
358
|
+
? true
|
|
359
|
+
: false;
|
|
360
|
+
expectType<true>({} as HasActionableHint);
|
|
361
|
+
|
|
362
|
+
// ============================================================================
|
|
363
|
+
// Edge Cases
|
|
364
|
+
// ============================================================================
|
|
365
|
+
|
|
366
|
+
// Test: Union of multiple requirements
|
|
367
|
+
type UnionRequirements = ValidatePlugins<
|
|
368
|
+
[typeof imageLayer],
|
|
369
|
+
ImagePlugin | ZipPlugin | VideoPlugin
|
|
370
|
+
>;
|
|
371
|
+
|
|
372
|
+
expectType<ZipPlugin | VideoPlugin>(
|
|
373
|
+
{} as UnionRequirements extends { readonly __missing: infer M } ? M : never,
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
// Test: Complex nested requirements
|
|
377
|
+
type ComplexFlow = TypeSafeFlowFunction<
|
|
378
|
+
ImagePlugin | (ZipPlugin & { optional?: boolean })
|
|
379
|
+
>;
|
|
380
|
+
type ComplexRequirements = ExtractFlowPluginRequirements<ComplexFlow>;
|
|
381
|
+
|
|
382
|
+
// Should extract the union properly
|
|
383
|
+
type IsValidComplexExtraction = ComplexRequirements extends
|
|
384
|
+
| ImagePlugin
|
|
385
|
+
| ZipPlugin
|
|
386
|
+
? true
|
|
387
|
+
: false;
|
|
388
|
+
expectType<true>({} as IsValidComplexExtraction);
|