npm package: @kodeme-io/next-core-storage
Description: S3-compatible storage client for Next.js + Odoo applications
Author: ABC Food Development Team
Latest version: 0.8.4

# @next-core/storage

S3-compatible storage client and photo compression utilities for Next.js + Odoo applications.

## Features

- ✅ **S3 Upload** - Upload files to any S3-compatible storage
- ✅ **Photo Compression** - Reduce image file sizes for mobile apps
- ✅ **Presigned URLs** - Generate temporary access URLs
- ✅ **TypeScript** - Full type safety
- ✅ **Flexible** - Works with AWS S3, MinIO, Hetzner, etc.

## Installation

```bash
pnpm add @next-core/storage @aws-sdk/client-s3 @aws-sdk/lib-storage @aws-sdk/s3-request-presigner
```

## Environment Variables

Add to your `.env.local`:

```env
S3_ENDPOINT_URL=https://your-s3-endpoint.com
S3_ACCESS_KEY=your_access_key
S3_SECRET_KEY=your_secret_key
S3_BUCKET_NAME=your-bucket-name
S3_REGION=us-east-1
```

## Quick Start

### 1. Create S3 Client

```typescript
import { createS3Client } from '@next-core/storage'

const s3 = createS3Client({
  endpoint: process.env.S3_ENDPOINT_URL!,
  accessKeyId: process.env.S3_ACCESS_KEY!,
  secretAccessKey: process.env.S3_SECRET_KEY!,
  bucket: process.env.S3_BUCKET_NAME!,
  region: 'us-east-1',
})
```

### 2. Upload Files

```typescript
import { uploadToS3, uploadVisitPhoto } from '@next-core/storage'

// Upload generic file
const result = await uploadToS3(
  s3,
  'my-bucket',
  fileBuffer,
  'document.pdf',
  'application/pdf',
  'documents' // folder
)

// Upload visit photo (organized by visit ID)
const photo = await uploadVisitPhoto(
  s3,
  'my-bucket',
  base64Image,
  123, // visitId
  'checkin',
  process.env.S3_ENDPOINT_URL
)

console.log(photo.url) // https://endpoint.com/bucket/visits/123/photos/visit_123_checkin_2025-10-06.jpg
```

### 3. Compress Photos

```typescript
import { compressPhoto } from '@next-core/storage'

// Compress image to max 500KB
const compressed = await compressPhoto(
  base64Image,
  500, // maxSizeKB
  1920, // maxWidth
  1080 // maxHeight
)

// File size reduced from 3MB to 450KB!
```

## API Reference

### S3 Client Functions

#### `createS3Client(config)`

Create S3 client instance.

```typescript
const client = createS3Client({
  endpoint: 'https://s3.amazonaws.com',
  accessKeyId: 'YOUR_KEY',
  secretAccessKey: 'YOUR_SECRET',
  bucket: 'my-bucket',
  region: 'us-east-1',
  forcePathStyle: true, // Required for some S3-compatible services
})
```

#### `uploadToS3(client, bucket, fileBuffer, fileName, contentType, folder?, endpoint?)`

Upload a file to S3.

```typescript
const { key, url } = await uploadToS3(
  client,
  'my-bucket',
  fileBuffer,
  'photo.jpg',
  'image/jpeg',
  'photos',
  'https://endpoint.com'
)
```

**Parameters:**
- `client` - S3 client instance
- `bucket` - Bucket name
- `fileBuffer` - Buffer or base64 string
- `fileName` - File name
- `contentType` - MIME type
- `folder` - Optional folder path (default: 'attachments')
- `endpoint` - Optional endpoint for constructing URL

**Returns:** `{ key: string, url: string }`

#### `uploadVisitPhoto(client, bucket, base64Data, visitId, photoType, endpoint?)`

Upload a visit photo with organized folder structure.

```typescript
const { key, url, fileName } = await uploadVisitPhoto(
  client,
  'my-bucket',
  'data:image/jpeg;base64,...',
  123,
  'checkin',
  'https://endpoint.com'
)
// Uploads to: visits/123/photos/visit_123_checkin_2025-10-06.jpg
```

#### `uploadAttachment(client, bucket, fileBuffer, fileName, contentType, model, recordId, endpoint?)`

Upload an attachment organized by Odoo model.

```typescript
const { key, url } = await uploadAttachment(
  client,
  'my-bucket',
  buffer,
  'invoice.pdf',
  'application/pdf',
  'account.move',
  456,
  'https://endpoint.com'
)
// Uploads to: attachments/account.move/456/invoice.pdf
```

#### `getPresignedUrl(client, bucket, key, expiresIn?)`

Generate temporary access URL for private files.

```typescript
const url = await getPresignedUrl(
  client,
  'my-bucket',
  'private/document.pdf',
  3600 // expires in 1 hour
)
```

#### `deleteFromS3(client, bucket, key)`

Delete a file from S3.

```typescript
await deleteFromS3(client, 'my-bucket', 'photos/old-photo.jpg')
```

#### `validateS3Config(config)`

Validate S3 configuration.

```typescript
const isValid = validateS3Config({
  endpoint: process.env.S3_ENDPOINT_URL,
  accessKeyId: process.env.S3_ACCESS_KEY,
  secretAccessKey: process.env.S3_SECRET_KEY,
  bucket: process.env.S3_BUCKET_NAME,
})
```

### Photo Compression Functions

#### `compressPhoto(base64, maxSizeKB?, maxWidth?, maxHeight?)`

Compress a single photo.

```typescript
const compressed = await compressPhoto(
  'data:image/jpeg;base64,...',
  500, // max 500KB
  1920, // max 1920px width
  1080 // max 1080px height
)
```

#### `compressPhotoBatch(base64Images, maxSizeKB?)`

Compress multiple photos in parallel.

```typescript
const compressed = await compressPhotoBatch(
  [image1, image2, image3],
  300 // max 300KB each
)
```

#### `fileToBase64(file)`

Convert File object to base64.

```typescript
const base64 = await fileToBase64(fileInput.files[0])
```

#### `base64ToBlob(base64, contentType?)`

Convert base64 to Blob.

```typescript
const blob = base64ToBlob('data:image/jpeg;base64,...', 'image/jpeg')
```

#### `blobToBase64(blob)`

Convert Blob to base64.

```typescript
const base64 = await blobToBase64(blob)
```

#### `checkStorageQuota()`

Check browser storage quota.

```typescript
const { usedMB, availableMB, percentUsed } = await checkStorageQuota()
console.log(`Using ${usedMB.toFixed(2)}MB of ${availableMB.toFixed(2)}MB (${percentUsed.toFixed(1)}%)`)
```

## Common Use Cases

### Upload Photo from Camera

```typescript
import { compressPhoto, uploadVisitPhoto, createS3Client } from '@next-core/storage'

const s3 = createS3Client({...})

async function handlePhotoCapture(base64Image: string, visitId: number) {
  // 1. Compress photo (3MB → 400KB)
  const compressed = await compressPhoto(base64Image, 500)

  // 2. Upload to S3
  const result = await uploadVisitPhoto(
    s3,
    process.env.S3_BUCKET_NAME!,
    compressed,
    visitId,
    'store_front',
    process.env.S3_ENDPOINT_URL
  )

  console.log('Photo uploaded:', result.url)
  return result
}
```

### Upload File Attachment

```typescript
import { fileToBase64, uploadAttachment } from '@next-core/storage'

async function handleFileUpload(file: File) {
  // 1. Convert to base64
  const base64 = await fileToBase64(file)

  // 2. Upload to S3
  const result = await uploadAttachment(
    s3,
    bucket,
    base64,
    file.name,
    file.type,
    'sfa.visit',
    123,
    endpoint
  )

  return result.url
}
```

### Batch Photo Upload

```typescript
import { compressPhotoBatch, uploadToS3 } from '@next-core/storage'

async function uploadMultiplePhotos(photos: string[]) {
  // 1. Compress all photos in parallel
  const compressed = await compressPhotoBatch(photos, 300)

  // 2. Upload all compressed photos
  const uploads = compressed.map((photo, i) =>
    uploadToS3(s3, bucket, photo, `photo-${i}.jpg`, 'image/jpeg', 'batch')
  )

  const results = await Promise.all(uploads)
  return results
}
```

## TypeScript Types

```typescript
import type {
  S3ClientConfig,
  UploadResult,
  VisitPhotoUploadResult,
  PhotoCompressionOptions,
  StorageQuota,
} from '@next-core/storage'
```

## Best Practices

### 1. Always Compress Photos

Mobile photos can be 3-5MB. Always compress before upload:

```typescript
// ❌ Bad: Upload 3MB photo
await uploadToS3(s3, bucket, rawPhoto, ...)

// ✅ Good: Compress to 400KB first
const compressed = await compressPhoto(rawPhoto, 500)
await uploadToS3(s3, bucket, compressed, ...)
```

### 2. Use Organized Folder Structure

```typescript
// ✅ Good: Organized by purpose and ID
visits/123/photos/checkin.jpg
attachments/res.partner/456/contract.pdf
documents/2025/10/invoice-001.pdf
```

### 3. Validate Config on Startup

```typescript
// In your app initialization
if (!validateS3Config({
  endpoint: process.env.S3_ENDPOINT_URL,
  accessKeyId: process.env.S3_ACCESS_KEY,
  secretAccessKey: process.env.S3_SECRET_KEY,
  bucket: process.env.S3_BUCKET_NAME,
})) {
  throw new Error('Invalid S3 configuration')
}
```

### 4. Handle Errors Gracefully

```typescript
try {
  const result = await uploadToS3(...)
  return result
} catch (error) {
  console.error('Upload failed:', error)
  // Fallback: Store locally for retry
  await storeInLocalStorage(photo)
  throw error
}
```

## S3-Compatible Services

This package works with:

- ✅ **AWS S3** - Amazon's object storage
- ✅ **MinIO** - Self-hosted S3-compatible storage
- ✅ **Hetzner Storage Box** - German provider
- ✅ **DigitalOcean Spaces** - DO's object storage
- ✅ **Wasabi** - Hot cloud storage
- ✅ **Backblaze B2** - Cloud storage with S3 API

## License

MIT

## Related Packages

- [@next-core/ui](../ui) - UI components and themes
- [@next-core/auth](../auth) - Authentication
- [@next-core/odoo-api](../odoo-api) - Odoo integration

---

**Built with ❤️ by ABC Food Development Team**