# @bernierllc/content-storage-adapter

Type-safe CRUD operations for content management with automatic soft-delete handling, version tracking, and optimistic locking.

## Features

- **Type-Safe CRUD Operations** - Fully typed content models with TypeScript strict mode
- **Automatic Soft-Delete** - All deletes are logical (deleted_at timestamp)
- **Optimistic Locking** - Concurrent edit detection via updated_at
- **Version History** - Track content changes over time
- **Unique Slug Generation** - SEO-friendly URLs with automatic uniqueness
- **Advanced Filtering** - Search by status, type, date range, and more
- **Batch Operations** - Bulk update, delete, and restore
- **Pagination Support** - Efficient list operations with hasMore indicator

## Installation

```bash
npm install @bernierllc/content-storage-adapter
```

## Dependencies

- `@bernierllc/database-adapter-core` - Abstract database interface
- `@bernierllc/slug-generator` - SEO-friendly URL generation

## Usage

### Basic CRUD Operations

```typescript
import { ContentStorageAdapter, ContentType, ContentStatus } from '@bernierllc/content-storage-adapter';
import { PostgreSQLAdapter } from '@bernierllc/database-adapter-postgresql';

// Initialize database adapter
const dbAdapter = new PostgreSQLAdapter(config);
await dbAdapter.connect();

// Create storage adapter
const storage = new ContentStorageAdapter(dbAdapter);

// Create content
const content = await storage.create({
  userId: 'user-123',
  title: 'My First Post',
  body: 'Hello, world!',
  excerpt: 'A brief introduction',
  type: ContentType.BLOG,
  status: ContentStatus.DRAFT
});

// Read content
const retrieved = await storage.getById(content.id);
const bySlug = await storage.getBySlug('my-first-post');

// Update content
const updated = await storage.update(content.id, {
  status: ContentStatus.PUBLISHED,
  publishedAt: new Date()
});

// Soft delete
await storage.delete(content.id);

// Restore deleted content
const restored = await storage.restore(content.id);
```

### Optimistic Locking

Prevent concurrent edit conflicts:

```typescript
const content = await storage.getById('content-id');

try {
  // Update with expected timestamp
  const updated = await storage.update(
    content.id,
    { title: 'Updated Title' },
    content.updatedAt
  );
} catch (error) {
  if (error instanceof OptimisticLockError) {
    // Content was modified by another user
    console.error('Conflict detected! Reload and try again.');
  }
}
```

### Advanced Filtering

```typescript
import { IContentFilters } from '@bernierllc/content-storage-adapter';

const filters: IContentFilters = {
  userId: 'user-123',
  status: [ContentStatus.PUBLISHED, ContentStatus.SCHEDULED],
  type: ContentType.BLOG,
  publishedAfter: new Date('2024-01-01'),
  searchQuery: 'TypeScript',
  limit: 20,
  offset: 0,
  orderBy: 'publishedAt',
  orderDirection: 'DESC'
};

const result = await storage.list(filters);

console.log(`Found ${result.total} items`);
console.log(`Has more: ${result.hasMore}`);
result.data.forEach(item => console.log(item.title));
```

### Batch Operations

```typescript
import { ContentBatchOperations } from '@bernierllc/content-storage-adapter';

const batchOps = new ContentBatchOperations(storage);

// Bulk update status
const updated = await batchOps.bulkUpdateStatus(
  ['id1', 'id2', 'id3'],
  ContentStatus.ARCHIVED
);
console.log(`Updated ${updated} items`);

// Bulk delete
const deleted = await batchOps.bulkDelete(['id4', 'id5']);

// Bulk restore
const restored = await batchOps.bulkRestore(['id4', 'id5']);
```

### Version History

```typescript
// Get all versions
const versions = await storage.getVersions('content-id');
versions.forEach(v => {
  console.log(`Version ${v.versionNumber}: ${v.title}`);
  console.log(`Changed by ${v.createdBy} at ${v.createdAt}`);
});

// Get specific version
const version2 = await storage.getVersion('content-id', 2);
```

## API

### ContentStorageAdapter

#### `create(data: IContentCreateData): Promise<IContent>`

Create new content with automatic slug generation.

#### `getById(id: string, options?: { includeDeleted?: boolean }): Promise<IContent | null>`

Get content by ID. Excludes soft-deleted content by default.

#### `getBySlug(slug: string): Promise<IContent | null>`

Get content by URL slug.

#### `update(id: string, data: IContentUpdateData, expectedUpdatedAt?: Date): Promise<IContent>`

Update content. Optional `expectedUpdatedAt` enables optimistic locking.

#### `delete(id: string): Promise<void>`

Soft delete content (sets deleted_at timestamp).

#### `hardDelete(id: string): Promise<void>`

Permanently delete content. Use sparingly!

#### `restore(id: string): Promise<IContent>`

Restore soft-deleted content.

#### `list(filters?: IContentFilters): Promise<IPaginatedResult<IContent>>`

List content with filtering, pagination, and ordering.

#### `getVersions(contentId: string): Promise<IContentVersion[]>`

Get all versions of content, ordered by version number descending.

#### `getVersion(contentId: string, versionNumber: number): Promise<IContentVersion | null>`

Get specific content version.

### ContentBatchOperations

#### `bulkUpdateStatus(ids: string[], status: ContentStatus): Promise<number>`

Update status for multiple content items. Returns count of updated items.

#### `bulkDelete(ids: string[]): Promise<number>`

Soft delete multiple items. Returns count of deleted items.

#### `bulkRestore(ids: string[]): Promise<number>`

Restore multiple soft-deleted items. Returns count of restored items.

## Types

### ContentType

```typescript
enum ContentType {
  BLOG = 'blog',
  ARTICLE = 'article',
  PAGE = 'page',
  SOCIAL = 'social'
}
```

### ContentFormat

```typescript
enum ContentFormat {
  MARKDOWN = 'markdown',
  HTML = 'html',
  PLAINTEXT = 'plaintext'
}
```

### ContentStatus

```typescript
enum ContentStatus {
  DRAFT = 'draft',
  IN_REVIEW = 'in_review',
  APPROVED = 'approved',
  SCHEDULED = 'scheduled',
  PUBLISHED = 'published',
  ARCHIVED = 'archived'
}
```

## Error Handling

```typescript
import {
  ContentNotFoundError,
  OptimisticLockError,
  DuplicateSlugError
} from '@bernierllc/content-storage-adapter';

try {
  await storage.update('invalid-id', { title: 'New Title' });
} catch (error) {
  if (error instanceof ContentNotFoundError) {
    console.error('Content not found or already deleted');
  } else if (error instanceof OptimisticLockError) {
    console.error('Content was modified by another user');
  }
}
```

## Database Schema

The adapter expects the following PostgreSQL schema:

```sql
CREATE TABLE content (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id VARCHAR(255) NOT NULL,
  title TEXT NOT NULL,
  slug VARCHAR(255) UNIQUE NOT NULL,
  body TEXT NOT NULL,
  excerpt TEXT,
  type VARCHAR(50) NOT NULL,
  format VARCHAR(50) NOT NULL,
  status VARCHAR(50) NOT NULL,
  workflow_stage VARCHAR(50),
  assigned_to VARCHAR(255),
  ai_reviewed BOOLEAN DEFAULT FALSE,
  ai_review_score FLOAT,
  ai_provider VARCHAR(100),
  published_at TIMESTAMP,
  scheduled_for TIMESTAMP,
  deleted_at TIMESTAMP,
  meta_title TEXT,
  meta_description TEXT,
  keywords TEXT[],
  view_count INTEGER DEFAULT 0,
  share_count INTEGER DEFAULT 0,
  generated_from_commit BOOLEAN DEFAULT FALSE,
  source_commit_sha VARCHAR(255),
  source_repository VARCHAR(500),
  created_at TIMESTAMP DEFAULT NOW(),
  updated_at TIMESTAMP DEFAULT NOW()
);

CREATE TABLE content_versions (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  content_id UUID NOT NULL REFERENCES content(id),
  version_number INTEGER NOT NULL,
  title TEXT NOT NULL,
  body TEXT NOT NULL,
  change_summary TEXT,
  is_autosave BOOLEAN DEFAULT FALSE,
  created_at TIMESTAMP DEFAULT NOW(),
  created_by VARCHAR(255) NOT NULL,
  UNIQUE(content_id, version_number)
);

CREATE INDEX idx_content_user_id ON content(user_id);
CREATE INDEX idx_content_slug ON content(slug);
CREATE INDEX idx_content_status ON content(status);
CREATE INDEX idx_content_deleted_at ON content(deleted_at);
CREATE INDEX idx_content_versions_content_id ON content_versions(content_id);
```

## Testing

```bash
# Run tests
npm test

# Run tests without watch
npm run test:run

# Run tests with coverage
npm run test:coverage

# Build TypeScript
npm run build

# Lint code
npm run lint
```

## License

Copyright (c) 2025 Bernier LLC

This file is licensed to the client under a limited-use license.
The client may use and modify this code *only within the scope of the project it was delivered for*.
Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.

## Related Packages

- [@bernierllc/database-adapter-core](../database-adapter-core) - Abstract database interface
- [@bernierllc/slug-generator](../slug-generator) - SEO-friendly URL generation
- [@bernierllc/content-storage-service](../../service/content-storage-service) - Higher-level storage orchestration
