@happyvertical/smrt-assets

Provider-agnostic asset management with versioning, type classification, metadata fields, and polymorphic associations.

v0.20.44Asset ManagementProvider-Agnostic

Overview

The @happyvertical/smrt-assets package provides provider-agnostic asset management with versioning via primaryVersionId chains, hierarchical derivatives via parentId, polymorphic associations via AssetAssociation, and folder organization via STI.

Key Features

  • STI Asset Model: Asset base with Folder STI subclass for hierarchical organization
  • Versioning: Sequential via primaryVersionId chain + version number
  • Derivatives: Parent-child hierarchy via parentId for thumbnails, crops, format conversions
  • Polymorphic Association: AssetAssociation links assets to any SmrtObject via metaType + metaId
  • Metadata Fields: AssetMetafield with JSON validation rules
  • Tag Integration: addTag()/removeTag() via raw join table
  • Provider-Agnostic: AssetStore abstraction for S3, local, GCS, CDN

Architecture

┌─────────────────────────────────────────────┐
│        Asset Management System               │
├─────────────────────────────────────────────┤
│  Asset (STI base)                            │
│  • name, slug, sourceUri, mimeType           │
│  • versioning: primaryVersionId + version    │
│  • hierarchy: parentId (derivatives)         │
│  • ownerProfileId, typeSlug, statusSlug      │
├─────────────────────────────────────────────┤
│  Folder (STI subclass, typeSlug='folder')    │
│  • Hierarchical organization                 │
├─────────────────────────────────────────────┤
│  AssetAssociation (polymorphic join)         │
│  • assetId + metaType + metaId + role        │
│  • sortOrder for ordering                    │
├─────────────────────────────────────────────┤
│  Lookup Tables                               │
│  • AssetType - classification                │
│  • AssetStatus - lifecycle                   │
│  • AssetMetafield - custom metadata defs     │
├─────────────────────────────────────────────┤
│  AssetStore                                  │
│  • Provider-agnostic file I/O               │
│  • S3, local, GCS, CDN backends              │
└─────────────────────────────────────────────┘
			

Installation

Using pnpm

bash
pnpm add @happyvertical/smrt-assets

Using npm

bash
npm install @happyvertical/smrt-assets

Setup

typescript
import {
  Asset, AssetCollection,
  AssetAssociation, AssetAssociationCollection,
  AssetType, AssetStatus, AssetMetafield,
  Folder, FolderCollection,
  AssetStore
} from '@happyvertical/smrt-assets';

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

Quick Start

1. Create Asset

typescript
// Create lookup records first
const imageType = new AssetType({ slug: 'image', name: 'Image' });
await imageType.save();

const published = new AssetStatus({ slug: 'published', name: 'Published' });
await published.save();

// Create an asset
const photo = new Asset({
  name: 'Product Photo',
  slug: 'product-photo-001',
  sourceUri: 's3://mybucket/products/photo.jpg',
  mimeType: 'image/jpeg',
  typeSlug: 'image',
  statusSlug: 'published',
  version: 1
});
await photo.save();

2. Versioning (primaryVersionId chain)

typescript
// Create version 2 -- chain via primaryVersionId
const v2 = new Asset({
  name: 'Product Photo',
  slug: 'product-photo-002',
  version: 2,
  primaryVersionId: photo.id,
  sourceUri: 's3://mybucket/products/photo-v2.jpg',
  mimeType: 'image/jpeg',
  typeSlug: 'image',
  statusSlug: 'published'
});
await v2.save();

3. Derivatives via parentId

typescript
// Create thumbnail derivative
const thumb = new Asset({
  name: 'Thumbnail',
  slug: 'product-photo-001-thumb',
  parentId: photo.id,
  sourceUri: 's3://mybucket/products/photo-001-thumb.jpg',
  mimeType: 'image/jpeg',
  typeSlug: 'image',
  statusSlug: 'published'
});
await thumb.save();

3b. Polymorphic Association

typescript
// Link asset to any SmrtObject via AssetAssociation
const assoc = new AssetAssociation({
  assetId: photo.id,
  metaType: '@happyvertical/smrt-content:Article',
  metaId: 'article-123',
  role: 'hero',
  sortOrder: 0
});
await assoc.save();

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 via primaryVersionId chain and version number:

typescript
// Version 1 created
const v1 = new Asset({
  name: 'Photo', sourceUri: 'v1.jpg',
  mimeType: 'image/jpeg', typeSlug: 'image',
  statusSlug: 'published', version: 1
});
await v1.save();
// v1.primaryVersionId = v1.id (self-reference)

// Version 2 -- chain via primaryVersionId
const v2 = new Asset({
  ...v1, slug: 'photo-v2', version: 2,
  primaryVersionId: v1.id,
  sourceUri: 'v2.jpg'
});
await v2.save();

// findVersions() to retrieve history
const history = await collection.findVersions(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

Assets inherit is() and do() from SmrtObject (smrt-core). Image-specific AI features (alt text generation, categorization) are provided by the smrt-images package, which extends Asset via STI.

Content Analysis (via smrt-core)

typescript
// Boolean validation (is)
const isHighQuality = await asset.is('a high-quality product image');

// Generate descriptions (do)
const description = await asset.do('generate a brief caption');

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) {
  const deriv = new Asset({
    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'
  });
  await deriv.save();
}

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: AssetStore Pipeline

typescript
import { AssetStore } from '@happyvertical/smrt-assets';

// AssetStore provides provider-agnostic file I/O
const store = new AssetStore({ collection, filesystem });

// Store writes buffer to storage and creates Asset record
const asset = await store.store({
  buffer: fileBuffer,
  mimeType: 'image/png',
  name: 'screenshot'
});

// Use Folder STI subclass for organization
const folder = new Folder({
  name: 'Product Images',
  slug: 'product-images'
});
await folder.save();

Integration with Other Modules

smrt-core

  • Asset extends SmrtObject for persistence
  • STI support (Folder subclass)
  • 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');

AssetStore (Provider-Agnostic File I/O)

typescript
import { AssetStore } from '@happyvertical/smrt-assets';

// AssetStore writes buffers to storage and creates Asset records
const store = new AssetStore({ collection, filesystem });
await store.store({ buffer, mimeType: 'image/png', name: 'screenshot' });

// Storage-agnostic sourceUri formats
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

Other Models

ExportDescription
AssetAssociationPolymorphic join: assetId + metaType + metaId + role + sortOrder
AssetTypeLookup table for asset type classification
AssetStatusLookup table for lifecycle status
AssetMetafieldCustom metadata field definitions with JSON validation rules
FolderSTI subclass of Asset (typeSlug='folder') for hierarchical organization

Collections

ExportDescription
AssetCollectionCRUD for Asset
AssetAssociationCollectionCRUD for AssetAssociation
AssetTypeCollectionCRUD for AssetType
AssetStatusCollectionCRUD for AssetStatus
AssetMetafieldCollectionCRUD for AssetMetafield
FolderCollectionCRUD for Folder

Utilities

ExportDescription
AssetStoreProvider-agnostic file I/O that writes buffers to storage and creates Asset records