@bernierllc/content-storage-adapter
v1.0.4
Published
Type-safe CRUD operations for content management with soft-delete, version tracking, and optimistic locking
Readme
@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
npm install @bernierllc/content-storage-adapterDependencies
@bernierllc/database-adapter-core- Abstract database interface@bernierllc/slug-generator- SEO-friendly URL generation
Usage
Basic CRUD Operations
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:
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
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
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
// 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
enum ContentType {
BLOG = 'blog',
ARTICLE = 'article',
PAGE = 'page',
SOCIAL = 'social'
}ContentFormat
enum ContentFormat {
MARKDOWN = 'markdown',
HTML = 'html',
PLAINTEXT = 'plaintext'
}ContentStatus
enum ContentStatus {
DRAFT = 'draft',
IN_REVIEW = 'in_review',
APPROVED = 'approved',
SCHEDULED = 'scheduled',
PUBLISHED = 'published',
ARCHIVED = 'archived'
}Error Handling
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:
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
# 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 lintLicense
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 - Abstract database interface
- @bernierllc/slug-generator - SEO-friendly URL generation
- @bernierllc/content-storage-service - Higher-level storage orchestration
