Skip to content

Commit 712f1d9

Browse files
committed
feat: Implement Blob node and associated components for file uploads
1 parent 0375077 commit 712f1d9

15 files changed

Lines changed: 597 additions & 18 deletions

File tree

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { NodeExecution, NodeType } from "@dafthunk/types";
2+
3+
import { ExecutableNode } from "../types";
4+
import { NodeContext } from "../types";
5+
6+
/**
7+
* Blob node implementation
8+
* This node provides a blob widget that allows users to upload any file type and outputs them as binary data.
9+
*/
10+
export class BlobNode extends ExecutableNode {
11+
public static readonly nodeType: NodeType = {
12+
id: "blob",
13+
name: "Blob",
14+
type: "blob",
15+
description: "A blob widget for uploading any file type",
16+
tags: ["Widget", "Blob", "Load", "File"],
17+
icon: "file",
18+
documentation:
19+
"This node provides a blob widget for uploading any file type and outputs them as blob references. It can accept any MIME type and connect to any specific blob type (image, audio, document, etc.).",
20+
inputs: [
21+
{
22+
name: "value",
23+
type: "blob",
24+
description: "Current file as a blob reference",
25+
hidden: true,
26+
},
27+
],
28+
outputs: [
29+
{
30+
name: "blob",
31+
type: "blob",
32+
description: "The uploaded file as a blob reference",
33+
},
34+
],
35+
};
36+
37+
async execute(context: NodeContext): Promise<NodeExecution> {
38+
try {
39+
const { value } = context.inputs;
40+
41+
// If no value is provided, fail
42+
if (!value) {
43+
return this.createErrorResult("No blob data provided");
44+
}
45+
46+
// Convert raw object to BlobValue
47+
return this.createSuccessResult({
48+
blob: value,
49+
});
50+
} catch (error) {
51+
return this.createErrorResult(
52+
error instanceof Error ? error.message : "Unknown error"
53+
);
54+
}
55+
}
56+
}

apps/api/src/nodes/blob/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from "./blob-node";

apps/api/src/nodes/cloudflare-node-registry.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import { ListGuildChannelsDiscordNode } from "./discord/list-guild-channels-disc
3838
import { ListUserGuildsDiscordNode } from "./discord/list-user-guilds-discord-node";
3939
import { SendDMDiscordNode } from "./discord/send-dm-discord-node";
4040
import { SendMessageDiscordNode } from "./discord/send-message-discord-node";
41+
import { BlobNode } from "./blob/blob-node";
4142
import { DocumentNode } from "./document/document-node";
4243
import { ToMarkdownNode } from "./document/to-markdown-node";
4344
import { ParseEmailNode } from "./email/parse-email-node";
@@ -517,6 +518,7 @@ export class CloudflareNodeRegistry extends BaseNodeRegistry {
517518

518519
this.registerImplementation(AudioRecorderNode);
519520
this.registerImplementation(ToMarkdownNode);
521+
this.registerImplementation(BlobNode);
520522
this.registerImplementation(DocumentNode);
521523
this.registerImplementation(HttpRequestNode);
522524

apps/api/src/nodes/parameter-mapper.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
import { ObjectStore } from "../stores/object-store";
77
import {
88
AudioParameter as NodeAudioParameter,
9+
BlobParameter as NodeBlobParameter,
910
BufferGeometryParameter as NodeBufferGeometryParameter,
1011
DocumentParameter as NodeDocumentParameter,
1112
GltfParameter as NodeGltfParameter,
@@ -14,6 +15,17 @@ import {
1415
} from "./types";
1516

1617
// Type guards for binary parameter types
18+
function isBlobParameter(value: unknown): value is NodeBlobParameter {
19+
return (
20+
!!value &&
21+
typeof value === "object" &&
22+
"data" in value &&
23+
"mimeType" in value &&
24+
value["data"] instanceof Uint8Array &&
25+
typeof value["mimeType"] === "string"
26+
);
27+
}
28+
1729
function isImageParameter(value: unknown): value is NodeImageParameter {
1830
return (
1931
!!value &&
@@ -142,6 +154,40 @@ const converters = {
142154
nodeToApi: typeValidatingNodeToApi("boolean"),
143155
apiToNode: typeValidatingApiToNode("boolean"),
144156
},
157+
blob: {
158+
nodeToApi: async (
159+
value: NodeParameterValue,
160+
objectStore: ObjectStore,
161+
organizationId: string,
162+
executionId?: string
163+
) => {
164+
if (!isBlobParameter(value)) return undefined;
165+
const blob = new Blob([value.data], { type: value.mimeType });
166+
const buffer = await blob.arrayBuffer();
167+
const data = new Uint8Array(buffer);
168+
return await objectStore.writeObject(
169+
data,
170+
blob.type,
171+
organizationId,
172+
executionId
173+
);
174+
},
175+
apiToNode: async (value: ApiParameterValue, objectStore: ObjectStore) => {
176+
if (
177+
!value ||
178+
typeof value !== "object" ||
179+
!("id" in value) ||
180+
!("mimeType" in value)
181+
)
182+
return undefined;
183+
const result = await objectStore.readObject(value as ObjectReference);
184+
if (!result) return undefined;
185+
return {
186+
data: result.data,
187+
mimeType: (value as ObjectReference).mimeType,
188+
} as NodeBlobParameter;
189+
},
190+
},
145191
image: {
146192
nodeToApi: async (
147193
value: NodeParameterValue,
@@ -525,6 +571,7 @@ export async function nodeToApiParameter(
525571
);
526572

527573
if (
574+
type === "blob" ||
528575
type === "image" ||
529576
type === "document" ||
530577
type === "audio" ||

apps/api/src/nodes/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ export type ParameterType =
4747
type: "boolean";
4848
value?: boolean;
4949
}
50+
| {
51+
type: "blob";
52+
value?: BlobParameter;
53+
}
5054
| {
5155
type: "image";
5256
value?: ImageParameter;

apps/api/src/utils/workflows.ts

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -116,12 +116,26 @@ export function validateTypeCompatibility(
116116
};
117117
}
118118

119-
if (sourceParam.type !== targetParam.type) {
120-
// Allow "any" type to be compatible with all other types
121-
if (sourceParam.type === "any" || targetParam.type === "any") {
122-
continue;
123-
}
124-
119+
// Define blob-compatible types
120+
const blobTypes = new Set([
121+
"image",
122+
"audio",
123+
"document",
124+
"buffergeometry",
125+
"gltf",
126+
]);
127+
128+
// Check type compatibility
129+
const exactMatch = sourceParam.type === targetParam.type;
130+
const anyTypeMatch =
131+
sourceParam.type === "any" || targetParam.type === "any";
132+
const blobCompatible =
133+
(sourceParam.type === "blob" && blobTypes.has(targetParam.type)) ||
134+
(targetParam.type === "blob" && blobTypes.has(sourceParam.type));
135+
136+
const typesMatch = exactMatch || anyTypeMatch || blobCompatible;
137+
138+
if (!typesMatch) {
125139
return {
126140
type: "TYPE_MISMATCH",
127141
message: `Type mismatch: ${sourceParam.type.toLowerCase().replace("value", "")} -> ${targetParam.type.toLowerCase().replace("value", "")}`,

0 commit comments

Comments
 (0)