@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,217 @@
|
|
|
1
|
+
import type { UploadistaError } from "@uploadista/core";
|
|
2
|
+
import type {
|
|
3
|
+
CredentialProviderLayer,
|
|
4
|
+
ExtractLayerServices,
|
|
5
|
+
Flow,
|
|
6
|
+
ImageAiPluginLayer,
|
|
7
|
+
ImagePluginLayer,
|
|
8
|
+
ZipPluginLayer,
|
|
9
|
+
} from "@uploadista/core/flow";
|
|
10
|
+
import type { Effect, Layer } from "effect";
|
|
11
|
+
import type { z } from "zod";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Utility type to extract all services from a tuple of layers.
|
|
15
|
+
* Given [Layer<A>, Layer<B>], extracts A | B.
|
|
16
|
+
*
|
|
17
|
+
* This is a wrapper around the shared ExtractLayerServices utility from @uploadista/core.
|
|
18
|
+
*
|
|
19
|
+
* @deprecated Use ExtractLayerServices from @uploadista/core/flow/types instead.
|
|
20
|
+
* This will be removed in a future version.
|
|
21
|
+
*/
|
|
22
|
+
// biome-ignore lint/suspicious/noExplicitAny: Utility type for extracting services from any layer tuple
|
|
23
|
+
export type ExtractServicesFromLayers<
|
|
24
|
+
// biome-ignore lint/suspicious/noExplicitAny: Generic constraint must accept any layer configuration
|
|
25
|
+
T extends readonly Layer.Layer<any, any, any>[],
|
|
26
|
+
> = ExtractLayerServices<T>;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Known plugin layer types for better type inference.
|
|
30
|
+
* This union helps TypeScript understand which plugins are available.
|
|
31
|
+
*/
|
|
32
|
+
export type KnownPluginLayer =
|
|
33
|
+
| ImagePluginLayer
|
|
34
|
+
| ImageAiPluginLayer
|
|
35
|
+
| CredentialProviderLayer
|
|
36
|
+
| ZipPluginLayer;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Type-safe plugin tuple that only accepts known plugin layers.
|
|
40
|
+
* This provides autocomplete and validation for plugin arrays.
|
|
41
|
+
*/
|
|
42
|
+
export type PluginTuple = readonly KnownPluginLayer[];
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Extracts the union of all plugin services from a plugin tuple.
|
|
46
|
+
*
|
|
47
|
+
* Uses the shared ExtractLayerServices utility from @uploadista/core for consistency.
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* ```typescript
|
|
51
|
+
* type Plugins = [ImagePluginLayer, ZipPluginLayer];
|
|
52
|
+
* type Services = PluginServices<Plugins>;
|
|
53
|
+
* // Services = ImagePlugin | ZipPlugin
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
export type PluginServices<TPlugins extends PluginTuple> =
|
|
57
|
+
ExtractLayerServices<TPlugins>;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Type-safe flow function that declares its plugin requirements.
|
|
61
|
+
*
|
|
62
|
+
* @template TRequirements - Union of plugin services this flow needs
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* ```typescript
|
|
66
|
+
* // Flow that requires ImagePlugin
|
|
67
|
+
* const myFlow: TypeSafeFlowFunction<ImagePlugin> = (flowId, clientId) =>
|
|
68
|
+
* Effect.gen(function* () {
|
|
69
|
+
* const imageService = yield* ImagePlugin;
|
|
70
|
+
* // ...
|
|
71
|
+
* });
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
export type TypeSafeFlowFunction<TRequirements = never> = (
|
|
75
|
+
flowId: string,
|
|
76
|
+
clientId: string | null,
|
|
77
|
+
) => Effect.Effect<
|
|
78
|
+
Flow<z.ZodSchema<unknown>, z.ZodSchema<unknown>, TRequirements>,
|
|
79
|
+
UploadistaError,
|
|
80
|
+
TRequirements
|
|
81
|
+
>;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Validates that plugins satisfy flow requirements.
|
|
85
|
+
*
|
|
86
|
+
* This type creates a compile-time error if required plugins are missing.
|
|
87
|
+
* When validation fails, it returns an error object with detailed information
|
|
88
|
+
* including a human-readable message.
|
|
89
|
+
*
|
|
90
|
+
* @template TPlugins - The plugin tuple provided
|
|
91
|
+
* @template TRequirements - The services required by flows
|
|
92
|
+
*
|
|
93
|
+
* @example
|
|
94
|
+
* ```typescript
|
|
95
|
+
* // ✅ Valid: ImagePlugin is provided and required
|
|
96
|
+
* type Valid = ValidatePlugins<[ImagePluginLayer], ImagePlugin>;
|
|
97
|
+
* // Result: true
|
|
98
|
+
*
|
|
99
|
+
* // ❌ Error: ImagePlugin required but not provided
|
|
100
|
+
* type Invalid = ValidatePlugins<[], ImagePlugin>;
|
|
101
|
+
* // Result: {
|
|
102
|
+
* // __error: "MISSING_REQUIRED_PLUGINS";
|
|
103
|
+
* // __message: "Missing required plugins: ...";
|
|
104
|
+
* // __required: ImagePlugin;
|
|
105
|
+
* // __provided: never;
|
|
106
|
+
* // __missing: ImagePlugin;
|
|
107
|
+
* // }
|
|
108
|
+
* ```
|
|
109
|
+
*/
|
|
110
|
+
export type ValidatePlugins<
|
|
111
|
+
TPlugins extends PluginTuple,
|
|
112
|
+
TRequirements,
|
|
113
|
+
> = TRequirements extends never
|
|
114
|
+
? true // No requirements, always valid
|
|
115
|
+
: TRequirements extends PluginServices<TPlugins>
|
|
116
|
+
? true // All requirements satisfied
|
|
117
|
+
: {
|
|
118
|
+
readonly __error: "MISSING_REQUIRED_PLUGINS";
|
|
119
|
+
readonly __message: "Missing required plugins. Check __missing field for details.";
|
|
120
|
+
readonly __required: TRequirements;
|
|
121
|
+
readonly __provided: PluginServices<TPlugins>;
|
|
122
|
+
readonly __missing: Exclude<TRequirements, PluginServices<TPlugins>>;
|
|
123
|
+
readonly __hint: "Add the missing plugins to your server configuration's plugins array.";
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Type-safe server configuration with compile-time plugin validation.
|
|
128
|
+
*
|
|
129
|
+
* This ensures that all plugins required by flows are actually provided.
|
|
130
|
+
*
|
|
131
|
+
* @template TPlugins - Tuple of plugin layers
|
|
132
|
+
* @template TFlowRequirements - Union of services that flows need
|
|
133
|
+
*
|
|
134
|
+
* @example
|
|
135
|
+
* ```typescript
|
|
136
|
+
* // ✅ Compiles: ImagePlugin provided and required
|
|
137
|
+
* const config: TypeSafePluginConfig<
|
|
138
|
+
* [ImagePluginLayer],
|
|
139
|
+
* ImagePlugin
|
|
140
|
+
* > = {
|
|
141
|
+
* plugins: [sharpImagePlugin],
|
|
142
|
+
* flows: (flowId, clientId) => imageFlow
|
|
143
|
+
* };
|
|
144
|
+
*
|
|
145
|
+
* // ❌ Compile error: ImagePlugin required but not provided
|
|
146
|
+
* const bad: TypeSafePluginConfig<
|
|
147
|
+
* [],
|
|
148
|
+
* ImagePlugin
|
|
149
|
+
* > = {
|
|
150
|
+
* plugins: [],
|
|
151
|
+
* flows: (flowId, clientId) => imageFlow
|
|
152
|
+
* };
|
|
153
|
+
* ```
|
|
154
|
+
*/
|
|
155
|
+
export type TypeSafePluginConfig<
|
|
156
|
+
TPlugins extends PluginTuple,
|
|
157
|
+
TFlowRequirements,
|
|
158
|
+
> = ValidatePlugins<TPlugins, TFlowRequirements> extends true
|
|
159
|
+
? {
|
|
160
|
+
plugins: TPlugins;
|
|
161
|
+
flows: TypeSafeFlowFunction<TFlowRequirements>;
|
|
162
|
+
}
|
|
163
|
+
: ValidatePlugins<TPlugins, TFlowRequirements>; // Returns error object
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Extracts plugin requirements from a flow function type.
|
|
167
|
+
*
|
|
168
|
+
* This navigates through the flow function signature to extract the requirements
|
|
169
|
+
* from the Flow type it returns, excluding UploadServer (provided by runtime).
|
|
170
|
+
*
|
|
171
|
+
* @template TFlowFn - The flow function type to extract requirements from
|
|
172
|
+
*
|
|
173
|
+
* @example
|
|
174
|
+
* ```typescript
|
|
175
|
+
* const myFlow = (flowId: string, clientId: string | null) =>
|
|
176
|
+
* Effect.succeed(
|
|
177
|
+
* createFlow({ ... }) // Returns Flow<..., ..., ImagePlugin | ZipPlugin>
|
|
178
|
+
* );
|
|
179
|
+
*
|
|
180
|
+
* type Requirements = ExtractFlowPluginRequirements<typeof myFlow>;
|
|
181
|
+
* // Requirements = ImagePlugin | ZipPlugin
|
|
182
|
+
* ```
|
|
183
|
+
*/
|
|
184
|
+
export type ExtractFlowPluginRequirements<
|
|
185
|
+
TFlowFn extends (
|
|
186
|
+
flowId: string,
|
|
187
|
+
clientId: string | null,
|
|
188
|
+
) => Effect.Effect<unknown, unknown, unknown>,
|
|
189
|
+
> = ReturnType<TFlowFn> extends Effect.Effect<infer TFlow, any, any>
|
|
190
|
+
? TFlow extends Flow<any, any, infer TRequirements>
|
|
191
|
+
? Exclude<TRequirements, never> // Exclude UploadServer is handled by FlowPluginRequirements in core
|
|
192
|
+
: never
|
|
193
|
+
: never;
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Helper type to infer plugin requirements from a flow function.
|
|
197
|
+
*
|
|
198
|
+
* @example
|
|
199
|
+
* ```typescript
|
|
200
|
+
* const myFlow: TypeSafeFlowFunction<ImagePlugin | ZipPlugin> = ...;
|
|
201
|
+
* type Requirements = InferFlowRequirements<typeof myFlow>;
|
|
202
|
+
* // Requirements = ImagePlugin | ZipPlugin
|
|
203
|
+
* ```
|
|
204
|
+
*/
|
|
205
|
+
export type InferFlowRequirements<T> = T extends TypeSafeFlowFunction<infer R>
|
|
206
|
+
? R
|
|
207
|
+
: never;
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Converts PluginLayer types to Layer.Layer<any, never, any> for runtime use.
|
|
211
|
+
* Maintains type safety at compile time while allowing flexible runtime composition.
|
|
212
|
+
*/
|
|
213
|
+
export type RuntimePluginLayers<T extends PluginTuple> = {
|
|
214
|
+
[K in keyof T]: T[K] extends Layer.Layer<infer S, infer E, infer R>
|
|
215
|
+
? Layer.Layer<S, E, R>
|
|
216
|
+
: never;
|
|
217
|
+
};
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime plugin validation utilities.
|
|
3
|
+
*
|
|
4
|
+
* This module provides runtime validation to ensure that all plugins required
|
|
5
|
+
* by flows are actually provided to the server. While Effect-TS will catch
|
|
6
|
+
* missing dependencies at runtime, this validation provides better error messages
|
|
7
|
+
* and fails fast during server initialization.
|
|
8
|
+
*
|
|
9
|
+
* @module plugin-validation
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { PluginLayer } from "@uploadista/core";
|
|
13
|
+
import { Effect } from "effect";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Result of plugin validation.
|
|
17
|
+
*/
|
|
18
|
+
export type PluginValidationResult =
|
|
19
|
+
| {
|
|
20
|
+
success: true;
|
|
21
|
+
}
|
|
22
|
+
| {
|
|
23
|
+
success: false;
|
|
24
|
+
required: string[];
|
|
25
|
+
provided: string[];
|
|
26
|
+
missing: string[];
|
|
27
|
+
suggestions: Array<{
|
|
28
|
+
name: string;
|
|
29
|
+
packageName: string;
|
|
30
|
+
importStatement: string;
|
|
31
|
+
}>;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Known plugin mapping for generating helpful error messages.
|
|
36
|
+
*
|
|
37
|
+
* This maps service identifiers to their package names and variable names
|
|
38
|
+
* for generating import suggestions.
|
|
39
|
+
*/
|
|
40
|
+
const KNOWN_PLUGINS: Record<
|
|
41
|
+
string,
|
|
42
|
+
{ packageName: string; variableName: string }
|
|
43
|
+
> = {
|
|
44
|
+
ImagePlugin: {
|
|
45
|
+
packageName: "@uploadista/flow-images-sharp",
|
|
46
|
+
variableName: "sharpImagePlugin",
|
|
47
|
+
},
|
|
48
|
+
ImageAiPlugin: {
|
|
49
|
+
packageName: "@uploadista/flow-images-replicate",
|
|
50
|
+
variableName: "replicateImagePlugin",
|
|
51
|
+
},
|
|
52
|
+
ZipPlugin: {
|
|
53
|
+
packageName: "@uploadista/flow-utility-zipjs",
|
|
54
|
+
variableName: "zipPlugin",
|
|
55
|
+
},
|
|
56
|
+
CredentialProvider: {
|
|
57
|
+
packageName: "@uploadista/core",
|
|
58
|
+
variableName: "credentialProviderLayer",
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Extracts service identifier from a plugin layer.
|
|
64
|
+
*
|
|
65
|
+
* This attempts to identify the service provided by a layer using various
|
|
66
|
+
* heuristics. The exact implementation depends on how Effect-TS exposes
|
|
67
|
+
* layer metadata.
|
|
68
|
+
*
|
|
69
|
+
* @param layer - The plugin layer to inspect
|
|
70
|
+
* @returns Service identifier string or null if not identifiable
|
|
71
|
+
*/
|
|
72
|
+
function extractServiceIdentifier(layer: PluginLayer): string | null {
|
|
73
|
+
// Attempt to extract service identifier from layer
|
|
74
|
+
// Note: Effect-TS doesn't expose this information in a standard way,
|
|
75
|
+
// so we use Symbol.toStringTag or constructor name as fallbacks
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
// Try to get the service tag if available
|
|
79
|
+
// biome-ignore lint/suspicious/noExplicitAny: Layer introspection requires accessing internal properties
|
|
80
|
+
const layerAny = layer as any;
|
|
81
|
+
|
|
82
|
+
// Check for common patterns in Effect layers
|
|
83
|
+
if (layerAny._tag) {
|
|
84
|
+
return layerAny._tag;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (layerAny.constructor?.name) {
|
|
88
|
+
return layerAny.constructor.name;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Try to extract from the layer's context if available
|
|
92
|
+
if (layerAny.context?.services) {
|
|
93
|
+
const services = Array.from(layerAny.context.services.keys());
|
|
94
|
+
if (services.length > 0) {
|
|
95
|
+
// biome-ignore lint/suspicious/noExplicitAny: Service introspection requires accessing internal properties
|
|
96
|
+
const firstService = services[0] as any;
|
|
97
|
+
if (firstService.key) {
|
|
98
|
+
return firstService.key;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return null;
|
|
104
|
+
} catch {
|
|
105
|
+
// If we can't extract the identifier, return null
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Extracts service identifiers from an array of plugin layers.
|
|
112
|
+
*
|
|
113
|
+
* @param plugins - Array of plugin layers
|
|
114
|
+
* @returns Array of service identifier strings
|
|
115
|
+
*/
|
|
116
|
+
export function extractServiceIdentifiers(
|
|
117
|
+
plugins: readonly PluginLayer[],
|
|
118
|
+
): string[] {
|
|
119
|
+
return plugins
|
|
120
|
+
.map((plugin) => extractServiceIdentifier(plugin))
|
|
121
|
+
.filter((id): id is string => id !== null);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Validates that all required plugins are provided.
|
|
126
|
+
*
|
|
127
|
+
* This is a runtime validation function that checks if the plugins array
|
|
128
|
+
* contains all services required by the flows. It's called during server
|
|
129
|
+
* initialization to provide early, clear error messages.
|
|
130
|
+
*
|
|
131
|
+
* Note: This validation is best-effort because we can't reliably extract
|
|
132
|
+
* requirements from flow functions at runtime without executing them.
|
|
133
|
+
* The main validation happens via Effect-TS's dependency injection.
|
|
134
|
+
*
|
|
135
|
+
* @param config - Validation configuration
|
|
136
|
+
* @returns Validation result with detailed error information if validation fails
|
|
137
|
+
*
|
|
138
|
+
* @example
|
|
139
|
+
* ```typescript
|
|
140
|
+
* const result = validatePluginRequirements({
|
|
141
|
+
* plugins: [sharpImagePlugin, zipPlugin],
|
|
142
|
+
* expectedServices: ['ImagePlugin', 'ZipPlugin']
|
|
143
|
+
* });
|
|
144
|
+
*
|
|
145
|
+
* if (!result.success) {
|
|
146
|
+
* console.error('Missing plugins:', result.missing);
|
|
147
|
+
* console.error('Suggestions:', result.suggestions);
|
|
148
|
+
* }
|
|
149
|
+
* ```
|
|
150
|
+
*/
|
|
151
|
+
export function validatePluginRequirements(config: {
|
|
152
|
+
plugins: readonly PluginLayer[];
|
|
153
|
+
expectedServices?: string[];
|
|
154
|
+
}): PluginValidationResult {
|
|
155
|
+
const { plugins, expectedServices = [] } = config;
|
|
156
|
+
|
|
157
|
+
// Extract identifiers from provided plugins
|
|
158
|
+
const providedServices = extractServiceIdentifiers(plugins);
|
|
159
|
+
|
|
160
|
+
// Check for missing services
|
|
161
|
+
const missing = expectedServices.filter(
|
|
162
|
+
(required) => !providedServices.includes(required),
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
if (missing.length === 0) {
|
|
166
|
+
return { success: true };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Generate suggestions for missing plugins
|
|
170
|
+
const suggestions = missing
|
|
171
|
+
.map((service) => {
|
|
172
|
+
const knownPlugin = KNOWN_PLUGINS[service];
|
|
173
|
+
if (!knownPlugin) {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
name: service,
|
|
179
|
+
packageName: knownPlugin.packageName,
|
|
180
|
+
importStatement: `import { ${knownPlugin.variableName} } from '${knownPlugin.packageName}';`,
|
|
181
|
+
};
|
|
182
|
+
})
|
|
183
|
+
.filter((s): s is NonNullable<typeof s> => s !== null);
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
success: false,
|
|
187
|
+
required: expectedServices,
|
|
188
|
+
provided: providedServices,
|
|
189
|
+
missing,
|
|
190
|
+
suggestions,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Creates a formatted error message for plugin validation failures.
|
|
196
|
+
*
|
|
197
|
+
* This generates a detailed, human-readable error message that includes:
|
|
198
|
+
* - List of required plugins
|
|
199
|
+
* - List of provided plugins
|
|
200
|
+
* - List of missing plugins
|
|
201
|
+
* - Import statements for missing plugins (if known)
|
|
202
|
+
* - Example server configuration
|
|
203
|
+
*
|
|
204
|
+
* @param result - Failed validation result
|
|
205
|
+
* @returns Formatted error message string
|
|
206
|
+
*
|
|
207
|
+
* @example
|
|
208
|
+
* ```typescript
|
|
209
|
+
* const result = validatePluginRequirements({ ... });
|
|
210
|
+
* if (!result.success) {
|
|
211
|
+
* const message = formatPluginValidationError(result);
|
|
212
|
+
* throw new Error(message);
|
|
213
|
+
* }
|
|
214
|
+
* ```
|
|
215
|
+
*/
|
|
216
|
+
export function formatPluginValidationError(
|
|
217
|
+
result: Extract<PluginValidationResult, { success: false }>,
|
|
218
|
+
): string {
|
|
219
|
+
const lines: string[] = [
|
|
220
|
+
"Server initialization failed: Missing required plugins",
|
|
221
|
+
"",
|
|
222
|
+
`Required: ${result.required.join(", ")}`,
|
|
223
|
+
`Provided: ${result.provided.length > 0 ? result.provided.join(", ") : "(none)"}`,
|
|
224
|
+
`Missing: ${result.missing.join(", ")}`,
|
|
225
|
+
"",
|
|
226
|
+
];
|
|
227
|
+
|
|
228
|
+
if (result.suggestions.length > 0) {
|
|
229
|
+
lines.push("Add the missing plugins to your configuration:");
|
|
230
|
+
lines.push("");
|
|
231
|
+
for (const suggestion of result.suggestions) {
|
|
232
|
+
lines.push(` ${suggestion.importStatement}`);
|
|
233
|
+
}
|
|
234
|
+
lines.push("");
|
|
235
|
+
lines.push(" const server = await createUploadistaServer({");
|
|
236
|
+
lines.push(
|
|
237
|
+
` plugins: [${[...result.provided, ...result.missing.map((m) => KNOWN_PLUGINS[m]?.variableName || m)].join(", ")}],`,
|
|
238
|
+
);
|
|
239
|
+
lines.push(" // ...");
|
|
240
|
+
lines.push(" });");
|
|
241
|
+
} else {
|
|
242
|
+
lines.push(
|
|
243
|
+
"Note: Could not determine package names for missing plugins.",
|
|
244
|
+
);
|
|
245
|
+
lines.push("Please ensure all required plugin layers are provided.");
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return lines.join("\n");
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Effect-based plugin validation that can be composed with other Effects.
|
|
253
|
+
*
|
|
254
|
+
* This provides an Effect-TS native way to validate plugins, allowing it
|
|
255
|
+
* to be composed with other Effects in the server initialization pipeline.
|
|
256
|
+
*
|
|
257
|
+
* @param config - Validation configuration
|
|
258
|
+
* @returns Effect that succeeds if validation passes, fails with UploadistaError if not
|
|
259
|
+
*
|
|
260
|
+
* @example
|
|
261
|
+
* ```typescript
|
|
262
|
+
* const validatedServer = Effect.gen(function* () {
|
|
263
|
+
* yield* validatePluginRequirementsEffect({
|
|
264
|
+
* plugins: [sharpImagePlugin],
|
|
265
|
+
* expectedServices: ['ImagePlugin', 'ZipPlugin']
|
|
266
|
+
* });
|
|
267
|
+
*
|
|
268
|
+
* return yield* createServerEffect(...);
|
|
269
|
+
* });
|
|
270
|
+
* ```
|
|
271
|
+
*/
|
|
272
|
+
export function validatePluginRequirementsEffect(config: {
|
|
273
|
+
plugins: readonly PluginLayer[];
|
|
274
|
+
expectedServices?: string[];
|
|
275
|
+
}): Effect.Effect<void, Error> {
|
|
276
|
+
return Effect.sync(() => {
|
|
277
|
+
const result = validatePluginRequirements(config);
|
|
278
|
+
|
|
279
|
+
if (!result.success) {
|
|
280
|
+
const message = formatPluginValidationError(result);
|
|
281
|
+
throw new Error(message);
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Validates plugin configuration at runtime during server initialization.
|
|
288
|
+
*
|
|
289
|
+
* This is a convenience function that performs validation and throws a
|
|
290
|
+
* descriptive error if validation fails. Use this at the beginning of
|
|
291
|
+
* createUploadistaServer to fail fast with clear error messages.
|
|
292
|
+
*
|
|
293
|
+
* @param config - Validation configuration
|
|
294
|
+
* @throws Error with detailed message if validation fails
|
|
295
|
+
*
|
|
296
|
+
* @example
|
|
297
|
+
* ```typescript
|
|
298
|
+
* export const createUploadistaServer = async (config) => {
|
|
299
|
+
* // Validate plugins early
|
|
300
|
+
* validatePluginsOrThrow({
|
|
301
|
+
* plugins: config.plugins,
|
|
302
|
+
* expectedServices: ['ImagePlugin', 'ZipPlugin']
|
|
303
|
+
* });
|
|
304
|
+
*
|
|
305
|
+
* // Continue with server creation...
|
|
306
|
+
* };
|
|
307
|
+
* ```
|
|
308
|
+
*/
|
|
309
|
+
export function validatePluginsOrThrow(config: {
|
|
310
|
+
plugins: readonly PluginLayer[];
|
|
311
|
+
expectedServices?: string[];
|
|
312
|
+
}): void {
|
|
313
|
+
const result = validatePluginRequirements(config);
|
|
314
|
+
|
|
315
|
+
if (!result.success) {
|
|
316
|
+
const message = formatPluginValidationError(result);
|
|
317
|
+
throw new Error(message);
|
|
318
|
+
}
|
|
319
|
+
}
|