<div align="center">
  <img src="https://cdn.akadenia.com/images/akadenia-webp/logo/horizontal-logo.svg" alt="Akadenia" width="200" />
  
  <h2><code>@akadenia/azure-storage</code></h2>
  
  <img src="https://cdn.akadenia.com/images/badges/npm-version-azure-storage.svg" alt="npm version" />
  <img src="https://cdn.akadenia.com/images/badges/license-mit.svg" alt="License: MIT" />
  <img src="https://cdn.akadenia.com/images/badges/typescript.svg" alt="TypeScript" />
  
  A TypeScript library that wraps Azure Blob, Table, and Queue Storage with a clean, consistent API. Supports both connection strings and Managed Identity — so it works the same in local dev and production Azure environments.

  [Documentation](https://akadenia.com/packages/akadenia-azure-storage) · [GitHub](https://github.com/akadenia/AkadeniaAzureStorage) · [Issues](https://github.com/akadenia/AkadeniaAzureStorage/issues)
</div>

## Features

- **Blob Storage**: Upload, download, list, and manage blobs with SAS URL support
- **Table Storage**: Complete CRUD operations for Azure Table Storage
- **Queue Storage**: Send, receive, and manage queue messages
- **Managed Identity Support**: Passwordless authentication using Azure Managed Identity (system-assigned and user-assigned)
- **SAS URL Support**: Generate Shared Access Signature URLs — User Delegation SAS with Managed Identity, Service SAS with connection strings
- **TypeScript Support**: Full type definitions included
- **Error Handling**: Built-in error handling and validation

## Table of Contents

- [Installation](#installation)
- [Quick Start](#quick-start)
- [Blob Storage](#blob-storage)
- [Table Storage](#table-storage)
- [Queue Storage](#queue-storage)
- [Configuration](#configuration)
- [Error Handling](#error-handling)
- [Best Practices](#best-practices)
- [API Reference](#api-reference)
- [Contributing](#contributing)
- [License](#license)

## Installation

```bash
npm install @akadenia/azure-storage --save
```

## Quick Start

### Prerequisites

You can authenticate using either:
- **Connection String**: Get this from the Azure Portal under your storage account's "Access keys" section
- **Managed Identity**: For applications running in Azure (recommended for production)

**Option 1: Connection string**

```typescript
import { BlobStorage, TableStorage, QueueStorage } from '@akadenia/azure-storage';

const connectionString = "DefaultEndpointsProtocol=https;AccountName=yourstorageaccount;AccountKey=yourkey;EndpointSuffix=core.windows.net";

const blobStorage  = new BlobStorage(connectionString);
const tableStorage = new TableStorage(connectionString, 'tableName');
const queueStorage = new QueueStorage(connectionString);
```

**Option 2: Managed Identity (recommended for production)**

```typescript
import { BlobStorage, TableStorage, QueueStorage } from '@akadenia/azure-storage';

const blobStorage  = new BlobStorage({ accountName: 'yourstorageaccount' });
const tableStorage = new TableStorage({ accountName: 'yourstorageaccount', tableName: 'tableName' });
const queueStorage = new QueueStorage({ accountName: 'yourstorageaccount' });
```

---

## Blob Storage

The `BlobStorage` class provides methods to interact with Azure Blob Storage.

### Basic Setup

```typescript
import { BlobStorage } from '@akadenia/azure-storage';

const blobStorage = new BlobStorage(connectionString);
```

### Container Operations

```typescript
// Create a container
const containerCreated = await blobStorage.createContainer('my-container');
console.log('Container created:', containerCreated);

// Delete a container
const containerDeleted = await blobStorage.deleteContainer('my-container');
console.log('Container deleted:', containerDeleted);
```

### Upload Operations

```typescript
// Upload data from Buffer
const data = Buffer.from('Hello, Azure Blob Storage!');
const uploaded = await blobStorage.uploadData('my-container', 'my-blob.txt', data, {
  blobContentType: 'text/plain'
});

// Upload from stream
import { Readable } from 'stream';
const stream = Readable.from(['Stream data content']);
await blobStorage.uploadStream('my-container', 'stream-blob.txt', stream, {
  blobContentType: 'text/plain'
});

// Upload with additional headers
await blobStorage.uploadData('my-container', 'document.pdf', pdfBuffer, {
  blobContentType: 'application/pdf',
  blobCacheControl: 'max-age=3600',
  blobContentEncoding: 'gzip'
});

// Legacy upload method
await blobStorage.upload('my-container', 'file.pdf', buffer, buffer.length, 'application/pdf');
```

### Download Operations

```typescript
// Download blob as Buffer
const blobData = await blobStorage.downloadBlob('my-container', 'my-blob.txt');
console.log('Downloaded content:', blobData.toString());

// Check if blob exists before downloading
const exists = await blobStorage.blobExists('my-container', 'my-blob.txt');
if (exists) {
  const data = await blobStorage.downloadBlob('my-container', 'my-blob.txt');
}
```

### List Operations

```typescript
// List all blobs with a prefix
const blobs = await blobStorage.listBlobs('my-container', 'documents/');
blobs.forEach(blob => {
  console.log(`Blob: ${blob.name}, Size: ${blob.properties.contentLength}`);
});
```

### Delete Operations

```typescript
const deleted = await blobStorage.deleteBlob('my-container', 'my-blob.txt');
console.log('Blob deleted:', deleted);
```

### SAS URL Generation

`generateSASUrl` supports two SAS token types automatically:
- **User Delegation SAS** — used with Managed Identity (more secure, no account keys)
- **Service SAS** — used with connection string authentication

```typescript
import { BlobPermissions } from '@akadenia/azure-storage';

// Read-only SAS, 1 hour expiry
const sasUrl = await blobStorage.generateSASUrl('my-container', 'my-blob.txt', {
  startsOn: new Date(),
  expiresOn: new Date(Date.now() + 3600 * 1000),
  permissions: [BlobPermissions.READ]
});
console.log('SAS URL:', sasUrl.fullUrlWithSAS);

// Write-enabled container-level SAS
const containerSas = await blobStorage.generateSASUrl('my-container', undefined, {
  permissions: [BlobPermissions.ADD, BlobPermissions.WRITE],
  expiresOn: new Date(Date.now() + 24 * 3600 * 1000)
});
```

#### SAS Permissions

```typescript
import { BlobPermissions } from '@akadenia/azure-storage';

BlobPermissions.READ    // "r"
BlobPermissions.WRITE   // "w"
BlobPermissions.CREATE  // "c"
BlobPermissions.DELETE  // "d"
BlobPermissions.ADD     // "a"
```

---

## Table Storage

The `TableStorage` class provides methods to interact with Azure Table Storage.

### Basic Setup

```typescript
import { TableStorage, ITableEntity } from '@akadenia/azure-storage';

const tableStorage = new TableStorage(connectionString, 'MyTable');
```

### Table Management

```typescript
const tableCreated = await tableStorage.createTable();
const tableDeleted = await tableStorage.deleteTable();
```

### Entity Operations

```typescript
interface UserEntity extends ITableEntity {
  partitionKey: string;
  rowKey: string;
  name: string;
  email: string;
  age: number;
  isActive: boolean;
}

// Insert
const user: UserEntity = {
  partitionKey: 'users',
  rowKey: 'user-123',
  name: 'John Doe',
  email: 'john@example.com',
  age: 30,
  isActive: true
};
await tableStorage.insert(user);

// Get
const retrievedUser = await tableStorage.get('users', 'user-123');

// Update
user.age = 31;
await tableStorage.update(user);

// Upsert (insert or update)
await tableStorage.upsert(newUser);

// Delete
await tableStorage.delete('users', 'user-123');
```

### List Operations

```typescript
// List all entities
const allUsers = await tableStorage.list<UserEntity>();

// List with filter
const activeUsers = await tableStorage.list<UserEntity>({
  queryOptions: { filter: "isActive eq true and age gt 25" }
});

// Access underlying TableClient for advanced operations
const tableClient = tableStorage.getTableClient();
```

---

## Queue Storage

The `QueueStorage` class provides methods to interact with Azure Queue Storage.

### Basic Setup

```typescript
import { QueueStorage } from '@akadenia/azure-storage';

const queueStorage = new QueueStorage(connectionString);
// or with Managed Identity:
const queueStorage = new QueueStorage({ accountName: 'yourstorageaccount' });
```

### Message Operations

```typescript
// Send a string message (base64 encoded by default)
await queueStorage.sendMessage('my-queue', 'Hello, Queue!');

// Send an object (auto JSON stringified + base64 encoded)
await queueStorage.sendMessage('my-queue', { orderNumber: '12345', action: 'process' });

// Send without base64 encoding
await queueStorage.sendMessage('my-queue', 'plain text', false);

// Receive messages
const { receivedMessageItems } = await queueStorage.receiveMessages('my-queue', 5, 30);

// Process and delete
for (const message of receivedMessageItems) {
  const decoded = Buffer.from(message.messageText, 'base64').toString('utf-8');
  const data = JSON.parse(decoded);
  // ... process data ...
  await queueStorage.deleteMessage('my-queue', message.messageId, message.popReceipt);
}

// Peek without consuming
const { peekedMessageItems } = await queueStorage.peekMessages('my-queue', 5);
```

### Queue Management

```typescript
await queueStorage.createQueue('my-new-queue');

const exists = await queueStorage.queueExists('my-queue');
const count  = await queueStorage.getMessageCount('my-queue');

await queueStorage.clearMessages('my-queue');
await queueStorage.deleteQueue('old-queue');

// Access underlying QueueClient for advanced options
const queueClient = queueStorage.getQueueClient('my-queue');
await queueClient.sendMessage('Custom', {
  visibilityTimeoutInSeconds: 30,
  timeToLiveInSeconds: 3600
});
```

### Integration Example: Order Processing Pipeline

```typescript
class OrderProcessor {
  private queueStorage: QueueStorage;

  constructor(connectionString: string) {
    this.queueStorage = new QueueStorage(connectionString);
  }

  async queueOrder(orderNumber: string): Promise<void> {
    await this.queueStorage.sendMessage('orders-to-process', { orderNumber, timestamp: Date.now() });
  }

  async processOrders(): Promise<void> {
    const { receivedMessageItems } = await this.queueStorage.receiveMessages('orders-to-process', 10);

    for (const message of receivedMessageItems) {
      try {
        const decoded = Buffer.from(message.messageText, 'base64').toString('utf-8');
        const order = JSON.parse(decoded);
        console.log(`Processing order ${order.orderNumber}`);
        await this.queueStorage.deleteMessage('orders-to-process', message.messageId, message.popReceipt);
      } catch (error) {
        console.error('Failed to process message:', error);
        // Message will become visible again after visibility timeout
      }
    }
  }
}
```

---

## Configuration

### Environment Variables

```typescript
const connectionString = process.env.AZURE_STORAGE_CONNECTION_STRING;
if (!connectionString) throw new Error('AZURE_STORAGE_CONNECTION_STRING is required');
```

### Local Development with Azurite

```typescript
const blobStorage  = new BlobStorage("UseDevelopmentStorage=true");
const tableStorage = new TableStorage("UseDevelopmentStorage=true", 'MyTable');
const queueStorage = new QueueStorage("UseDevelopmentStorage=true");
```

### Managed Identity Authentication

#### System-Assigned (Most Common)

Automatically created when you enable managed identity on your Azure resource — no client ID needed.

```typescript
const blobStorage  = new BlobStorage({ accountName: 'yourstorageaccount' });
const tableStorage = new TableStorage({ accountName: 'yourstorageaccount', tableName: 'MyTable' });
const queueStorage = new QueueStorage({ accountName: 'yourstorageaccount' });
```

#### User-Assigned

A standalone identity shared across multiple Azure resources. Requires the client ID.

```typescript
const blobStorage = new BlobStorage({
  accountName: 'yourstorageaccount',
  managedIdentityClientId: 'your-client-id'
});
```

#### Environment-Based Configuration

```typescript
const blobStorage = process.env.NODE_ENV === 'production'
  ? new BlobStorage({ accountName: process.env.AZURE_STORAGE_ACCOUNT_NAME! })
  : new BlobStorage(process.env.AZURE_STORAGE_CONNECTION_STRING!);
```

> **Note:** When using Managed Identity, ensure your Azure resource has the appropriate role assignments:
> - **Storage Blob Data Contributor** for Blob Storage
> - **Storage Table Data Contributor** for Table Storage
> - **Storage Queue Data Contributor** for Queue Storage

---

## Error Handling

```typescript
try {
  const blobData = await blobStorage.downloadBlob('container', 'non-existent.txt');
} catch (error) {
  if (error.statusCode === 404) {
    console.log('Blob not found');
  } else {
    console.error('Error downloading blob:', error);
  }
}
```

---

## Best Practices

### 1. Use Managed Identity in Production

```typescript
const blobStorage = process.env.NODE_ENV === 'production'
  ? new BlobStorage({ accountName: process.env.AZURE_STORAGE_ACCOUNT_NAME! })
  : new BlobStorage(process.env.AZURE_STORAGE_CONNECTION_STRING!);
```

### 2. Short-Lived SAS URLs

```typescript
const sasUrl = await blobStorage.generateSASUrl('container', 'blob', {
  permissions: [BlobPermissions.READ],
  expiresOn: new Date(Date.now() + 3600 * 1000) // 1 hour max
});
```

### 3. Always Handle Errors

```typescript
async function safeUpload(container: string, name: string, data: Buffer) {
  try {
    return await blobStorage.uploadData(container, name, data);
  } catch (error) {
    console.error('Upload failed:', error);
    throw error;
  }
}
```

### 4. Clean Up Temporary Resources

```typescript
async function cleanup() {
  try {
    await blobStorage.deleteContainer('temp-container');
    await tableStorage.deleteTable();
  } catch (error) {
    console.error('Cleanup failed:', error);
  }
}
```

---

## API Reference

### BlobStorage

| Method | Description | Returns |
|---|---|---|
| `createContainer(containerName)` | Creates a container if it doesn't exist | `Promise<boolean>` |
| `deleteContainer(containerName)` | Deletes a container | `Promise<boolean>` |
| `upload(container, blob, buffer, length, contentType)` | Uploads a buffer (legacy) | `Promise<boolean>` |
| `uploadData(container, blob, data, headers?)` | Uploads a Buffer with optional headers | `Promise<boolean>` |
| `uploadStream(container, blob, stream, headers?)` | Uploads a readable stream | `Promise<boolean>` |
| `downloadBlob(container, blob)` | Downloads a blob as Buffer | `Promise<Buffer>` |
| `blobExists(container, blob)` | Checks if a blob exists | `Promise<boolean>` |
| `listBlobs(container, prefix)` | Lists blobs with a prefix | `Promise<BlobItem[]>` |
| `deleteBlob(container, blob)` | Deletes a blob | `Promise<boolean>` |
| `generateSASUrl(container, blob?, options?)` | Generates a SAS URL | `Promise<SASUrlComponents>` |

### TableStorage

| Method | Description | Returns |
|---|---|---|
| `createTable()` | Creates the table | `Promise<boolean>` |
| `deleteTable()` | Deletes the table | `Promise<boolean>` |
| `insert(entity)` | Inserts an entity | `Promise<boolean>` |
| `update(entity)` | Updates an entity | `Promise<boolean>` |
| `upsert(entity)` | Inserts or updates an entity | `Promise<boolean>` |
| `get(partitionKey, rowKey)` | Gets an entity | `Promise<GetTableEntityResponse>` |
| `delete(partitionKey, rowKey)` | Deletes an entity | `Promise<boolean>` |
| `list(options?)` | Lists entities with optional filter | `Promise<T[]>` |
| `getTableClient()` | Returns the underlying TableClient | `TableClient` |

### QueueStorage

| Method | Description | Returns |
|---|---|---|
| `getQueueClient(queueName)` | Gets a QueueClient | `QueueClient` |
| `sendMessage(queue, message, base64Encode?)` | Sends a message | `Promise<QueueSendMessageResponse>` |
| `receiveMessages(queue, maxMessages?, visibilityTimeout?)` | Receives messages | `Promise<any>` |
| `deleteMessage(queue, messageId, popReceipt)` | Deletes a message | `Promise<void>` |
| `peekMessages(queue, maxMessages?)` | Peeks messages without consuming | `Promise<any>` |
| `clearMessages(queue)` | Clears all messages | `Promise<void>` |
| `createQueue(queue)` | Creates a queue | `Promise<void>` |
| `deleteQueue(queue)` | Deletes a queue | `Promise<void>` |
| `queueExists(queue)` | Checks if a queue exists | `Promise<boolean>` |
| `getMessageCount(queue)` | Gets approximate message count | `Promise<number>` |

---

## Contributing

We welcome contributions! Please feel free to submit a Pull Request.

### Development Setup

```bash
git clone https://github.com/akadenia/AkadeniaAzureStorage.git
cd AkadeniaAzureStorage
npm install
npm run build
npm run test:with-azurite  # requires Azurite running locally
```

### Commit Message Guidelines

We follow [Conventional Commits](https://www.conventionalcommits.org/). Scope is required.

```text
type(scope): description
```

**Common scopes:** `blob` · `table` · `queue` · `docs` · `deps` · `test` · `build` · `ci`

**Types:** `feat` · `fix` · `docs` · `style` · `refactor` · `test` · `chore`

## Requirements

- Node.js >= 20
- Azure Storage account (or Azurite for local development)

## License

[MIT](https://github.com/akadenia/AkadeniaAzureStorage/blob/main/LICENSE)

## Support

For support, please open an issue on [GitHub](https://github.com/akadenia/AkadeniaAzureStorage/issues).
