UNPKG

magically-sdk

Version:

Official SDK for Magically - Build mobile apps with AI

278 lines (277 loc) 11.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.MagicallyFiles = void 0; const Logger_1 = require("./Logger"); const APIClient_1 = require("./APIClient"); class MagicallyFiles { constructor(config, auth) { this.config = config; this.auth = auth; this.logger = new Logger_1.Logger(config.debug || false, 'MagicallyFiles'); this.apiClient = new APIClient_1.APIClient(config, 'MagicallyFiles'); } /** * Convert a URI (from camera, image picker, etc.) to a proper File object * * This is the CRITICAL step that LLMs often miss. All Expo camera/picker results * give you URIs, but the upload API needs proper File objects. * * Steps performed: * 1. Fetch the URI to get the actual binary data * 2. Convert response to blob * 3. Create proper File object with name and MIME type * * @param uri - URI from camera/picker (asset.uri or photo.uri) * @param fileName - Name for the file (include extension!) * @param mimeType - MIME type (e.g., 'image/jpeg', 'image/png') * @returns Proper File object ready for upload * * @example * // From camera * const photo = await cameraRef.current.takePictureAsync(); * const file = await magically.files.convertUriToFile( * photo.uri, * 'photo.jpg', * 'image/jpeg' * ); * * @example * // From image picker * const result = await ImagePicker.launchImageLibraryAsync(); * const asset = result.assets[0]; * const file = await magically.files.convertUriToFile( * asset.uri, * asset.fileName || 'image.jpg', * asset.mimeType || 'image/jpeg' * ); */ async convertUriToFile(uri, fileName, mimeType = 'image/jpeg') { try { this.logger.debug('Converting URI to File', { uri, fileName, mimeType }); // Step 1: Fetch the URI to get binary data const response = await fetch(uri); if (!response.ok) { throw new Error(`Failed to fetch URI: ${response.statusText}`); } // Step 2: Convert to blob and validate const blob = await response.blob(); // Validate blob has content if (blob.size === 0) { throw new Error('File is empty or could not be read from URI'); } this.logger.debug('Blob created from URI', { blobSize: blob.size, blobType: blob.type }); // Step 3: Create proper File object const file = new File([blob], fileName, { type: mimeType }); // Validate File object was created properly if (file.size === 0) { throw new Error('Failed to create valid File object - size is 0'); } if (file.size !== blob.size) { this.logger.warn('File size mismatch after creation', { blobSize: blob.size, fileSize: file.size }); } this.logger.debug('URI conversion successful', { originalUri: uri, fileName: file.name, fileSize: file.size, fileType: file.type, conversionValid: file.size > 0 }); return file; } catch (error) { throw new Error(`Failed to convert URI to File: ${error instanceof Error ? error.message : 'Unknown error'}`); } } /** * Upload a file to Vercel Blob storage and save metadata to MongoDB * @param file - File object to upload (from file input or camera) * @param options - Upload options (tags, metadata) * @returns Uploaded file metadata */ async upload(file, options) { try { this.logger.debug('Starting file upload', { fileName: file instanceof File ? file.name : 'blob', fileSize: file.size, fileType: file.type, options }); const token = await this.auth.getValidToken(); // Convert to blob for raw upload (like Convex pattern) const blob = file instanceof File ? new Blob([file], { type: file.type }) : file; const fileName = file instanceof File ? file.name : 'blob'; // Build query parameters for metadata const params = new URLSearchParams(); params.set('fileName', fileName); params.set('fileSize', file.size.toString()); params.set('mimeType', file.type); if (options?.tags) { params.set('tags', JSON.stringify(options.tags)); } if (options?.metadata) { params.set('metadata', JSON.stringify(options.metadata)); } // Use APIClient for request - special handling for blob upload const response = await fetch(`${this.config.apiUrl || 'https://trymagically.com'}/api/project/${this.config.projectId}/data/files?${params.toString()}`, { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': file.type, 'Content-Length': file.size.toString(), }, body: blob, }); // Log the request const requestId = this.logger.networkRequest('POST', `/api/project/${this.config.projectId}/data/files`, { headers: { 'Content-Type': file.type, 'Content-Length': file.size.toString() }, body: { fileName, fileSize: file.size, mimeType: file.type, ...options }, operation: 'upload:file' }); const startTime = Date.now(); const responseData = await response.json(); const duration = Date.now() - startTime; if (!response.ok) { this.logger.networkError(requestId, responseData, { duration, operation: 'upload:file' }); throw new Error(responseData.error_description || `File upload failed: ${response.statusText}`); } this.logger.networkResponse(requestId, { status: response.status, statusText: response.statusText, duration, data: responseData, operation: 'upload:file' }); this.logger.success('File upload completed', { fileId: responseData.file._id, fileName: responseData.file.originalName, fileSize: responseData.file.size, url: responseData.file.url }); return responseData.file; } catch (error) { throw error; } } /** * List uploaded files with filtering and pagination * @param options - List options (limit, skip, tags, mimeType) * @returns List of files with pagination info */ async list(options) { try { this.logger.debug('Listing files', { options }); const token = await this.auth.getValidToken(); // Build query parameters const params = new URLSearchParams(); if (options?.limit) params.append('limit', options.limit.toString()); if (options?.skip) params.append('skip', options.skip.toString()); if (options?.tags) params.append('tags', options.tags.join(',')); if (options?.mimeType) params.append('mimeType', options.mimeType); const endpoint = `/api/project/${this.config.projectId}/data/files${params.toString() ? `?${params.toString()}` : ''}`; const result = await this.apiClient.request(endpoint, { method: 'GET', operation: 'list:files' }, token); this.logger.success('Files listed', { count: result.data.length, total: result.total, limit: result.limit, skip: result.skip }); return result; } catch (error) { throw error; } } /** * Delete a file from both Vercel Blob and MongoDB * @param fileId - MongoDB document ID of the file to delete * @returns Success confirmation */ async delete(fileId) { try { this.logger.debug('Deleting file', { fileId }); const token = await this.auth.getValidToken(); const result = await this.apiClient.request(`/api/project/${this.config.projectId}/data/files?fileId=${fileId}`, { method: 'DELETE', operation: `delete:file:${fileId}` }, token); this.logger.success('File deleted', { fileId }); return result; } catch (error) { throw error; } } /** * Upload multiple files in parallel * @param files - Array of files to upload * @param options - Upload options applied to all files * @returns Array of uploaded file metadata */ async uploadMultiple(files, options) { try { this.logger.debug('Starting multiple file upload', { fileCount: files.length }); const uploadPromises = files.map(file => this.upload(file, options)); const results = await Promise.all(uploadPromises); this.logger.success('Multiple file upload completed', { uploadedCount: results.length, totalSize: results.reduce((sum, file) => sum + file.size, 0) }); return results; } catch (error) { throw error; } } /** * Get files by tags * @param tags - Array of tags to filter by * @param options - Additional list options * @returns Files matching the tags */ async getByTags(tags, options) { return this.list({ ...options, tags }); } /** * Get files by MIME type * @param mimeType - MIME type to filter by (supports partial matching) * @param options - Additional list options * @returns Files matching the MIME type */ async getByMimeType(mimeType, options) { return this.list({ ...options, mimeType }); } /** * Get image files only * @param options - List options * @returns Image files only */ async getImages(options) { return this.getByMimeType('image/', options); } /** * Get document files only (PDF, DOC, etc.) * @param options - List options * @returns Document files only */ async getDocuments(options) { const documentTypes = ['application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument']; // For multiple MIME types, we'll need to make separate calls or enhance the API // For now, let's just get PDFs as an example return this.getByMimeType('application/pdf', options); } } exports.MagicallyFiles = MagicallyFiles;