@uploadista/core 0.0.13 → 0.0.14

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.
Files changed (97) hide show
  1. package/dist/{checksum-CtOagryS.mjs → checksum-BaO9w1gC.mjs} +2 -2
  2. package/dist/{checksum-CtOagryS.mjs.map → checksum-BaO9w1gC.mjs.map} +1 -1
  3. package/dist/{checksum-jmKtZ9W8.cjs → checksum-DXCv7Avr.cjs} +1 -1
  4. package/dist/errors/index.cjs +1 -1
  5. package/dist/errors/index.d.cts +1 -1
  6. package/dist/errors/index.d.mts +1 -1
  7. package/dist/errors/index.mjs +1 -1
  8. package/dist/flow/index.cjs +1 -1
  9. package/dist/flow/index.d.cts +5 -5
  10. package/dist/flow/index.d.mts +5 -5
  11. package/dist/flow/index.mjs +1 -1
  12. package/dist/flow-DhuIQwjv.mjs +2 -0
  13. package/dist/flow-DhuIQwjv.mjs.map +1 -0
  14. package/dist/flow-s_AlC4r5.cjs +1 -0
  15. package/dist/{index-Bi9YYid8.d.mts → index-3jSHmGwH.d.mts} +2 -2
  16. package/dist/{index-Bi9YYid8.d.mts.map → index-3jSHmGwH.d.mts.map} +1 -1
  17. package/dist/{index-4VDJDcWM.d.cts → index-5K4oXy67.d.cts} +822 -169
  18. package/dist/index-5K4oXy67.d.cts.map +1 -0
  19. package/dist/{index-RgOX4psL.d.mts → index-BB1v4Ynz.d.mts} +822 -169
  20. package/dist/index-BB1v4Ynz.d.mts.map +1 -0
  21. package/dist/{index-Cbf1OPLp.d.mts → index-Bu5i-gcV.d.mts} +2 -2
  22. package/dist/index-Bu5i-gcV.d.mts.map +1 -0
  23. package/dist/{index-De4wQJwR.d.cts → index-CHGBYDtr.d.cts} +2 -2
  24. package/dist/{index-De4wQJwR.d.cts.map → index-CHGBYDtr.d.cts.map} +1 -1
  25. package/dist/{index-qZ90PVNl.d.cts → index-T6MZvUlM.d.cts} +2 -2
  26. package/dist/{index-Cbf1OPLp.d.mts.map → index-T6MZvUlM.d.cts.map} +1 -1
  27. package/dist/index.cjs +1 -1
  28. package/dist/index.d.cts +5 -5
  29. package/dist/index.d.mts +5 -5
  30. package/dist/index.mjs +1 -1
  31. package/dist/{stream-limiter-D9rrsvAT.cjs → stream-limiter-BcTJAjs-.cjs} +1 -1
  32. package/dist/{stream-limiter-D9KSAaoY.mjs → stream-limiter-D1-sVS5i.mjs} +2 -2
  33. package/dist/{stream-limiter-D9KSAaoY.mjs.map → stream-limiter-D1-sVS5i.mjs.map} +1 -1
  34. package/dist/streams/index.cjs +1 -1
  35. package/dist/streams/index.d.cts +2 -2
  36. package/dist/streams/index.d.mts +2 -2
  37. package/dist/streams/index.mjs +1 -1
  38. package/dist/testing/index.cjs +1 -1
  39. package/dist/testing/index.d.cts +4 -4
  40. package/dist/testing/index.d.mts +4 -4
  41. package/dist/testing/index.mjs +1 -1
  42. package/dist/types/index.cjs +1 -1
  43. package/dist/types/index.d.cts +4 -4
  44. package/dist/types/index.d.mts +4 -4
  45. package/dist/types/index.mjs +1 -1
  46. package/dist/types-B-EckCWW.cjs +1 -0
  47. package/dist/types-CO-R4pFG.mjs +2 -0
  48. package/dist/types-CO-R4pFG.mjs.map +1 -0
  49. package/dist/upload/index.cjs +1 -1
  50. package/dist/upload/index.d.cts +4 -4
  51. package/dist/upload/index.d.mts +4 -4
  52. package/dist/upload/index.mjs +1 -1
  53. package/dist/{upload-D-eiOIVG.cjs → upload-BwXGQQ26.cjs} +1 -1
  54. package/dist/upload-C_Ew1NMF.mjs +2 -0
  55. package/dist/{upload-Yj5lrtZo.mjs.map → upload-C_Ew1NMF.mjs.map} +1 -1
  56. package/dist/{uploadista-error-B-n8Kfyh.cjs → uploadista-error-Blmj3lpk.cjs} +5 -1
  57. package/dist/{uploadista-error-DUWw6OqS.d.mts → uploadista-error-Cpn3uBLO.d.mts} +2 -2
  58. package/dist/uploadista-error-Cpn3uBLO.d.mts.map +1 -0
  59. package/dist/{uploadista-error-BQLhNZcY.d.cts → uploadista-error-DgdQnozn.d.cts} +2 -2
  60. package/dist/uploadista-error-DgdQnozn.d.cts.map +1 -0
  61. package/dist/{uploadista-error-Buscq-FR.mjs → uploadista-error-DhNBioWq.mjs} +5 -1
  62. package/dist/uploadista-error-DhNBioWq.mjs.map +1 -0
  63. package/dist/utils/index.cjs +1 -1
  64. package/dist/utils/index.d.cts +2 -2
  65. package/dist/utils/index.d.mts +2 -2
  66. package/dist/utils/index.mjs +1 -1
  67. package/dist/{utils-BWiu6lqv.mjs → utils-7gziergl.mjs} +2 -2
  68. package/dist/{utils-BWiu6lqv.mjs.map → utils-7gziergl.mjs.map} +1 -1
  69. package/dist/{utils-_StwBtxT.cjs → utils-C_STf6Wl.cjs} +1 -1
  70. package/package.json +3 -3
  71. package/src/errors/uploadista-error.ts +21 -1
  72. package/src/flow/event.ts +28 -4
  73. package/src/flow/flow-server.ts +43 -12
  74. package/src/flow/flow.ts +92 -13
  75. package/src/flow/index.ts +7 -0
  76. package/src/flow/node-types/index.ts +85 -0
  77. package/src/flow/node.ts +48 -6
  78. package/src/flow/nodes/input-node.ts +2 -0
  79. package/src/flow/nodes/storage-node.ts +2 -0
  80. package/src/flow/type-guards.ts +293 -0
  81. package/src/flow/type-registry.ts +345 -0
  82. package/src/flow/types/flow-job.ts +22 -6
  83. package/src/flow/types/flow-types.ts +152 -3
  84. package/tests/flow/type-system.test.ts +799 -0
  85. package/dist/flow-ChADffZ5.cjs +0 -1
  86. package/dist/flow-_J9-Dm_m.mjs +0 -2
  87. package/dist/flow-_J9-Dm_m.mjs.map +0 -1
  88. package/dist/index-4VDJDcWM.d.cts.map +0 -1
  89. package/dist/index-RgOX4psL.d.mts.map +0 -1
  90. package/dist/index-qZ90PVNl.d.cts.map +0 -1
  91. package/dist/types-BI_KmpTc.mjs +0 -2
  92. package/dist/types-BI_KmpTc.mjs.map +0 -1
  93. package/dist/types-f08UsX4E.cjs +0 -1
  94. package/dist/upload-Yj5lrtZo.mjs +0 -2
  95. package/dist/uploadista-error-BQLhNZcY.d.cts.map +0 -1
  96. package/dist/uploadista-error-Buscq-FR.mjs.map +0 -1
  97. package/dist/uploadista-error-DUWw6OqS.d.mts.map +0 -1
@@ -0,0 +1,293 @@
1
+ /**
2
+ * Type guards and helpers for safe type narrowing of flow results.
3
+ *
4
+ * This module provides runtime type guards for discriminating between different
5
+ * types of flow outputs. Type guards validate both the type tag and the data
6
+ * structure against registered schemas.
7
+ *
8
+ * @module flow/type-guards
9
+ *
10
+ * @example
11
+ * ```typescript
12
+ * import { isStorageOutput, filterOutputsByType } from "@uploadista/core/flow";
13
+ *
14
+ * // Type-safe result consumption
15
+ * if (result.success && result.flowOutputs) {
16
+ * const storageOutputs = filterOutputsByType(result.flowOutputs, isStorageOutput);
17
+ * for (const output of storageOutputs) {
18
+ * // output.data is typed as UploadFile
19
+ * console.log("Stored at:", output.data.url);
20
+ * }
21
+ * }
22
+ * ```
23
+ */
24
+
25
+ import { Effect } from "effect";
26
+ import { UploadistaError } from "../errors";
27
+ import type { UploadFile } from "../types";
28
+ import type { TypedOutput } from "./types/flow-types";
29
+ import { flowTypeRegistry } from "./type-registry";
30
+
31
+ /**
32
+ * Factory function to create type guards for specific node types.
33
+ *
34
+ * Creates a TypeScript type guard that validates both the type tag and
35
+ * the data structure against the registered schema. This enables type-safe
36
+ * narrowing of TypedOutput objects in TypeScript.
37
+ *
38
+ * @template T - The expected TypeScript type after narrowing
39
+ * @param typeId - The registered type ID to check against (e.g., "storage-output-v1")
40
+ * @returns A type guard function that narrows TypedOutput to TypedOutput<T>
41
+ *
42
+ * @example
43
+ * ```typescript
44
+ * import { createTypeGuard } from "@uploadista/core/flow";
45
+ * import { z } from "zod";
46
+ *
47
+ * const descriptionSchema = z.object({
48
+ * description: z.string(),
49
+ * confidence: z.number(),
50
+ * });
51
+ *
52
+ * type DescriptionOutput = z.infer<typeof descriptionSchema>;
53
+ *
54
+ * const isDescriptionOutput = createTypeGuard<DescriptionOutput>(
55
+ * "description-output-v1"
56
+ * );
57
+ *
58
+ * // Use in code
59
+ * if (isDescriptionOutput(output)) {
60
+ * // output.data is typed as DescriptionOutput
61
+ * console.log(output.data.description);
62
+ * }
63
+ * ```
64
+ */
65
+ export function createTypeGuard<T>(
66
+ typeId: string,
67
+ ): (output: TypedOutput) => output is TypedOutput<T> {
68
+ return (output: TypedOutput): output is TypedOutput<T> => {
69
+ // Check type matches
70
+ if (output.nodeType !== typeId) return false;
71
+
72
+ // Validate against registered schema
73
+ const typeDef = flowTypeRegistry.get(typeId);
74
+ if (!typeDef) return false;
75
+
76
+ const result = typeDef.schema.safeParse(output.data);
77
+ return result.success;
78
+ };
79
+ }
80
+
81
+ /**
82
+ * Type guard for storage output nodes.
83
+ *
84
+ * Validates that an output is from a storage node and contains valid UploadFile data.
85
+ *
86
+ * @param output - The output to check
87
+ * @returns True if the output is a storage output with valid UploadFile data
88
+ *
89
+ * @example
90
+ * ```typescript
91
+ * import { isStorageOutput } from "@uploadista/core/flow";
92
+ *
93
+ * if (isStorageOutput(output)) {
94
+ * // output.data is typed as UploadFile
95
+ * console.log("File URL:", output.data.url);
96
+ * console.log("File size:", output.data.size);
97
+ * }
98
+ * ```
99
+ */
100
+ export const isStorageOutput = createTypeGuard<UploadFile>(
101
+ "storage-output-v1",
102
+ );
103
+
104
+ /**
105
+ * Filter an array of outputs to only those matching a specific type.
106
+ *
107
+ * This helper function filters outputs using a type guard and returns a
108
+ * properly typed array of results. It's useful for extracting specific
109
+ * output types from multi-output flows.
110
+ *
111
+ * @template T - The expected output data type
112
+ * @param outputs - Array of typed outputs to filter
113
+ * @param typeGuard - Type guard function to use for filtering
114
+ * @returns Array of outputs that match the type guard, properly typed
115
+ *
116
+ * @example
117
+ * ```typescript
118
+ * import { filterOutputsByType, isStorageOutput } from "@uploadista/core/flow";
119
+ *
120
+ * // Get all storage outputs from a multi-output flow
121
+ * const storageOutputs = filterOutputsByType(
122
+ * flowResult.outputs,
123
+ * isStorageOutput
124
+ * );
125
+ *
126
+ * for (const output of storageOutputs) {
127
+ * // Each output.data is typed as UploadFile
128
+ * console.log("Saved file:", output.data.url);
129
+ * }
130
+ * ```
131
+ */
132
+ export function filterOutputsByType<T>(
133
+ outputs: TypedOutput[],
134
+ typeGuard: (output: TypedOutput) => output is TypedOutput<T>,
135
+ ): TypedOutput<T>[] {
136
+ return outputs.filter(typeGuard);
137
+ }
138
+
139
+ /**
140
+ * Get a single output of a specific type from an array of outputs.
141
+ *
142
+ * This helper function finds exactly one output matching the type guard.
143
+ * It throws an error if no outputs match or if multiple outputs match,
144
+ * ensuring the caller receives exactly the expected result.
145
+ *
146
+ * @template T - The expected output data type
147
+ * @param outputs - Array of typed outputs to search
148
+ * @param typeGuard - Type guard function to use for matching
149
+ * @returns The single matching output, properly typed
150
+ * @throws {UploadistaError} If no outputs match (OUTPUT_NOT_FOUND)
151
+ * @throws {UploadistaError} If multiple outputs match (MULTIPLE_OUTPUTS_FOUND)
152
+ *
153
+ * @example
154
+ * ```typescript
155
+ * import { getSingleOutputByType, isStorageOutput } from "@uploadista/core/flow";
156
+ *
157
+ * try {
158
+ * const storageOutput = getSingleOutputByType(
159
+ * flowResult.outputs,
160
+ * isStorageOutput
161
+ * );
162
+ * // storageOutput.data is typed as UploadFile
163
+ * console.log("File saved at:", storageOutput.data.url);
164
+ * } catch (error) {
165
+ * if (error.code === "OUTPUT_NOT_FOUND") {
166
+ * console.error("No storage output found");
167
+ * } else if (error.code === "MULTIPLE_OUTPUTS_FOUND") {
168
+ * console.error("Multiple storage outputs found, expected one");
169
+ * }
170
+ * }
171
+ * ```
172
+ */
173
+ export function getSingleOutputByType<T>(
174
+ outputs: TypedOutput[],
175
+ typeGuard: (output: TypedOutput) => output is TypedOutput<T>,
176
+ ): Effect.Effect<TypedOutput<T>, UploadistaError> {
177
+ return Effect.gen(function* () {
178
+ const filtered = filterOutputsByType(outputs, typeGuard);
179
+
180
+ if (filtered.length === 0) {
181
+ return yield* UploadistaError.fromCode("OUTPUT_NOT_FOUND", {
182
+ body: "No output of the specified type was found in the flow results",
183
+ }).toEffect();
184
+ }
185
+
186
+ if (filtered.length > 1) {
187
+ return yield* UploadistaError.fromCode("MULTIPLE_OUTPUTS_FOUND", {
188
+ body: `Found ${filtered.length} outputs of the specified type, expected exactly one`,
189
+ details: {
190
+ foundCount: filtered.length,
191
+ nodeIds: filtered.map((o) => o.nodeId),
192
+ },
193
+ }).toEffect();
194
+ }
195
+
196
+ // TypeScript knows filtered.length is 1 here due to the checks above
197
+ // biome-ignore lint/style/noNonNullAssertion: We've checked the length above
198
+ return filtered[0]!;
199
+ });
200
+ }
201
+
202
+ /**
203
+ * Get the first output of a specific type, if any exists.
204
+ *
205
+ * Unlike getSingleOutputByType, this function returns undefined if no outputs
206
+ * match, and returns the first match if multiple outputs exist. This is useful
207
+ * when you want a more lenient matching strategy.
208
+ *
209
+ * @template T - The expected output data type
210
+ * @param outputs - Array of typed outputs to search
211
+ * @param typeGuard - Type guard function to use for matching
212
+ * @returns The first matching output, or undefined if none match
213
+ *
214
+ * @example
215
+ * ```typescript
216
+ * import { getFirstOutputByType, isStorageOutput } from "@uploadista/core/flow";
217
+ *
218
+ * const storageOutput = getFirstOutputByType(
219
+ * flowResult.outputs,
220
+ * isStorageOutput
221
+ * );
222
+ *
223
+ * if (storageOutput) {
224
+ * console.log("First storage output:", storageOutput.data.url);
225
+ * } else {
226
+ * console.log("No storage outputs found");
227
+ * }
228
+ * ```
229
+ */
230
+ export function getFirstOutputByType<T>(
231
+ outputs: TypedOutput[],
232
+ typeGuard: (output: TypedOutput) => output is TypedOutput<T>,
233
+ ): TypedOutput<T> | undefined {
234
+ const filtered = filterOutputsByType(outputs, typeGuard);
235
+ return filtered[0];
236
+ }
237
+
238
+ /**
239
+ * Get an output by its node ID.
240
+ *
241
+ * This helper finds an output produced by a specific node instance,
242
+ * regardless of its type. Useful when you know the specific node ID
243
+ * you're looking for.
244
+ *
245
+ * @param outputs - Array of typed outputs to search
246
+ * @param nodeId - The node ID to match
247
+ * @returns The output from the specified node, or undefined if not found
248
+ *
249
+ * @example
250
+ * ```typescript
251
+ * import { getOutputByNodeId } from "@uploadista/core/flow";
252
+ *
253
+ * const cdnOutput = getOutputByNodeId(flowResult.outputs, "cdn-storage");
254
+ * if (cdnOutput) {
255
+ * console.log("CDN output:", cdnOutput.data);
256
+ * }
257
+ * ```
258
+ */
259
+ export function getOutputByNodeId(
260
+ outputs: TypedOutput[],
261
+ nodeId: string,
262
+ ): TypedOutput | undefined {
263
+ return outputs.find((output) => output.nodeId === nodeId);
264
+ }
265
+
266
+ /**
267
+ * Check if any outputs match a specific type.
268
+ *
269
+ * Simple predicate function to check if at least one output of a given
270
+ * type exists in the results.
271
+ *
272
+ * @template T - The expected output data type
273
+ * @param outputs - Array of typed outputs to check
274
+ * @param typeGuard - Type guard function to use for checking
275
+ * @returns True if at least one output matches the type guard
276
+ *
277
+ * @example
278
+ * ```typescript
279
+ * import { hasOutputOfType, isStorageOutput } from "@uploadista/core/flow";
280
+ *
281
+ * if (hasOutputOfType(flowResult.outputs, isStorageOutput)) {
282
+ * console.log("Flow produced at least one storage output");
283
+ * } else {
284
+ * console.log("No storage outputs in this flow");
285
+ * }
286
+ * ```
287
+ */
288
+ export function hasOutputOfType<T>(
289
+ outputs: TypedOutput[],
290
+ typeGuard: (output: TypedOutput) => output is TypedOutput<T>,
291
+ ): boolean {
292
+ return outputs.some(typeGuard);
293
+ }
@@ -0,0 +1,345 @@
1
+ /**
2
+ * Type registry system for flow input and output nodes.
3
+ *
4
+ * This module provides a centralized registry for node type definitions with schemas
5
+ * and metadata. The registry enables type-safe flow result consumption in dynamic
6
+ * client environments by allowing clients to safely cast flow results based on
7
+ * registered node types.
8
+ *
9
+ * @module flow/type-registry
10
+ * @see {@link FlowTypeRegistry} for the registry implementation
11
+ * @see {@link NodeTypeDefinition} for type definition structure
12
+ *
13
+ * @example
14
+ * ```typescript
15
+ * // Register a custom output type
16
+ * import { flowTypeRegistry } from "@uploadista/core/flow";
17
+ * import { z } from "zod";
18
+ *
19
+ * const descriptionOutputSchema = z.object({
20
+ * description: z.string(),
21
+ * confidence: z.number(),
22
+ * });
23
+ *
24
+ * flowTypeRegistry.register({
25
+ * id: "description-output-v1",
26
+ * category: "output",
27
+ * schema: descriptionOutputSchema,
28
+ * version: "1.0.0",
29
+ * description: "AI-powered image description output",
30
+ * });
31
+ *
32
+ * // Later, validate data against the registered type
33
+ * const result = flowTypeRegistry.validate("description-output-v1", data);
34
+ * if (result.success) {
35
+ * console.log(result.data.description);
36
+ * }
37
+ * ```
38
+ */
39
+
40
+ import type { z } from "zod";
41
+ import { UploadistaError } from "../errors";
42
+
43
+ /**
44
+ * Node type category - determines where the node appears in the flow.
45
+ *
46
+ * - `input`: Nodes that receive data from external sources (e.g., file uploads)
47
+ * - `output`: Nodes that produce final results (e.g., storage, webhooks, descriptions)
48
+ */
49
+ export type NodeTypeCategory = "input" | "output";
50
+
51
+ /**
52
+ * Defines a registered node type with its schema and metadata.
53
+ *
54
+ * Node type definitions are registered globally and used to validate and type-narrow
55
+ * flow results at runtime. Each definition includes:
56
+ * - A unique identifier with versioning
57
+ * - A category (input or output)
58
+ * - A Zod schema for runtime validation
59
+ * - A semantic version for evolution
60
+ * - A human-readable description
61
+ *
62
+ * @template TSchema - The Zod schema type for this node's data
63
+ *
64
+ * @property id - Unique identifier (e.g., "storage-output-v1", "webhook-output-v1")
65
+ * @property category - Whether this is an input or output node type
66
+ * @property schema - Zod schema for validating data produced by this node type
67
+ * @property version - Semantic version (e.g., "1.0.0") for tracking type evolution
68
+ * @property description - Human-readable explanation of what this node type does
69
+ *
70
+ * @example
71
+ * ```typescript
72
+ * const storageOutputDef: NodeTypeDefinition<z.infer<typeof uploadFileSchema>> = {
73
+ * id: "storage-output-v1",
74
+ * category: "output",
75
+ * schema: uploadFileSchema,
76
+ * version: "1.0.0",
77
+ * description: "Storage output node that saves files to configured storage backend",
78
+ * };
79
+ * ```
80
+ */
81
+ export interface NodeTypeDefinition<TSchema = unknown> {
82
+ id: string;
83
+ category: NodeTypeCategory;
84
+ schema: z.ZodSchema<TSchema>;
85
+ version: string;
86
+ description: string;
87
+ }
88
+
89
+ /**
90
+ * Result type for validation operations.
91
+ *
92
+ * @template T - The expected type on successful validation
93
+ */
94
+ export type ValidationResult<T> =
95
+ | { success: true; data: T }
96
+ | { success: false; error: UploadistaError };
97
+
98
+ /**
99
+ * Central registry for node type definitions.
100
+ *
101
+ * The FlowTypeRegistry maintains a global registry of node types with their schemas
102
+ * and metadata. It provides methods for:
103
+ * - Registering new node types
104
+ * - Retrieving type definitions
105
+ * - Listing types by category
106
+ * - Validating data against registered schemas
107
+ *
108
+ * The registry is immutable after registration - types cannot be modified or removed
109
+ * once registered to prevent runtime errors.
110
+ *
111
+ * @remarks
112
+ * - This is a singleton - use the exported `flowTypeRegistry` instance
113
+ * - Types cannot be unregistered or modified after registration
114
+ * - Duplicate type IDs are rejected
115
+ * - Version strings should follow semantic versioning
116
+ *
117
+ * @example
118
+ * ```typescript
119
+ * // Register a new type
120
+ * flowTypeRegistry.register({
121
+ * id: "webhook-output-v1",
122
+ * category: "output",
123
+ * schema: webhookResponseSchema,
124
+ * version: "1.0.0",
125
+ * description: "HTTP webhook notification output",
126
+ * });
127
+ *
128
+ * // Retrieve a type definition
129
+ * const def = flowTypeRegistry.get("webhook-output-v1");
130
+ * if (def) {
131
+ * console.log(def.description);
132
+ * }
133
+ *
134
+ * // List all output types
135
+ * const outputTypes = flowTypeRegistry.listByCategory("output");
136
+ * console.log(outputTypes.map(t => t.id));
137
+ *
138
+ * // Validate data
139
+ * const result = flowTypeRegistry.validate("webhook-output-v1", data);
140
+ * if (result.success) {
141
+ * // data is now typed according to the schema
142
+ * processWebhookResponse(result.data);
143
+ * }
144
+ * ```
145
+ */
146
+ export class FlowTypeRegistry {
147
+ private readonly types: Map<string, NodeTypeDefinition<unknown>>;
148
+
149
+ constructor() {
150
+ this.types = new Map();
151
+ }
152
+
153
+ /**
154
+ * Register a new node type in the registry.
155
+ *
156
+ * Once registered, a type cannot be modified or removed. Attempting to register
157
+ * a type with a duplicate ID will throw an error.
158
+ *
159
+ * @template T - The TypeScript type inferred from the Zod schema
160
+ * @param definition - The complete type definition including schema and metadata
161
+ * @throws {UploadistaError} If a type with the same ID is already registered
162
+ *
163
+ * @example
164
+ * ```typescript
165
+ * import { z } from "zod";
166
+ *
167
+ * flowTypeRegistry.register({
168
+ * id: "description-output-v1",
169
+ * category: "output",
170
+ * schema: z.object({
171
+ * description: z.string(),
172
+ * confidence: z.number().min(0).max(1),
173
+ * tags: z.array(z.string()).optional(),
174
+ * }),
175
+ * version: "1.0.0",
176
+ * description: "AI-generated image description with confidence score",
177
+ * });
178
+ * ```
179
+ */
180
+ register<T>(definition: NodeTypeDefinition<T>): void {
181
+ if (this.types.has(definition.id)) {
182
+ throw UploadistaError.fromCode("VALIDATION_ERROR", {
183
+ body: `Node type "${definition.id}" is already registered. Types cannot be modified or re-registered.`,
184
+ details: { typeId: definition.id },
185
+ });
186
+ }
187
+
188
+ // Store as unknown to avoid generic constraints in the Map
189
+ this.types.set(definition.id, definition as NodeTypeDefinition<unknown>);
190
+ }
191
+
192
+ /**
193
+ * Retrieve a registered type definition by its ID.
194
+ *
195
+ * @param id - The unique type identifier (e.g., "storage-output-v1")
196
+ * @returns The type definition if found, undefined otherwise
197
+ *
198
+ * @example
199
+ * ```typescript
200
+ * const def = flowTypeRegistry.get("storage-output-v1");
201
+ * if (def) {
202
+ * console.log(`Found ${def.description} (v${def.version})`);
203
+ * } else {
204
+ * console.warn("Type not registered");
205
+ * }
206
+ * ```
207
+ */
208
+ get(id: string): NodeTypeDefinition<unknown> | undefined {
209
+ return this.types.get(id);
210
+ }
211
+
212
+ /**
213
+ * List all registered types in a specific category.
214
+ *
215
+ * @param category - The node category to filter by ("input" or "output")
216
+ * @returns Array of type definitions in the specified category
217
+ *
218
+ * @example
219
+ * ```typescript
220
+ * // List all registered output types
221
+ * const outputTypes = flowTypeRegistry.listByCategory("output");
222
+ * console.log("Available output types:");
223
+ * for (const type of outputTypes) {
224
+ * console.log(`- ${type.id}: ${type.description}`);
225
+ * }
226
+ * ```
227
+ */
228
+ listByCategory(category: NodeTypeCategory): NodeTypeDefinition<unknown>[] {
229
+ const result: NodeTypeDefinition<unknown>[] = [];
230
+ for (const definition of this.types.values()) {
231
+ if (definition.category === category) {
232
+ result.push(definition);
233
+ }
234
+ }
235
+ return result;
236
+ }
237
+
238
+ /**
239
+ * Validate data against a registered type's schema.
240
+ *
241
+ * This method performs runtime validation using the Zod schema associated with
242
+ * the type. If validation succeeds, the data is returned with proper typing.
243
+ * If validation fails, an UploadistaError is returned with details.
244
+ *
245
+ * @template T - The expected TypeScript type after validation
246
+ * @param typeId - The ID of the registered type to validate against
247
+ * @param data - The data to validate
248
+ * @returns A result object with either the validated data or an error
249
+ *
250
+ * @example
251
+ * ```typescript
252
+ * const result = flowTypeRegistry.validate("storage-output-v1", unknownData);
253
+ *
254
+ * if (result.success) {
255
+ * // TypeScript knows result.data is an UploadFile
256
+ * console.log(`File stored at: ${result.data.url}`);
257
+ * } else {
258
+ * console.error(`Validation failed: ${result.error.body}`);
259
+ * }
260
+ * ```
261
+ */
262
+ validate<T>(typeId: string, data: unknown): ValidationResult<T> {
263
+ const typeDef = this.types.get(typeId);
264
+
265
+ if (!typeDef) {
266
+ return {
267
+ success: false,
268
+ error: UploadistaError.fromCode("VALIDATION_ERROR", {
269
+ body: `Node type "${typeId}" is not registered`,
270
+ details: { typeId },
271
+ }),
272
+ };
273
+ }
274
+
275
+ try {
276
+ const parsed = typeDef.schema.parse(data);
277
+ return { success: true, data: parsed as T };
278
+ } catch (error) {
279
+ return {
280
+ success: false,
281
+ error: UploadistaError.fromCode("VALIDATION_ERROR", {
282
+ body: `Data validation failed for type "${typeId}"`,
283
+ cause: error,
284
+ details: { typeId, validationErrors: error },
285
+ }),
286
+ };
287
+ }
288
+ }
289
+
290
+ /**
291
+ * Check if a type is registered.
292
+ *
293
+ * @param id - The unique type identifier to check
294
+ * @returns True if the type is registered, false otherwise
295
+ *
296
+ * @example
297
+ * ```typescript
298
+ * if (flowTypeRegistry.has("custom-output-v1")) {
299
+ * console.log("Custom output type is available");
300
+ * }
301
+ * ```
302
+ */
303
+ has(id: string): boolean {
304
+ return this.types.has(id);
305
+ }
306
+
307
+ /**
308
+ * Get the total number of registered types.
309
+ *
310
+ * @returns The count of registered types
311
+ *
312
+ * @example
313
+ * ```typescript
314
+ * console.log(`Registry contains ${flowTypeRegistry.size()} types`);
315
+ * ```
316
+ */
317
+ size(): number {
318
+ return this.types.size;
319
+ }
320
+ }
321
+
322
+ /**
323
+ * Global singleton instance of the flow type registry.
324
+ *
325
+ * Use this instance to register and access node type definitions throughout
326
+ * your application. The registry is initialized once and shared globally.
327
+ *
328
+ * @example
329
+ * ```typescript
330
+ * import { flowTypeRegistry } from "@uploadista/core/flow";
331
+ *
332
+ * // Register a type
333
+ * flowTypeRegistry.register({
334
+ * id: "my-output-v1",
335
+ * category: "output",
336
+ * schema: mySchema,
337
+ * version: "1.0.0",
338
+ * description: "My custom output type",
339
+ * });
340
+ *
341
+ * // Validate data
342
+ * const result = flowTypeRegistry.validate("my-output-v1", data);
343
+ * ```
344
+ */
345
+ export const flowTypeRegistry = new FlowTypeRegistry();