@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.
- package/dist/{checksum-CtOagryS.mjs → checksum-BaO9w1gC.mjs} +2 -2
- package/dist/{checksum-CtOagryS.mjs.map → checksum-BaO9w1gC.mjs.map} +1 -1
- package/dist/{checksum-jmKtZ9W8.cjs → checksum-DXCv7Avr.cjs} +1 -1
- package/dist/errors/index.cjs +1 -1
- package/dist/errors/index.d.cts +1 -1
- package/dist/errors/index.d.mts +1 -1
- package/dist/errors/index.mjs +1 -1
- package/dist/flow/index.cjs +1 -1
- package/dist/flow/index.d.cts +5 -5
- package/dist/flow/index.d.mts +5 -5
- package/dist/flow/index.mjs +1 -1
- package/dist/flow-DhuIQwjv.mjs +2 -0
- package/dist/flow-DhuIQwjv.mjs.map +1 -0
- package/dist/flow-s_AlC4r5.cjs +1 -0
- package/dist/{index-Bi9YYid8.d.mts → index-3jSHmGwH.d.mts} +2 -2
- package/dist/{index-Bi9YYid8.d.mts.map → index-3jSHmGwH.d.mts.map} +1 -1
- package/dist/{index-4VDJDcWM.d.cts → index-5K4oXy67.d.cts} +822 -169
- package/dist/index-5K4oXy67.d.cts.map +1 -0
- package/dist/{index-RgOX4psL.d.mts → index-BB1v4Ynz.d.mts} +822 -169
- package/dist/index-BB1v4Ynz.d.mts.map +1 -0
- package/dist/{index-Cbf1OPLp.d.mts → index-Bu5i-gcV.d.mts} +2 -2
- package/dist/index-Bu5i-gcV.d.mts.map +1 -0
- package/dist/{index-De4wQJwR.d.cts → index-CHGBYDtr.d.cts} +2 -2
- package/dist/{index-De4wQJwR.d.cts.map → index-CHGBYDtr.d.cts.map} +1 -1
- package/dist/{index-qZ90PVNl.d.cts → index-T6MZvUlM.d.cts} +2 -2
- package/dist/{index-Cbf1OPLp.d.mts.map → index-T6MZvUlM.d.cts.map} +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +5 -5
- package/dist/index.d.mts +5 -5
- package/dist/index.mjs +1 -1
- package/dist/{stream-limiter-D9rrsvAT.cjs → stream-limiter-BcTJAjs-.cjs} +1 -1
- package/dist/{stream-limiter-D9KSAaoY.mjs → stream-limiter-D1-sVS5i.mjs} +2 -2
- package/dist/{stream-limiter-D9KSAaoY.mjs.map → stream-limiter-D1-sVS5i.mjs.map} +1 -1
- package/dist/streams/index.cjs +1 -1
- package/dist/streams/index.d.cts +2 -2
- package/dist/streams/index.d.mts +2 -2
- package/dist/streams/index.mjs +1 -1
- package/dist/testing/index.cjs +1 -1
- package/dist/testing/index.d.cts +4 -4
- package/dist/testing/index.d.mts +4 -4
- package/dist/testing/index.mjs +1 -1
- package/dist/types/index.cjs +1 -1
- package/dist/types/index.d.cts +4 -4
- package/dist/types/index.d.mts +4 -4
- package/dist/types/index.mjs +1 -1
- package/dist/types-B-EckCWW.cjs +1 -0
- package/dist/types-CO-R4pFG.mjs +2 -0
- package/dist/types-CO-R4pFG.mjs.map +1 -0
- package/dist/upload/index.cjs +1 -1
- package/dist/upload/index.d.cts +4 -4
- package/dist/upload/index.d.mts +4 -4
- package/dist/upload/index.mjs +1 -1
- package/dist/{upload-D-eiOIVG.cjs → upload-BwXGQQ26.cjs} +1 -1
- package/dist/upload-C_Ew1NMF.mjs +2 -0
- package/dist/{upload-Yj5lrtZo.mjs.map → upload-C_Ew1NMF.mjs.map} +1 -1
- package/dist/{uploadista-error-B-n8Kfyh.cjs → uploadista-error-Blmj3lpk.cjs} +5 -1
- package/dist/{uploadista-error-DUWw6OqS.d.mts → uploadista-error-Cpn3uBLO.d.mts} +2 -2
- package/dist/uploadista-error-Cpn3uBLO.d.mts.map +1 -0
- package/dist/{uploadista-error-BQLhNZcY.d.cts → uploadista-error-DgdQnozn.d.cts} +2 -2
- package/dist/uploadista-error-DgdQnozn.d.cts.map +1 -0
- package/dist/{uploadista-error-Buscq-FR.mjs → uploadista-error-DhNBioWq.mjs} +5 -1
- package/dist/uploadista-error-DhNBioWq.mjs.map +1 -0
- package/dist/utils/index.cjs +1 -1
- package/dist/utils/index.d.cts +2 -2
- package/dist/utils/index.d.mts +2 -2
- package/dist/utils/index.mjs +1 -1
- package/dist/{utils-BWiu6lqv.mjs → utils-7gziergl.mjs} +2 -2
- package/dist/{utils-BWiu6lqv.mjs.map → utils-7gziergl.mjs.map} +1 -1
- package/dist/{utils-_StwBtxT.cjs → utils-C_STf6Wl.cjs} +1 -1
- package/package.json +3 -3
- package/src/errors/uploadista-error.ts +21 -1
- package/src/flow/event.ts +28 -4
- package/src/flow/flow-server.ts +43 -12
- package/src/flow/flow.ts +92 -13
- package/src/flow/index.ts +7 -0
- package/src/flow/node-types/index.ts +85 -0
- package/src/flow/node.ts +48 -6
- package/src/flow/nodes/input-node.ts +2 -0
- package/src/flow/nodes/storage-node.ts +2 -0
- package/src/flow/type-guards.ts +293 -0
- package/src/flow/type-registry.ts +345 -0
- package/src/flow/types/flow-job.ts +22 -6
- package/src/flow/types/flow-types.ts +152 -3
- package/tests/flow/type-system.test.ts +799 -0
- package/dist/flow-ChADffZ5.cjs +0 -1
- package/dist/flow-_J9-Dm_m.mjs +0 -2
- package/dist/flow-_J9-Dm_m.mjs.map +0 -1
- package/dist/index-4VDJDcWM.d.cts.map +0 -1
- package/dist/index-RgOX4psL.d.mts.map +0 -1
- package/dist/index-qZ90PVNl.d.cts.map +0 -1
- package/dist/types-BI_KmpTc.mjs +0 -2
- package/dist/types-BI_KmpTc.mjs.map +0 -1
- package/dist/types-f08UsX4E.cjs +0 -1
- package/dist/upload-Yj5lrtZo.mjs +0 -2
- package/dist/uploadista-error-BQLhNZcY.d.cts.map +0 -1
- package/dist/uploadista-error-Buscq-FR.mjs.map +0 -1
- 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();
|