s-m-r-t

@happyvertical/smrt-assets

Production-grade asset management with versioning, derivatives, hierarchical tagging, and AI-powered operations.

v0.19.0Asset ManagementAI-Powered

Overview

The @happyvertical/smrt-assets package provides sophisticated asset organization, versioning, and lifecycle management built on the SMRT framework.

Key Features

  • Flexible Asset Types: Images, videos, documents, audio with STI
  • Versioning System: Parent-child relationships for derivatives
  • Controlled Metadata: EAV pattern with validation
  • Hierarchical Tagging: Integration with smrt-tags
  • AI-Powered: Auto-generate alt text, descriptions, analysis
  • Storage Agnostic: Works with S3, local, CDN, etc.
  • Auto-Generated APIs: REST, CLI, MCP tools

Architecture

┌─────────────────────────────────────────────┐
│        Asset Management System               │
├─────────────────────────────────────────────┤
│  SmrtObject (Base)                           │
│  • ORM capabilities                          │
│  • Auto-generated REST/CLI/MCP               │
│  • AI methods (is, do, describe)             │
├─────────────────────────────────────────────┤
│  Asset (Core Model)                          │
│  • name, slug, sourceUri                     │
│  • MIME type, versioning                     │
│  • parent/child relationships                │
│  • tag integration                           │
├─────────────────────────────────────────────┤
│  Image (Specialized)                         │
│  • width, height, alt text                   │
│  • aspect ratio calculations                 │
│  • AI alt text generation                    │
├─────────────────────────────────────────────┤
│  Collections                                 │
│  • AssetCollection - bulk operations         │
│  • ImageCollection - image queries           │
│  • Tag management                            │
│  • Version history                           │
└─────────────────────────────────────────────┘
			

Installation

Using pnpm

bash
pnpm add @happyvertical/smrt-assets

Using npm

bash
npm install @happyvertical/smrt-assets

Setup

typescript
import { AssetCollection, ImageCollection @happyvertical '@happyvertical/smrt-assets';

const assets = await AssetCollection.create({
  db: { type: \'sqlite\', url: \'./assets.db\' }
});

const images = await ImageCollection.create({
  db: { type: \'sqlite\', url: \'./assets.db\' }
});

Quick Start

1. Create Asset

typescript
const asset = await assets.create({
  name: 'Product Photo',
  slug: 'product-photo-001',
  sourceUri: 's3://mybucket/products/photo.jpg',
  mimeType: 'image/jpeg',
  description: 'Main product photograph',
  typeSlug: 'image',
  statusSlug: 'published',
  version: 1
});

2. Create Image with Dimensions

typescript
const image = await images.create({
  name: 'hero-image.jpg',
  sourceUri: 'file:///images/hero.jpg',
  mimeType: 'image/jpeg',
  width: 1920,
  height: 1080,
  alt: 'Hero banner',
  typeSlug: 'image',
  statusSlug: 'published'
});

console.log('Aspect ratio: ' + image.aspectRatio);  // 1.777
console.log('Is landscape: ' + image.isLandscape);  // true

3. Manage Versions

typescript
// Create new version
const newVersion = await assets.createNewVersion(
  asset.id,
  's3://mybucket/products/photo-v2.jpg',
  { description: \'Updated product photo\' }
);

// Get latest version
const latest = await assets.getLatestVersion(asset.id);

// List all versions
const versions = await assets.listVersions(asset.id);

4. Work with Tags

typescript
// Add tags
await assets.addTag(asset.id, 'featured');
await assets.addTag(asset.id, 'products');

// Check tag
const isFeatured = await asset.hasTag('featured');

// Get all tags
const tags = await asset.getTags();

// Find assets by tag
const featuredAssets = await assets.getByTag('featured');

Core Concepts

1. Versioning System

Track sequential evolution of assets:

typescript
// Version 1 created
const v1 = await assets.create({
  name: 'Photo',
  sourceUri: 'v1.jpg'
});
// primaryVersionId = v1.id

// Version 2 created
const v2 = await assets.createNewVersion(v1.id, 'v2.jpg');
// primaryVersionId = v1.id (same lineage)

// Get latest
const latest = await assets.getLatestVersion(v1.id);

// List history
const history = await assets.listVersions(v1.id);

2. Parent-Child Relationships (Derivatives)

Parallel processing variants for different purposes:

typescript
// Original asset
const original = await assets.create({
  name: 'Original Photo',
  sourceUri: 's3://bucket/original.jpg'
});

// Create thumbnail derivative
const thumbnail = await assets.create({
  name: 'Original Photo Thumbnail',
  sourceUri: 's3://bucket/original-thumb.jpg',
  parentId: original.id
});

// Get all derivatives
const derivatives = await original.getChildren();

// Navigate back
const parent = await thumbnail.getParent();

3. Asset Types and Statuses

TypeUse Case
imageImage files (JPEG, PNG, etc.)
videoVideo files (MP4, MOV, etc.)
documentDocument files (PDF, DOCX, etc.)
audioAudio files (MP3, WAV, etc.)
StatusMeaning
draftWork in progress
publishedLive and available
archivedNo longer active
deletedMarked for deletion

4. Metadata System

Controlled vocabulary with validation:

typescript
// Define metadata field
const widthField = new AssetMetafield({
  slug: 'width',
  name: 'Width',
  validation: JSON.stringify({
    type: 'integer',
    minimum: 0,
    maximum: 10000
  })
});

// Validation examples
{ type: \'integer\', minimum: 0, maximum: 10000 }
{ type: \'string\', enum: [\'portrait\', \'landscape\', \'square\'] }
{ type: \'string\', pattern: \'^#[0-9A-Fa-f]{6}$\' }

5. Hierarchical Tagging

typescript
// Hierarchical tag structure
// category/products/shoes
// category/products/clothing
// featured/homepage
// featured/social-media

await assets.addTag(asset.id, 'category/products/shoes');
await assets.addTag(asset.id, 'featured/homepage');

// Query by tag
const products = await assets.getByTag('category/products');
const featured = await assets.getByTag('featured/homepage');

AI-Powered Features

Auto-Generate Alt Text

typescript
const image = await images.get({ id: imageId });
const altText = await image.generateAltText();
image.alt = altText;
await image.save();

Content Analysis

typescript
// Boolean validation
const isHighQuality = await asset.is('a high-quality product image');
const isSafeForWork = await asset.is('appropriate for all audiences');
const containsPeople = await asset.is('contains human faces');

// Generate descriptions
const description = await asset.do('generate a brief caption');
const details = await asset.describe('what this image shows');

Tutorials

Tutorial 1: E-Commerce Product Images

Step 1: Create Master Image

typescript
const master = await images.create({
  name: 'Sneaker Model XYZ - White',
  slug: 'sneaker-xyz-white',
  sourceUri: 's3://products/sneakers/xyz-white.jpg',
  mimeType: 'image/jpeg',
  width: 2560,
  height: 1920,
  typeSlug: 'product-image',
  statusSlug: 'published'
});

Step 2: Create Responsive Derivatives

typescript
const sizes = [
  { slug: \'thumb\', width: 150, height: 150 },
  { slug: \'preview\', width: 400, height: 300 },
  { slug: \'full\', width: 1000, height: 750 }
];

for (const size of sizes) {
  await images.create({
    name: master.name + ' - ' + size.slug,
    slug: master.slug + '-' + size.slug,
    sourceUri: 's3://products/sneakers/xyz-' + size.slug + '.jpg',
    mimeType: 'image/jpeg',
    width: size.width,
    height: size.height,
    parentId: master.id,
    typeSlug: 'product-image',
    statusSlug: 'published'
  });
}

Step 3: Tag and Organize

typescript
await assets.addTag(master.id, 'category/products/shoes');
await assets.addTag(master.id, 'brand/nike');
await assets.addTag(master.id, 'featured/homepage');

Tutorial 2: Auto-Processing Pipeline

typescript
async function processUpload(file: File, userId: string) {
  // Create asset
  const asset = await assets.create({
    name: file.name,
    sourceUri: 's3://uploads/' + file.name,
    mimeType: file.type,
    typeSlug: detectAssetType(file.type),
    statusSlug: 'draft',
    ownerProfileId: userId
  });

  if (file.type.startsWith('image/')) {
    const image = asset as Image;

    // Generate alt text
    const altText = await image.generateAltText();
    image.alt = altText;

    // Validate quality
    const isHighQuality = await image.is(
      'high-quality image suitable for professional use'
    );

    image.statusSlug = isHighQuality ? 'published' : 'review';
    await image.save();
  }

  return asset;
}

Integration with Other Modules

smrt-core

  • Asset extends SmrtObject for persistence
  • STI support for Image specialization
  • Auto-generated REST API, CLI, MCP tools

smrt-tags

typescript
// Hierarchical organization
await assets.addTag(assetId, 'media-type/image/product');
await assets.addTag(assetId, 'usage/ecommerce');
await assets.addTag(assetId, 'quality/high-res');

const products = await assets.getByTag('media-type/image/product');

Storage Backends

typescript
// Storage-agnostic sourceUri
sourceUri: 's3://my-bucket/images/image.jpg'
sourceUri: 'file:///var/assets/image.jpg'
sourceUri: 'gs://my-bucket/images/image.jpg'
sourceUri: 'https://cdn.example.com/images/image.jpg'

Best Practices

1. Asset Naming

typescript
// Good: descriptive, semantic slugs
'product-photo-nike-shoes-white-001'
'blog-hero-smrt-framework-2024'
'screenshot-setup-step-03'

// Bad: generic names
'image1', 'photo', 'asset-123'

2. Version vs Derivative Strategy

typescript
// Use versions for:
// - Content/source URI changes
// - Tracking historical changes
// - Example: replacing outdated marketing image

// Use derivatives for:
// - Different sizes (responsive images)
// - Format conversions (JPEG, WebP, AVIF)
// - Quality levels (high-res, compressed)
// - Crops or effects

3. Metadata Best Practices

typescript
// Define fields with validation upfront
const widthField = await metafields.getOrCreate('width', 'Width', {
  type: 'integer',
  minimum: 0,
  maximum: 10000
});

// Then use validated metadata
metadata.width = 1920;  // Validated

// Avoid arbitrary metadata
metadata.w = 'really big';  // ✗ No validation

4. Query Optimization

typescript
// Good: Filter early in query
const images = await assets.list({
  where: {
    typeSlug: 'image',
    statusSlug: 'published',
    ownerProfileId: userId
  }
});

// Avoid: Load everything then filter
const allAssets = await assets.list({});
const filtered = allAssets.filter(a => a.typeSlug === 'image');

Troubleshooting

Image dimensions not saved

Solution: Use collection.create()

typescript
// Good
const image = await images.create({
  name: 'test.jpg',
  width: 1920,
  mimeType: 'image/jpeg'
});

// Or ensure required fields
image.typeSlug = 'image';
image.statusSlug = 'published';
await image.save();

Query returns empty results

Solution: Initialize types and statuses

typescript
await types.initializeCommonTypes();
await statuses.initializeCommonStatuses();

// Then query
const images = await assets.getByType('image');

Version tracking confusion

Solution: primaryVersionId always points to first version

typescript
const v1 = await assets.create({...});  // primaryVersionId = v1.id
const v2 = await assets.createNewVersion(v1.id, 'v2.jpg');
// v2.primaryVersionId = v1.id

// Get all versions
const history = await assets.listVersions(v1.id);

API Reference

Asset Class

MethodReturnsDescription
getTags()Promise<Tag[]>Get all tags
hasTag(slug)Promise<boolean>Check if has tag
getParent()Promise<Asset | null>Get parent asset
getChildren()Promise<Asset[]>Get derivative assets
is(condition)Promise<boolean>AI validation
do(action)Promise<string>AI action

Image Class

Property/MethodType/ReturnsDescription
widthnumberImage width in pixels
heightnumberImage height in pixels
aspectRationumberComputed width / height
isLandscapebooleanComputed width > height
generateAltText()Promise<string>AI alt text generation

AssetCollection

MethodReturnsDescription
getByType(slug)Promise<Asset[]>Filter by type
getByStatus(slug)Promise<Asset[]>Filter by status
getByTag(slug)Promise<Asset[]>Filter by tag
createNewVersion(id, uri, updates?)Promise<Asset>Create new version
getLatestVersion(id)Promise<Asset | null>Get latest version
listVersions(id)Promise<Asset[]>List all versions