@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,472 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration tests for plugin validation utilities.
|
|
3
|
+
*
|
|
4
|
+
* These tests verify runtime plugin validation behavior.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Layer } from "effect";
|
|
8
|
+
import { describe, expect, it } from "vitest";
|
|
9
|
+
import {
|
|
10
|
+
extractServiceIdentifiers,
|
|
11
|
+
formatPluginValidationError,
|
|
12
|
+
validatePluginRequirements,
|
|
13
|
+
validatePluginsOrThrow,
|
|
14
|
+
} from "../plugin-validation";
|
|
15
|
+
|
|
16
|
+
// ============================================================================
|
|
17
|
+
// Test Fixtures
|
|
18
|
+
// ============================================================================
|
|
19
|
+
|
|
20
|
+
class TestImagePlugin {
|
|
21
|
+
readonly _tag = "ImagePlugin";
|
|
22
|
+
resize(data: Buffer, width: number): Buffer {
|
|
23
|
+
return data;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
class TestZipPlugin {
|
|
28
|
+
readonly _tag = "ZipPlugin";
|
|
29
|
+
compress(files: Buffer[]): Buffer {
|
|
30
|
+
return Buffer.from([]);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
class TestVideoPlugin {
|
|
35
|
+
readonly _tag = "VideoPlugin";
|
|
36
|
+
transcode(data: Buffer): Buffer {
|
|
37
|
+
return data;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const imagePluginLayer = Layer.succeed(TestImagePlugin, new TestImagePlugin());
|
|
42
|
+
const zipPluginLayer = Layer.succeed(TestZipPlugin, new TestZipPlugin());
|
|
43
|
+
const videoPluginLayer = Layer.succeed(TestVideoPlugin, new TestVideoPlugin());
|
|
44
|
+
|
|
45
|
+
// ============================================================================
|
|
46
|
+
// extractServiceIdentifiers Tests
|
|
47
|
+
// ============================================================================
|
|
48
|
+
|
|
49
|
+
describe("extractServiceIdentifiers", () => {
|
|
50
|
+
it("should extract identifiers from plugin array", () => {
|
|
51
|
+
const plugins = [imagePluginLayer, zipPluginLayer];
|
|
52
|
+
const identifiers = extractServiceIdentifiers(plugins);
|
|
53
|
+
|
|
54
|
+
// Should return array of strings (exact values depend on Effect internals)
|
|
55
|
+
expect(Array.isArray(identifiers)).toBe(true);
|
|
56
|
+
expect(identifiers.length).toBeGreaterThanOrEqual(0);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("should handle empty plugin array", () => {
|
|
60
|
+
const plugins: never[] = [];
|
|
61
|
+
const identifiers = extractServiceIdentifiers(plugins);
|
|
62
|
+
|
|
63
|
+
expect(Array.isArray(identifiers)).toBe(true);
|
|
64
|
+
expect(identifiers).toHaveLength(0);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("should handle single plugin", () => {
|
|
68
|
+
const plugins = [imagePluginLayer];
|
|
69
|
+
const identifiers = extractServiceIdentifiers(plugins);
|
|
70
|
+
|
|
71
|
+
expect(Array.isArray(identifiers)).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// ============================================================================
|
|
76
|
+
// validatePluginRequirements Tests
|
|
77
|
+
// ============================================================================
|
|
78
|
+
|
|
79
|
+
describe("validatePluginRequirements", () => {
|
|
80
|
+
it("should return success when no services are expected", () => {
|
|
81
|
+
const result = validatePluginRequirements({
|
|
82
|
+
plugins: [imagePluginLayer],
|
|
83
|
+
expectedServices: [],
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
expect(result.success).toBe(true);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("should return success when expected services is undefined", () => {
|
|
90
|
+
const result = validatePluginRequirements({
|
|
91
|
+
plugins: [imagePluginLayer],
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
expect(result.success).toBe(true);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("should detect missing services", () => {
|
|
98
|
+
const result = validatePluginRequirements({
|
|
99
|
+
plugins: [],
|
|
100
|
+
expectedServices: ["ImagePlugin", "ZipPlugin"],
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
expect(result.success).toBe(false);
|
|
104
|
+
if (!result.success) {
|
|
105
|
+
expect(result.missing).toEqual(["ImagePlugin", "ZipPlugin"]);
|
|
106
|
+
expect(result.required).toEqual(["ImagePlugin", "ZipPlugin"]);
|
|
107
|
+
expect(result.provided).toEqual([]);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("should detect partially missing services", () => {
|
|
112
|
+
const result = validatePluginRequirements({
|
|
113
|
+
plugins: [imagePluginLayer],
|
|
114
|
+
expectedServices: ["ImagePlugin", "ZipPlugin"],
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// Since we can't reliably extract service identifiers from Effect layers,
|
|
118
|
+
// this test verifies the validation logic structure
|
|
119
|
+
expect(result).toHaveProperty("success");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("should generate suggestions for known plugins", () => {
|
|
123
|
+
const result = validatePluginRequirements({
|
|
124
|
+
plugins: [],
|
|
125
|
+
expectedServices: ["ImagePlugin"],
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
expect(result.success).toBe(false);
|
|
129
|
+
if (!result.success) {
|
|
130
|
+
expect(result.suggestions.length).toBeGreaterThan(0);
|
|
131
|
+
const suggestion = result.suggestions[0];
|
|
132
|
+
expect(suggestion).toHaveProperty("name");
|
|
133
|
+
expect(suggestion).toHaveProperty("packageName");
|
|
134
|
+
expect(suggestion).toHaveProperty("importStatement");
|
|
135
|
+
expect(suggestion.name).toBe("ImagePlugin");
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("should handle unknown plugin services", () => {
|
|
140
|
+
const result = validatePluginRequirements({
|
|
141
|
+
plugins: [],
|
|
142
|
+
expectedServices: ["UnknownPlugin"],
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
expect(result.success).toBe(false);
|
|
146
|
+
if (!result.success) {
|
|
147
|
+
expect(result.missing).toContain("UnknownPlugin");
|
|
148
|
+
// Should not generate suggestion for unknown plugins
|
|
149
|
+
const unknownSuggestion = result.suggestions.find(
|
|
150
|
+
(s) => s.name === "UnknownPlugin",
|
|
151
|
+
);
|
|
152
|
+
expect(unknownSuggestion).toBeUndefined();
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("should handle multiple known plugins", () => {
|
|
157
|
+
const result = validatePluginRequirements({
|
|
158
|
+
plugins: [],
|
|
159
|
+
expectedServices: ["ImagePlugin", "ZipPlugin"],
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
expect(result.success).toBe(false);
|
|
163
|
+
if (!result.success) {
|
|
164
|
+
expect(result.suggestions.length).toBe(2);
|
|
165
|
+
expect(result.suggestions.map((s) => s.name)).toEqual([
|
|
166
|
+
"ImagePlugin",
|
|
167
|
+
"ZipPlugin",
|
|
168
|
+
]);
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// ============================================================================
|
|
174
|
+
// formatPluginValidationError Tests
|
|
175
|
+
// ============================================================================
|
|
176
|
+
|
|
177
|
+
describe("formatPluginValidationError", () => {
|
|
178
|
+
it("should format error message with suggestions", () => {
|
|
179
|
+
const result = validatePluginRequirements({
|
|
180
|
+
plugins: [],
|
|
181
|
+
expectedServices: ["ImagePlugin", "ZipPlugin"],
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
expect(result.success).toBe(false);
|
|
185
|
+
if (!result.success) {
|
|
186
|
+
const message = formatPluginValidationError(result);
|
|
187
|
+
|
|
188
|
+
expect(message).toContain("Server initialization failed");
|
|
189
|
+
expect(message).toContain("Missing required plugins");
|
|
190
|
+
expect(message).toContain("ImagePlugin");
|
|
191
|
+
expect(message).toContain("ZipPlugin");
|
|
192
|
+
expect(message).toContain("Required:");
|
|
193
|
+
expect(message).toContain("Missing:");
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("should include import statements in error message", () => {
|
|
198
|
+
const result = validatePluginRequirements({
|
|
199
|
+
plugins: [],
|
|
200
|
+
expectedServices: ["ImagePlugin"],
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
expect(result.success).toBe(false);
|
|
204
|
+
if (!result.success) {
|
|
205
|
+
const message = formatPluginValidationError(result);
|
|
206
|
+
|
|
207
|
+
expect(message).toContain("import");
|
|
208
|
+
expect(message).toContain("@uploadista/flow-images-sharp");
|
|
209
|
+
expect(message).toContain("sharpImagePlugin");
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("should include example server configuration", () => {
|
|
214
|
+
const result = validatePluginRequirements({
|
|
215
|
+
plugins: [],
|
|
216
|
+
expectedServices: ["ImagePlugin"],
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
expect(result.success).toBe(false);
|
|
220
|
+
if (!result.success) {
|
|
221
|
+
const message = formatPluginValidationError(result);
|
|
222
|
+
|
|
223
|
+
expect(message).toContain("createUploadistaServer");
|
|
224
|
+
expect(message).toContain("plugins:");
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("should handle missing suggestions gracefully", () => {
|
|
229
|
+
const result = validatePluginRequirements({
|
|
230
|
+
plugins: [],
|
|
231
|
+
expectedServices: ["UnknownPlugin"],
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
expect(result.success).toBe(false);
|
|
235
|
+
if (!result.success) {
|
|
236
|
+
const message = formatPluginValidationError(result);
|
|
237
|
+
|
|
238
|
+
expect(message).toContain("Server initialization failed");
|
|
239
|
+
expect(message).toContain("UnknownPlugin");
|
|
240
|
+
expect(message).toContain(
|
|
241
|
+
"Could not determine package names for missing plugins",
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it("should show provided services when some are present", () => {
|
|
247
|
+
const result = validatePluginRequirements({
|
|
248
|
+
plugins: [imagePluginLayer],
|
|
249
|
+
expectedServices: ["ImagePlugin", "ZipPlugin"],
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
expect(result.success).toBe(false);
|
|
253
|
+
if (!result.success) {
|
|
254
|
+
const message = formatPluginValidationError(result);
|
|
255
|
+
|
|
256
|
+
expect(message).toContain("Provided:");
|
|
257
|
+
// The exact provided services depend on Effect's internal representation
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
// ============================================================================
|
|
263
|
+
// validatePluginsOrThrow Tests
|
|
264
|
+
// ============================================================================
|
|
265
|
+
|
|
266
|
+
describe("validatePluginsOrThrow", () => {
|
|
267
|
+
it("should not throw when validation passes", () => {
|
|
268
|
+
expect(() => {
|
|
269
|
+
validatePluginsOrThrow({
|
|
270
|
+
plugins: [imagePluginLayer],
|
|
271
|
+
expectedServices: [],
|
|
272
|
+
});
|
|
273
|
+
}).not.toThrow();
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it("should throw when validation fails", () => {
|
|
277
|
+
expect(() => {
|
|
278
|
+
validatePluginsOrThrow({
|
|
279
|
+
plugins: [],
|
|
280
|
+
expectedServices: ["ImagePlugin"],
|
|
281
|
+
});
|
|
282
|
+
}).toThrow("Server initialization failed");
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it("should throw error with detailed message", () => {
|
|
286
|
+
expect(() => {
|
|
287
|
+
validatePluginsOrThrow({
|
|
288
|
+
plugins: [],
|
|
289
|
+
expectedServices: ["ImagePlugin", "ZipPlugin"],
|
|
290
|
+
});
|
|
291
|
+
}).toThrow(/Missing required plugins/);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it("should include plugin names in error", () => {
|
|
295
|
+
try {
|
|
296
|
+
validatePluginsOrThrow({
|
|
297
|
+
plugins: [],
|
|
298
|
+
expectedServices: ["ImagePlugin", "ZipPlugin"],
|
|
299
|
+
});
|
|
300
|
+
expect.fail("Should have thrown");
|
|
301
|
+
} catch (error) {
|
|
302
|
+
expect(error).toBeInstanceOf(Error);
|
|
303
|
+
if (error instanceof Error) {
|
|
304
|
+
expect(error.message).toContain("ImagePlugin");
|
|
305
|
+
expect(error.message).toContain("ZipPlugin");
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
// ============================================================================
|
|
312
|
+
// Integration Tests
|
|
313
|
+
// ============================================================================
|
|
314
|
+
|
|
315
|
+
describe("Plugin Validation Integration", () => {
|
|
316
|
+
it("should handle realistic server configuration scenario", () => {
|
|
317
|
+
// Scenario: Server configured with image plugin, but flow needs both image and zip
|
|
318
|
+
const result = validatePluginRequirements({
|
|
319
|
+
plugins: [imagePluginLayer],
|
|
320
|
+
expectedServices: ["ImagePlugin", "ZipPlugin"],
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
if (!result.success) {
|
|
324
|
+
const message = formatPluginValidationError(result);
|
|
325
|
+
|
|
326
|
+
// Should provide actionable error message
|
|
327
|
+
expect(message).toContain("Missing required plugins");
|
|
328
|
+
expect(message).toContain("@uploadista/flow-utility-zipjs");
|
|
329
|
+
expect(message).toContain("zipPlugin");
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it("should handle empty configuration", () => {
|
|
334
|
+
const result = validatePluginRequirements({
|
|
335
|
+
plugins: [],
|
|
336
|
+
expectedServices: [],
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
expect(result.success).toBe(true);
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it("should handle over-provisioned plugins", () => {
|
|
343
|
+
// More plugins than required should still pass
|
|
344
|
+
const result = validatePluginRequirements({
|
|
345
|
+
plugins: [imagePluginLayer, zipPluginLayer, videoPluginLayer],
|
|
346
|
+
expectedServices: ["ImagePlugin"],
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
// This would pass if we could extract identifiers reliably
|
|
350
|
+
// For now, just verify structure
|
|
351
|
+
expect(result).toHaveProperty("success");
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it("should provide helpful error for complete plugin set missing", () => {
|
|
355
|
+
const result = validatePluginRequirements({
|
|
356
|
+
plugins: [],
|
|
357
|
+
expectedServices: ["ImagePlugin", "ZipPlugin", "VideoPlugin"],
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
expect(result.success).toBe(false);
|
|
361
|
+
if (!result.success) {
|
|
362
|
+
expect(result.missing).toHaveLength(3);
|
|
363
|
+
const message = formatPluginValidationError(result);
|
|
364
|
+
|
|
365
|
+
// Should list all missing plugins
|
|
366
|
+
expect(message).toContain("ImagePlugin");
|
|
367
|
+
expect(message).toContain("ZipPlugin");
|
|
368
|
+
expect(message).toContain("VideoPlugin");
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
// ============================================================================
|
|
374
|
+
// Known Plugins Mapping Tests
|
|
375
|
+
// ============================================================================
|
|
376
|
+
|
|
377
|
+
describe("Known Plugins Mapping", () => {
|
|
378
|
+
it("should generate suggestions for missing plugins", () => {
|
|
379
|
+
const result = validatePluginRequirements({
|
|
380
|
+
plugins: [],
|
|
381
|
+
expectedServices: ["ImagePlugin"],
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
expect(result.success).toBe(false);
|
|
385
|
+
if (!result.success) {
|
|
386
|
+
// Verify suggestions array exists
|
|
387
|
+
expect(result.suggestions).toBeDefined();
|
|
388
|
+
expect(Array.isArray(result.suggestions)).toBe(true);
|
|
389
|
+
|
|
390
|
+
// If there are suggestions, check the structure
|
|
391
|
+
if (result.suggestions.length > 0) {
|
|
392
|
+
const suggestion = result.suggestions[0];
|
|
393
|
+
expect(suggestion).toHaveProperty("name");
|
|
394
|
+
expect(suggestion).toHaveProperty("packageName");
|
|
395
|
+
expect(suggestion).toHaveProperty("importStatement");
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
it("should provide correct package for ImagePlugin", () => {
|
|
401
|
+
const result = validatePluginRequirements({
|
|
402
|
+
plugins: [],
|
|
403
|
+
expectedServices: ["ImagePlugin"],
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
expect(result.success).toBe(false);
|
|
407
|
+
if (!result.success && result.suggestions.length > 0) {
|
|
408
|
+
const suggestion = result.suggestions[0];
|
|
409
|
+
expect(suggestion.name).toBe("ImagePlugin");
|
|
410
|
+
expect(suggestion.packageName).toBe("@uploadista/flow-images-sharp");
|
|
411
|
+
expect(suggestion.importStatement).toContain("sharpImagePlugin");
|
|
412
|
+
expect(suggestion.importStatement).toContain("@uploadista/flow-images-sharp");
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
it("should provide correct package for ZipPlugin", () => {
|
|
417
|
+
const result = validatePluginRequirements({
|
|
418
|
+
plugins: [],
|
|
419
|
+
expectedServices: ["ZipPlugin"],
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
expect(result.success).toBe(false);
|
|
423
|
+
if (!result.success) {
|
|
424
|
+
expect(result.suggestions.length).toBeGreaterThan(0);
|
|
425
|
+
|
|
426
|
+
const suggestion = result.suggestions[0];
|
|
427
|
+
expect(suggestion).toBeDefined();
|
|
428
|
+
expect(suggestion.name).toBe("ZipPlugin");
|
|
429
|
+
expect(suggestion.packageName).toBe("@uploadista/flow-utility-zipjs");
|
|
430
|
+
expect(suggestion.importStatement).toContain("zipPlugin");
|
|
431
|
+
expect(suggestion.importStatement).toContain("@uploadista/flow-utility-zipjs");
|
|
432
|
+
}
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
it("should provide correct package for ImageAiPlugin", () => {
|
|
436
|
+
const result = validatePluginRequirements({
|
|
437
|
+
plugins: [],
|
|
438
|
+
expectedServices: ["ImageAiPlugin"],
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
expect(result.success).toBe(false);
|
|
442
|
+
if (!result.success) {
|
|
443
|
+
expect(result.suggestions.length).toBeGreaterThan(0);
|
|
444
|
+
|
|
445
|
+
const suggestion = result.suggestions[0];
|
|
446
|
+
expect(suggestion).toBeDefined();
|
|
447
|
+
expect(suggestion.name).toBe("ImageAiPlugin");
|
|
448
|
+
expect(suggestion.packageName).toBe("@uploadista/flow-images-replicate");
|
|
449
|
+
expect(suggestion.importStatement).toContain("replicateImagePlugin");
|
|
450
|
+
expect(suggestion.importStatement).toContain("@uploadista/flow-images-replicate");
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
it("should provide correct package for CredentialProvider", () => {
|
|
455
|
+
const result = validatePluginRequirements({
|
|
456
|
+
plugins: [],
|
|
457
|
+
expectedServices: ["CredentialProvider"],
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
expect(result.success).toBe(false);
|
|
461
|
+
if (!result.success) {
|
|
462
|
+
expect(result.suggestions.length).toBeGreaterThan(0);
|
|
463
|
+
|
|
464
|
+
const suggestion = result.suggestions[0];
|
|
465
|
+
expect(suggestion).toBeDefined();
|
|
466
|
+
expect(suggestion.name).toBe("CredentialProvider");
|
|
467
|
+
expect(suggestion.packageName).toBe("@uploadista/core");
|
|
468
|
+
expect(suggestion.importStatement).toContain("credentialProviderLayer");
|
|
469
|
+
expect(suggestion.importStatement).toContain("@uploadista/core");
|
|
470
|
+
}
|
|
471
|
+
});
|
|
472
|
+
});
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
PluginServices,
|
|
3
|
+
PluginTuple,
|
|
4
|
+
TypeSafeFlowFunction,
|
|
5
|
+
ValidatePlugins,
|
|
6
|
+
} from "./plugin-types";
|
|
7
|
+
import { createUploadistaServer } from "./server";
|
|
8
|
+
import type { UploadistaServer, UploadistaServerConfig } from "./types";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Type-safe configuration for Uploadista server with compile-time plugin validation.
|
|
12
|
+
*
|
|
13
|
+
* This configuration extends the base UploadistaServerConfig with stricter typing
|
|
14
|
+
* that validates plugins match flow requirements at compile time.
|
|
15
|
+
*
|
|
16
|
+
* @template TContext - Framework-specific request context type
|
|
17
|
+
* @template TResponse - Framework-specific response type
|
|
18
|
+
* @template TWebSocket - Framework-specific WebSocket handler type
|
|
19
|
+
* @template TPlugins - Tuple of plugin layers provided to the server
|
|
20
|
+
* @template TFlowRequirements - Union of plugin services required by flows
|
|
21
|
+
*/
|
|
22
|
+
export type TypeSafeServerConfig<
|
|
23
|
+
TContext,
|
|
24
|
+
TResponse,
|
|
25
|
+
TWebSocket,
|
|
26
|
+
TPlugins extends PluginTuple,
|
|
27
|
+
TFlowRequirements = PluginServices<TPlugins>,
|
|
28
|
+
> = Omit<
|
|
29
|
+
UploadistaServerConfig<TContext, TResponse, TWebSocket>,
|
|
30
|
+
"flows" | "plugins"
|
|
31
|
+
> & {
|
|
32
|
+
/**
|
|
33
|
+
* Tuple of plugin layers that provide services to flows.
|
|
34
|
+
* The plugins must satisfy all requirements declared by the flows.
|
|
35
|
+
*/
|
|
36
|
+
plugins: TPlugins;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Type-safe flow function with explicit requirements.
|
|
40
|
+
* TypeScript validates that all required plugins are provided.
|
|
41
|
+
*/
|
|
42
|
+
flows: TypeSafeFlowFunction<TFlowRequirements>;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Compile-time validation that plugins satisfy flow requirements.
|
|
46
|
+
* If this field has type errors, required plugins are missing.
|
|
47
|
+
*/
|
|
48
|
+
__validate?: ValidatePlugins<TPlugins, TFlowRequirements>;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @deprecated Use `createUploadistaServer` with optional type utilities instead.
|
|
53
|
+
*
|
|
54
|
+
* This function is deprecated in favor of the unified `createUploadistaServer` API.
|
|
55
|
+
* The new approach separates validation concerns from server creation, making the
|
|
56
|
+
* API simpler while still providing compile-time validation when desired.
|
|
57
|
+
*
|
|
58
|
+
* ## Migration Guide
|
|
59
|
+
*
|
|
60
|
+
* ### Old Approach (Deprecated)
|
|
61
|
+
* ```typescript
|
|
62
|
+
* import { createTypeSafeServer } from "@uploadista/server";
|
|
63
|
+
*
|
|
64
|
+
* const server = await createTypeSafeServer({
|
|
65
|
+
* plugins: [sharpImagePlugin] as const,
|
|
66
|
+
* flows: myFlowFunction,
|
|
67
|
+
* // ...
|
|
68
|
+
* });
|
|
69
|
+
* ```
|
|
70
|
+
*
|
|
71
|
+
* ### New Approach (Recommended)
|
|
72
|
+
*
|
|
73
|
+
* **Option 1: Runtime validation only (simplest)**
|
|
74
|
+
* ```typescript
|
|
75
|
+
* import { createUploadistaServer } from "@uploadista/server";
|
|
76
|
+
*
|
|
77
|
+
* const server = await createUploadistaServer({
|
|
78
|
+
* plugins: [sharpImagePlugin, zipPlugin],
|
|
79
|
+
* flows: myFlowFunction,
|
|
80
|
+
* // ... Effect validates at runtime
|
|
81
|
+
* });
|
|
82
|
+
* ```
|
|
83
|
+
*
|
|
84
|
+
* **Option 2: With compile-time validation (optional)**
|
|
85
|
+
* ```typescript
|
|
86
|
+
* import {
|
|
87
|
+
* createUploadistaServer,
|
|
88
|
+
* ValidatePlugins,
|
|
89
|
+
* ExtractFlowPluginRequirements
|
|
90
|
+
* } from "@uploadista/server";
|
|
91
|
+
*
|
|
92
|
+
* type Requirements = ExtractFlowPluginRequirements<typeof myFlowFunction>;
|
|
93
|
+
* const plugins = [sharpImagePlugin, zipPlugin] as const;
|
|
94
|
+
* type Validation = ValidatePlugins<typeof plugins, Requirements>;
|
|
95
|
+
* // IDE shows error if plugins don't match requirements
|
|
96
|
+
*
|
|
97
|
+
* const server = await createUploadistaServer({
|
|
98
|
+
* plugins,
|
|
99
|
+
* flows: myFlowFunction,
|
|
100
|
+
* // ...
|
|
101
|
+
* });
|
|
102
|
+
* ```
|
|
103
|
+
*
|
|
104
|
+
* ## Why This Changed
|
|
105
|
+
*
|
|
106
|
+
* 1. **Simpler API**: One function instead of two reduces confusion
|
|
107
|
+
* 2. **Separation of Concerns**: Validation is now optional and separate
|
|
108
|
+
* 3. **Better Flexibility**: Choose validation approach per use case
|
|
109
|
+
* 4. **Clearer Intent**: Explicit validation via type utilities
|
|
110
|
+
* 5. **Same Safety**: Effect-TS still validates at runtime
|
|
111
|
+
*
|
|
112
|
+
* The new approach trusts Effect-TS's design for dynamic dependency injection
|
|
113
|
+
* while providing optional compile-time validation through type utilities.
|
|
114
|
+
*
|
|
115
|
+
* @see createUploadistaServer - The unified server creation API
|
|
116
|
+
* @see ValidatePlugins - Compile-time validation type utility
|
|
117
|
+
* @see ExtractFlowPluginRequirements - Extract requirements from flows
|
|
118
|
+
* @see API_DECISION_GUIDE.md - Complete migration and usage guide
|
|
119
|
+
*
|
|
120
|
+
* @template TContext - Framework-specific request context type
|
|
121
|
+
* @template TResponse - Framework-specific response type
|
|
122
|
+
* @template TWebSocket - Framework-specific WebSocket handler type
|
|
123
|
+
* @template TPlugins - Tuple of plugin layers
|
|
124
|
+
* @template TFlowRequirements - Union of services required by flows
|
|
125
|
+
*
|
|
126
|
+
* @param config - Type-safe server configuration
|
|
127
|
+
* @returns Promise resolving to UploadistaServer instance
|
|
128
|
+
*/
|
|
129
|
+
export async function createTypeSafeServer<
|
|
130
|
+
TContext,
|
|
131
|
+
TResponse,
|
|
132
|
+
TWebSocket = unknown,
|
|
133
|
+
TPlugins extends PluginTuple = PluginTuple,
|
|
134
|
+
TFlowRequirements = PluginServices<TPlugins>,
|
|
135
|
+
>(
|
|
136
|
+
config: TypeSafeServerConfig<
|
|
137
|
+
TContext,
|
|
138
|
+
TResponse,
|
|
139
|
+
TWebSocket,
|
|
140
|
+
TPlugins,
|
|
141
|
+
TFlowRequirements
|
|
142
|
+
> &
|
|
143
|
+
// Enforce validation at function call site
|
|
144
|
+
(ValidatePlugins<TPlugins, TFlowRequirements> extends true
|
|
145
|
+
? object
|
|
146
|
+
: ValidatePlugins<TPlugins, TFlowRequirements>),
|
|
147
|
+
): Promise<UploadistaServer<TContext, TResponse, TWebSocket>> {
|
|
148
|
+
return createUploadistaServer(config);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Helper function to define flow functions with explicit type requirements.
|
|
153
|
+
* Provides better type inference and autocomplete for plugin services.
|
|
154
|
+
*
|
|
155
|
+
* @template TRequirements - Union of plugin services this flow needs
|
|
156
|
+
*
|
|
157
|
+
* @param fn - The flow function implementation
|
|
158
|
+
* @returns The same function with explicit type annotation
|
|
159
|
+
*
|
|
160
|
+
* @example
|
|
161
|
+
* ```typescript
|
|
162
|
+
* import { ImagePlugin } from "@uploadista/core/flow";
|
|
163
|
+
* import { defineFlow } from "@uploadista/server";
|
|
164
|
+
*
|
|
165
|
+
* // Explicitly declare that this flow requires ImagePlugin
|
|
166
|
+
* const imageProcessingFlow = defineFlow<ImagePlugin>((flowId, clientId) =>
|
|
167
|
+
* Effect.gen(function* () {
|
|
168
|
+
* const imageService = yield* ImagePlugin; // Autocomplete works!
|
|
169
|
+
* const optimized = yield* imageService.optimize(data, { quality: 80 });
|
|
170
|
+
* return createFlow({ ... });
|
|
171
|
+
* })
|
|
172
|
+
* );
|
|
173
|
+
* ```
|
|
174
|
+
*/
|
|
175
|
+
export function defineFlow<TRequirements = never>(
|
|
176
|
+
fn: TypeSafeFlowFunction<TRequirements>,
|
|
177
|
+
): TypeSafeFlowFunction<TRequirements> {
|
|
178
|
+
return fn;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Helper to create a flow that requires no plugins.
|
|
183
|
+
* Useful for simple flows that only use built-in functionality.
|
|
184
|
+
*
|
|
185
|
+
* @example
|
|
186
|
+
* ```typescript
|
|
187
|
+
* import { defineSimpleFlow } from "@uploadista/server";
|
|
188
|
+
*
|
|
189
|
+
* const simpleFlow = defineSimpleFlow((flowId, clientId) =>
|
|
190
|
+
* Effect.succeed(createFlow({
|
|
191
|
+
* id: "simple",
|
|
192
|
+
* nodes: [],
|
|
193
|
+
* edges: [],
|
|
194
|
+
* inputSchema: myInputSchema,
|
|
195
|
+
* outputSchema: myOutputSchema
|
|
196
|
+
* }))
|
|
197
|
+
* );
|
|
198
|
+
* ```
|
|
199
|
+
*/
|
|
200
|
+
export function defineSimpleFlow(
|
|
201
|
+
fn: TypeSafeFlowFunction<never>,
|
|
202
|
+
): TypeSafeFlowFunction<never> {
|
|
203
|
+
return fn;
|
|
204
|
+
}
|
package/src/core/index.ts
CHANGED
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
* all framework adapters (Hono, Express, Fastify, etc.).
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
export * from "./create-type-safe-server";
|
|
9
|
+
export * from "./plugin-types";
|
|
10
|
+
export * from "./plugin-validation";
|
|
8
11
|
export * from "./routes";
|
|
9
12
|
export * from "./server";
|
|
10
13
|
export * from "./types";
|