diff --git a/README.md b/README.md index b045e17..4868850 100644 --- a/README.md +++ b/README.md @@ -496,6 +496,21 @@ export const { } = makeCloudinaryAPI(components.cloudinary); ``` +### Upload Options + +All upload methods (`upload`, `generateUploadCredentials`, `uploadDirect`) accept these options: + +| Option | Type | Description | +| -------------- | ---------- | --------------------------------------------------------------------------- | +| `folder` | `string` | Cloudinary folder path (e.g., `"uploads/avatars"`) | +| `tags` | `string[]` | Tags for organization and filtering | +| `publicId` | `string` | Custom public ID (auto-generated if not provided) | +| `uploadPreset` | `string` | [Upload preset](https://cloudinary.com/documentation/upload_presets) name | +| `userId` | `string` | User ID for tracking ownership | +| `transformation` | `object` | Eager transformation applied during upload | + +**Upload Presets** allow you to define upload options centrally in Cloudinary (folder, transformations, moderation, etc.) and apply them by name. When using signed uploads, the preset must be configured as "signed" in Cloudinary. + ### React Implementation ```tsx @@ -521,6 +536,7 @@ function LargeFileUpload() { const credentials = await getCredentials({ folder: "large-uploads", tags: ["user-upload"], + uploadPreset: "my-preset", // Optional: apply a Cloudinary upload preset }); // Step 2: Upload directly to Cloudinary with progress tracking diff --git a/src/client/index.ts b/src/client/index.ts index ef9aff8..be6d00f 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -69,6 +69,7 @@ export interface UploadOptions { transformation?: CloudinaryTransformation; publicId?: string; userId?: string; + uploadPreset?: string; } export interface ListAssetsOptions { @@ -656,6 +657,7 @@ export class CloudinaryClient { transformation: v.optional(vTransformation), publicId: v.optional(v.string()), userId: v.optional(v.string()), + uploadPreset: v.optional(v.string()), }, returns: vUploadResult, handler: async (ctx, args) => { @@ -987,6 +989,7 @@ export function makeCloudinaryAPI( transformation: v.optional(vTransformation), publicId: v.optional(v.string()), userId: v.optional(v.string()), + uploadPreset: v.optional(v.string()), }, returns: vUploadResult, handler: async (ctx, args) => { @@ -1117,6 +1120,7 @@ export function makeCloudinaryAPI( transformation: v.optional(vTransformation), publicId: v.optional(v.string()), userId: v.optional(v.string()), + uploadPreset: v.optional(v.string()), }, returns: v.object({ uploadUrl: v.string(), @@ -1128,6 +1132,7 @@ export function makeCloudinaryAPI( tags: v.optional(v.string()), transformation: v.optional(v.string()), public_id: v.optional(v.string()), + upload_preset: v.optional(v.string()), }), }), handler: async (ctx, args) => { diff --git a/src/client/upload-utils.ts b/src/client/upload-utils.ts index aedab65..1d626b4 100644 --- a/src/client/upload-utils.ts +++ b/src/client/upload-utils.ts @@ -22,6 +22,7 @@ export interface DirectUploadOptions { transformation?: CloudinaryTransformation; publicId?: string; userId?: string; + uploadPreset?: string; } // Re-export types for convenience diff --git a/src/component/apiUtils.ts b/src/component/apiUtils.ts index 0d5d1dc..439eb3b 100644 --- a/src/component/apiUtils.ts +++ b/src/component/apiUtils.ts @@ -527,6 +527,7 @@ export interface CloudinaryUploadOptions { publicId?: string; userId?: string; eager?: CloudinaryTransformation[]; + uploadPreset?: string; } // Generate authentication signature for Cloudinary API @@ -676,6 +677,7 @@ export async function uploadToCloudinary( if (options.eager && options.eager.length > 0) { uploadParams.eager = eagerToString(options.eager); } + if (options.uploadPreset) uploadParams.upload_preset = options.uploadPreset; // Generate signature const { signature, timestamp } = await generateSignature( @@ -859,6 +861,7 @@ export async function generateDirectUploadCredentials( transformation?: CloudinaryTransformation; publicId?: string; resourceType?: string; + uploadPreset?: string; } = {} ): Promise { const uploadUrl = `https://api.cloudinary.com/v1_1/${cloudName}/${options.resourceType || "image"}/upload`; @@ -877,6 +880,7 @@ export async function generateDirectUploadCredentials( paramsToSign.transformation = transformationString; } } + if (options.uploadPreset) paramsToSign.upload_preset = options.uploadPreset; // Generate signature (without api_key) const { signature, timestamp } = await generateDirectUploadSignature( @@ -903,6 +907,7 @@ export async function generateDirectUploadCredentials( uploadParams.transformation = transformationString; } } + if (options.uploadPreset) uploadParams.upload_preset = options.uploadPreset; return { uploadUrl, diff --git a/src/component/lib.test.ts b/src/component/lib.test.ts index bc0e268..85a7dda 100644 --- a/src/component/lib.test.ts +++ b/src/component/lib.test.ts @@ -198,6 +198,41 @@ describe("Cloudinary component lib", () => { } }); + // Test uploadPreset is accepted in upload action + test("should accept uploadPreset in upload action", async () => { + const t = convexTest(schema, modules); + + // uploadPreset should be accepted without validation errors + const result = await t.action(api.lib.upload, { + base64Data: mockImageBase64, + filename: "test.png", + uploadPreset: "my-preset", + config: mockConfig, + }); + + // The upload will fail due to mock credentials, but the error should NOT be about uploadPreset + if (!result.success && result.error) { + expect(result.error).not.toContain("uploadPreset"); + expect(result.error).not.toContain("upload_preset"); + } + }); + + // Test generateUploadCredentials includes uploadPreset + test("should include upload_preset in generated credentials", async () => { + const t = convexTest(schema, modules); + + const result = await t.action(api.lib.generateUploadCredentials, { + folder: "test-folder", + uploadPreset: "my-preset", + config: mockConfig, + }); + + expect(result.uploadUrl).toContain("test-cloud"); + expect(result.uploadParams.upload_preset).toBe("my-preset"); + expect(result.uploadParams.folder).toBe("test-folder"); + expect(result.uploadParams.signature).toBeDefined(); + }); + // Note: Integration tests that require real Cloudinary credentials are skipped. // These tests would need real credentials to run: // - should upload and store asset diff --git a/src/component/lib.ts b/src/component/lib.ts index 3fad113..ccf1b25 100644 --- a/src/component/lib.ts +++ b/src/component/lib.ts @@ -1099,6 +1099,7 @@ export const upload = action({ transformation: v.optional(vTransformation), publicId: v.optional(v.string()), userId: v.optional(v.string()), + uploadPreset: v.optional(v.string()), config: v.object({ cloudName: v.string(), apiKey: v.string(), @@ -1147,6 +1148,7 @@ export const upload = action({ tags: args.tags, publicId: args.publicId, userId: args.userId, + uploadPreset: args.uploadPreset, }; if (args.transformation) { @@ -1270,6 +1272,7 @@ export const generateUploadCredentials = action({ transformation: v.optional(vTransformation), publicId: v.optional(v.string()), userId: v.optional(v.string()), + uploadPreset: v.optional(v.string()), config: v.object({ cloudName: v.string(), apiKey: v.string(), @@ -1286,6 +1289,7 @@ export const generateUploadCredentials = action({ tags: v.optional(v.string()), transformation: v.optional(v.string()), public_id: v.optional(v.string()), + upload_preset: v.optional(v.string()), }), }), handler: async (ctx, args) => { @@ -1319,6 +1323,7 @@ export const generateUploadCredentials = action({ tags: args.tags, transformation: args.transformation, publicId: args.publicId, + uploadPreset: args.uploadPreset, } ); diff --git a/src/react/index.tsx b/src/react/index.tsx index 9eed0d2..9ad019b 100644 --- a/src/react/index.tsx +++ b/src/react/index.tsx @@ -54,6 +54,7 @@ export interface CloudinaryAPI { transformation?: CloudinaryTransformation; publicId?: string; userId?: string; + uploadPreset?: string; }, UploadResult >;