@sanvika/cloudinary 0.1.0

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 (2) hide show
  1. package/dist/index.js +340 -0
  2. package/package.json +50 -0
package/dist/index.js ADDED
@@ -0,0 +1,340 @@
1
+ "use client";
2
+
3
+ // src/cloudinaryCore.js
4
+ import { v2 as cloudinary } from "cloudinary";
5
+
6
+ // src/cloudinaryErrors.js
7
+ var CloudinaryError = class extends Error {
8
+ constructor(message, operation, details = {}, originalError = null) {
9
+ super(message);
10
+ this.name = "CloudinaryError";
11
+ this.operation = operation;
12
+ this.details = details;
13
+ this.originalError = originalError;
14
+ this.timestamp = (/* @__PURE__ */ new Date()).toISOString();
15
+ }
16
+ toJSON() {
17
+ var _a;
18
+ return {
19
+ name: this.name,
20
+ message: this.message,
21
+ operation: this.operation,
22
+ details: this.details,
23
+ originalError: (_a = this.originalError) == null ? void 0 : _a.message,
24
+ timestamp: this.timestamp
25
+ };
26
+ }
27
+ };
28
+ function isRetriableError(error) {
29
+ var _a;
30
+ const msg = ((_a = error == null ? void 0 : error.message) == null ? void 0 : _a.toLowerCase()) || "";
31
+ if (msg.includes("rate limit") || msg.includes("network") || msg.includes("timeout") || msg.includes("socket") || msg.includes("econnreset")) {
32
+ return true;
33
+ }
34
+ if (msg.includes("not found") || msg.includes("unauthorized") || msg.includes("invalid") || msg.includes("signature")) {
35
+ return false;
36
+ }
37
+ return true;
38
+ }
39
+ async function withRetry(fn, options = {}) {
40
+ const { maxAttempts = 3, initialDelay = 500, operationName = "cloudinary_op" } = options;
41
+ let attempts = 0;
42
+ let lastError = null;
43
+ while (attempts < maxAttempts) {
44
+ try {
45
+ attempts++;
46
+ return await fn();
47
+ } catch (error) {
48
+ lastError = error;
49
+ if (attempts === maxAttempts || !isRetriableError(error)) break;
50
+ const delay = initialDelay * Math.pow(2, attempts - 1);
51
+ await new Promise((resolve) => setTimeout(resolve, delay));
52
+ }
53
+ }
54
+ throw new CloudinaryError(
55
+ `${operationName} failed after ${attempts} attempts: ${lastError.message}`,
56
+ operationName,
57
+ { attempts, maxAttempts },
58
+ lastError
59
+ );
60
+ }
61
+
62
+ // src/cloudinaryUtils.js
63
+ function isCloudinaryUrl(url) {
64
+ return typeof url === "string" && (url.includes("res.cloudinary.com") || url.includes("cloudinary.com"));
65
+ }
66
+ function extractPublicId(url) {
67
+ if (!isCloudinaryUrl(url)) return null;
68
+ try {
69
+ const parts = url.split("/");
70
+ const uploadIdx = parts.indexOf("upload");
71
+ if (uploadIdx === -1) return null;
72
+ let publicIdPath = parts.slice(uploadIdx + 1).join("/");
73
+ const versionMatch = publicIdPath.match(/^v\d+\//);
74
+ if (versionMatch) {
75
+ publicIdPath = publicIdPath.substring(versionMatch[0].length);
76
+ }
77
+ const lastDot = publicIdPath.lastIndexOf(".");
78
+ if (lastDot !== -1) {
79
+ const ext = publicIdPath.substring(lastDot + 1);
80
+ if (/^[a-zA-Z0-9]{2,5}$/.test(ext)) {
81
+ publicIdPath = publicIdPath.substring(0, lastDot);
82
+ }
83
+ }
84
+ return publicIdPath || null;
85
+ } catch {
86
+ return null;
87
+ }
88
+ }
89
+ function validatePublicId(publicId) {
90
+ if (!publicId || typeof publicId !== "string") return false;
91
+ const parts = publicId.split("/");
92
+ return parts.length >= 2 && parts.every((p) => p.length > 0);
93
+ }
94
+ function getFolderPath(appName, subfolder) {
95
+ if (!appName) throw new Error("appName is required for getFolderPath");
96
+ const parts = [appName];
97
+ if (subfolder) {
98
+ const cleaned = subfolder.replace(/^\/+|\/+$/g, "").split("/").filter(Boolean).join("/");
99
+ if (cleaned) parts.push(cleaned);
100
+ }
101
+ return parts.join("/");
102
+ }
103
+ function getOptimizedUrl(cloudName, publicId, transforms = {}, resourceType = "image") {
104
+ const base = `https://res.cloudinary.com/${cloudName}/${resourceType}/upload`;
105
+ const parts = Object.entries(transforms).map(([k, v]) => `${k}_${v}`).join(",");
106
+ return parts ? `${base}/${parts}/${publicId}` : `${base}/${publicId}`;
107
+ }
108
+ var TRANSFORM_PRESETS = {
109
+ thumbnail: { w: 150, h: 150, c: "fill", g: "auto", q: "auto", f: "auto" },
110
+ adCard: { w: 300, h: 200, c: "fill", g: "auto", q: "auto", f: "auto" },
111
+ profilePicture: { w: 100, h: 100, c: "fill", g: "face", r: "max", q: "auto", f: "auto" },
112
+ adDetail: { w: 800, h: 600, c: "fill", q: "auto", f: "auto" },
113
+ responsive: { w: "auto", dpr: "auto", q: "auto", f: "auto" }
114
+ };
115
+
116
+ // src/cloudinaryCore.js
117
+ var _appName = null;
118
+ var _configured = false;
119
+ function configureSanvikaCloudinary({ appName, cloudName, apiKey, apiSecret } = {}) {
120
+ if (!appName) throw new Error("appName is required for configureSanvikaCloudinary");
121
+ const cn = cloudName || process.env.CLOUDINARY_CLOUD_NAME || process.env.CLOUDINARY_NAME;
122
+ const ak = apiKey || process.env.CLOUDINARY_API_KEY;
123
+ const as = apiSecret || process.env.CLOUDINARY_API_SECRET;
124
+ if (!cn || !ak || !as) {
125
+ throw new Error("Cloudinary credentials missing. Set CLOUDINARY_CLOUD_NAME, CLOUDINARY_API_KEY, CLOUDINARY_API_SECRET in env.");
126
+ }
127
+ cloudinary.config({ cloud_name: cn, api_key: ak, api_secret: as, secure: true });
128
+ _appName = appName;
129
+ _configured = true;
130
+ }
131
+ function ensureConfigured() {
132
+ if (!_configured) {
133
+ const envApp = process.env.SANVIKA_CLOUDINARY_APP_NAME;
134
+ if (envApp) {
135
+ configureSanvikaCloudinary({ appName: envApp });
136
+ } else {
137
+ throw new Error("Call configureSanvikaCloudinary({ appName }) before using SDK operations.");
138
+ }
139
+ }
140
+ }
141
+ function getAppName() {
142
+ ensureConfigured();
143
+ return _appName;
144
+ }
145
+ async function uploadImage(fileOrBuffer, options = {}) {
146
+ ensureConfigured();
147
+ const { folder, tags = [], transforms, publicId, overwrite = false } = options;
148
+ const uploadFolder = getFolderPath(_appName, folder);
149
+ const uploadOpts = {
150
+ folder: uploadFolder,
151
+ resource_type: "image",
152
+ overwrite,
153
+ tags: [_appName, ...tags]
154
+ };
155
+ if (publicId) uploadOpts.public_id = publicId;
156
+ if (transforms) uploadOpts.eager = transforms;
157
+ return withRetry(
158
+ () => {
159
+ if (Buffer.isBuffer(fileOrBuffer)) {
160
+ return new Promise((resolve, reject) => {
161
+ const stream = cloudinary.uploader.upload_stream(uploadOpts, (err, result) => {
162
+ if (err) return reject(err);
163
+ resolve(result);
164
+ });
165
+ stream.end(fileOrBuffer);
166
+ });
167
+ }
168
+ return cloudinary.uploader.upload(fileOrBuffer, uploadOpts);
169
+ },
170
+ { operationName: "uploadImage", maxAttempts: 3 }
171
+ );
172
+ }
173
+ async function uploadImages(files, options = {}) {
174
+ if (!Array.isArray(files) || files.length === 0) return [];
175
+ return Promise.all(files.map((file) => uploadImage(file, options)));
176
+ }
177
+ async function uploadRawFile(buffer, options = {}) {
178
+ ensureConfigured();
179
+ const { folder, filename, tags = [] } = options;
180
+ if (!buffer || !Buffer.isBuffer(buffer)) throw new Error("Buffer is required for uploadRawFile");
181
+ const uploadFolder = getFolderPath(_appName, folder);
182
+ const baseName = filename ? filename.replace(/\.[^.]+$/, "") : "file";
183
+ return withRetry(
184
+ () => new Promise((resolve, reject) => {
185
+ const stream = cloudinary.uploader.upload_stream(
186
+ {
187
+ resource_type: "raw",
188
+ folder: uploadFolder,
189
+ public_id: baseName,
190
+ overwrite: true,
191
+ tags: ["raw", _appName, ...tags]
192
+ },
193
+ (err, result) => {
194
+ if (err) return reject(err);
195
+ resolve(result);
196
+ }
197
+ );
198
+ stream.end(buffer);
199
+ }),
200
+ { operationName: "uploadRawFile", maxAttempts: 3 }
201
+ );
202
+ }
203
+ async function deleteImage(urlOrPublicId, options = {}) {
204
+ ensureConfigured();
205
+ const { resourceType = "image" } = options;
206
+ const publicId = isCloudinaryUrl(urlOrPublicId) ? extractPublicId(urlOrPublicId) : urlOrPublicId;
207
+ if (!publicId) throw new CloudinaryError("Cannot extract public ID", "deleteImage", { urlOrPublicId });
208
+ return withRetry(
209
+ () => cloudinary.uploader.destroy(publicId, { resource_type: resourceType, type: "upload" }),
210
+ { operationName: "deleteImage", maxAttempts: 2 }
211
+ );
212
+ }
213
+ async function deleteImages(urls, options = {}) {
214
+ ensureConfigured();
215
+ const { fast = false, resourceType = "image" } = options;
216
+ if (!Array.isArray(urls) || urls.length === 0) return { success: true, total: 0, successful: 0, failed: 0 };
217
+ const BATCH_SIZE = fast ? 15 : 10;
218
+ const DELAY = fast ? 100 : 500;
219
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
220
+ const publicIds = urls.map((u) => isCloudinaryUrl(u) ? extractPublicId(u) : u).filter(Boolean);
221
+ if (publicIds.length === 0) return { success: true, total: urls.length, successful: 0, failed: 0, invalidUrls: urls.length };
222
+ const allResults = [];
223
+ for (let i = 0; i < publicIds.length; i += BATCH_SIZE) {
224
+ const batch = publicIds.slice(i, i + BATCH_SIZE);
225
+ const batchResults = await Promise.allSettled(
226
+ batch.map(async (pid) => {
227
+ try {
228
+ const result = await cloudinary.uploader.destroy(pid, { resource_type: resourceType, type: "upload" });
229
+ return { publicId: pid, success: result.result === "ok" || result.result === "not found", result: result.result };
230
+ } catch (err) {
231
+ return { publicId: pid, success: false, error: err.message };
232
+ }
233
+ })
234
+ );
235
+ allResults.push(...batchResults.map((r) => r.status === "fulfilled" ? r.value : { publicId: "unknown", success: false, error: "Promise rejected" }));
236
+ if (i + BATCH_SIZE < publicIds.length) await sleep(DELAY);
237
+ }
238
+ const successful = allResults.filter((r) => r.success).length;
239
+ const failed = allResults.filter((r) => !r.success).length;
240
+ return { success: failed === 0, total: urls.length, processed: allResults.length, successful, failed };
241
+ }
242
+ async function pingCloudinary() {
243
+ ensureConfigured();
244
+ return cloudinary.api.ping();
245
+ }
246
+
247
+ // src/SanvikaCloudinaryProvider.jsx
248
+ import { createContext, useContext, useMemo } from "react";
249
+ import { jsx } from "react/jsx-runtime";
250
+ var SanvikaCloudinaryContext = createContext(null);
251
+ function SanvikaCloudinaryProvider({ appName, cloudName, uploadPreset = "ml_default", children }) {
252
+ const value = useMemo(
253
+ () => ({ appName, cloudName, uploadPreset }),
254
+ [appName, cloudName, uploadPreset]
255
+ );
256
+ return /* @__PURE__ */ jsx(SanvikaCloudinaryContext.Provider, { value, children });
257
+ }
258
+ function useSanvikaCloudinary() {
259
+ const ctx = useContext(SanvikaCloudinaryContext);
260
+ if (!ctx) {
261
+ throw new Error("useSanvikaCloudinary must be used within a SanvikaCloudinaryProvider");
262
+ }
263
+ return ctx;
264
+ }
265
+
266
+ // src/useCloudinaryUpload.js
267
+ import { useState, useCallback } from "react";
268
+ function useCloudinaryUpload(options = {}) {
269
+ const { folder, resourceType = "image", onProgress } = options;
270
+ const { appName, cloudName, uploadPreset } = useSanvikaCloudinary();
271
+ const [uploading, setUploading] = useState(false);
272
+ const [error, setError] = useState(null);
273
+ const reset = useCallback(() => {
274
+ setError(null);
275
+ setUploading(false);
276
+ }, []);
277
+ const upload = useCallback(
278
+ async (file) => {
279
+ setError(null);
280
+ setUploading(true);
281
+ try {
282
+ const folderPath = folder ? `${appName}/${folder}` : appName;
283
+ const formData = new FormData();
284
+ formData.append("file", file);
285
+ formData.append("upload_preset", uploadPreset);
286
+ formData.append("folder", folderPath);
287
+ const xhr = new XMLHttpRequest();
288
+ const url = `https://api.cloudinary.com/v1_1/${cloudName}/${resourceType}/upload`;
289
+ const result = await new Promise((resolve, reject) => {
290
+ xhr.open("POST", url);
291
+ if (onProgress) {
292
+ xhr.upload.onprogress = (e) => {
293
+ if (e.lengthComputable) onProgress(Math.round(e.loaded / e.total * 100));
294
+ };
295
+ }
296
+ xhr.onload = () => {
297
+ if (xhr.status >= 200 && xhr.status < 300) {
298
+ resolve(JSON.parse(xhr.responseText));
299
+ } else {
300
+ reject(new Error(xhr.responseText || "Upload failed"));
301
+ }
302
+ };
303
+ xhr.onerror = () => reject(new Error("Network error during upload"));
304
+ xhr.send(formData);
305
+ });
306
+ return result;
307
+ } catch (err) {
308
+ const msg = (err == null ? void 0 : err.message) || "Upload failed";
309
+ setError(msg);
310
+ throw err;
311
+ } finally {
312
+ setUploading(false);
313
+ }
314
+ },
315
+ [appName, cloudName, uploadPreset, folder, resourceType, onProgress]
316
+ );
317
+ return { upload, uploading, error, reset };
318
+ }
319
+ export {
320
+ CloudinaryError,
321
+ SanvikaCloudinaryProvider,
322
+ TRANSFORM_PRESETS,
323
+ configureSanvikaCloudinary,
324
+ deleteImage,
325
+ deleteImages,
326
+ extractPublicId,
327
+ getAppName,
328
+ getFolderPath,
329
+ getOptimizedUrl,
330
+ isCloudinaryUrl,
331
+ isRetriableError,
332
+ pingCloudinary,
333
+ uploadImage,
334
+ uploadImages,
335
+ uploadRawFile,
336
+ useCloudinaryUpload,
337
+ useSanvikaCloudinary,
338
+ validatePublicId,
339
+ withRetry
340
+ };
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@sanvika/cloudinary",
3
+ "version": "0.1.0",
4
+ "description": "Centralized Cloudinary SDK for the Sanvika ecosystem — upload, delete, transform, and manage media across 50+ projects",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
8
+ "exports": {
9
+ ".": "./dist/index.js"
10
+ },
11
+ "files": [
12
+ "dist"
13
+ ],
14
+ "scripts": {
15
+ "build": "tsup",
16
+ "prepublishOnly": "npm run build"
17
+ },
18
+ "devDependencies": {
19
+ "tsup": "^8.5.1"
20
+ },
21
+ "dependencies": {
22
+ "cloudinary": "^2.5.0"
23
+ },
24
+ "keywords": [
25
+ "cloudinary",
26
+ "sanvika",
27
+ "image-upload",
28
+ "media-management",
29
+ "cdn",
30
+ "ecosystem"
31
+ ],
32
+ "author": "Sanvika Production",
33
+ "license": "MIT",
34
+ "peerDependencies": {
35
+ "react": ">=18.0.0",
36
+ "react-dom": ">=18.0.0"
37
+ },
38
+ "peerDependenciesMeta": {
39
+ "react": { "optional": true },
40
+ "react-dom": { "optional": true }
41
+ },
42
+ "engines": {
43
+ "node": ">=18.0.0"
44
+ },
45
+ "repository": {
46
+ "type": "git",
47
+ "url": "https://github.com/sanvikaproduction/sanvika-cloudinary",
48
+ "directory": "packages/sanvika-cloudinary-sdk"
49
+ }
50
+ }