npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@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-adapter

Dependencies

  • @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 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