Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
5 changes: 5 additions & 0 deletions src/client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export interface UploadOptions {
transformation?: CloudinaryTransformation;
publicId?: string;
userId?: string;
uploadPreset?: string;
}

export interface ListAssetsOptions {
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -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(),
Expand All @@ -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) => {
Expand Down
1 change: 1 addition & 0 deletions src/client/upload-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export interface DirectUploadOptions {
transformation?: CloudinaryTransformation;
publicId?: string;
userId?: string;
uploadPreset?: string;
}

// Re-export types for convenience
Expand Down
5 changes: 5 additions & 0 deletions src/component/apiUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -527,6 +527,7 @@ export interface CloudinaryUploadOptions {
publicId?: string;
userId?: string;
eager?: CloudinaryTransformation[];
uploadPreset?: string;
}

// Generate authentication signature for Cloudinary API
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -859,6 +861,7 @@ export async function generateDirectUploadCredentials(
transformation?: CloudinaryTransformation;
publicId?: string;
resourceType?: string;
uploadPreset?: string;
} = {}
): Promise<DirectUploadCredentials> {
const uploadUrl = `https://api.cloudinary.com/v1_1/${cloudName}/${options.resourceType || "image"}/upload`;
Expand All @@ -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(
Expand All @@ -903,6 +907,7 @@ export async function generateDirectUploadCredentials(
uploadParams.transformation = transformationString;
}
}
if (options.uploadPreset) uploadParams.upload_preset = options.uploadPreset;

return {
uploadUrl,
Expand Down
35 changes: 35 additions & 0 deletions src/component/lib.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions src/component/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down Expand Up @@ -1147,6 +1148,7 @@ export const upload = action({
tags: args.tags,
publicId: args.publicId,
userId: args.userId,
uploadPreset: args.uploadPreset,
};

if (args.transformation) {
Expand Down Expand Up @@ -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(),
Expand All @@ -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) => {
Expand Down Expand Up @@ -1319,6 +1323,7 @@ export const generateUploadCredentials = action({
tags: args.tags,
transformation: args.transformation,
publicId: args.publicId,
uploadPreset: args.uploadPreset,
}
);

Expand Down
1 change: 1 addition & 0 deletions src/react/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export interface CloudinaryAPI {
transformation?: CloudinaryTransformation;
publicId?: string;
userId?: string;
uploadPreset?: string;
},
UploadResult
>;
Expand Down