@happyvertical/smrt-assets
Provider-agnostic asset management with versioning, type classification, metadata fields, and polymorphic associations.
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
primaryVersionIdchain +versionnumber - Derivatives: Parent-child hierarchy via
parentIdfor thumbnails, crops, format conversions - Polymorphic Association:
AssetAssociationlinks assets to any SmrtObject viametaType+metaId - Metadata Fields:
AssetMetafieldwith JSON validation rules - Tag Integration:
addTag()/removeTag()via raw join table - Provider-Agnostic:
AssetStoreabstraction 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
pnpm add @happyvertical/smrt-assetsUsing npm
npm install @happyvertical/smrt-assetsSetup
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
// 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)
// 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
// 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
// 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
// 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:
// 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:
// 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
| Type | Use Case |
|---|---|
image | Image files (JPEG, PNG, etc.) |
video | Video files (MP4, MOV, etc.) |
document | Document files (PDF, DOCX, etc.) |
audio | Audio files (MP3, WAV, etc.) |
| Status | Meaning |
|---|---|
draft | Work in progress |
published | Live and available |
archived | No longer active |
deleted | Marked for deletion |
4. Metadata System
Controlled vocabulary with validation:
// 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
// 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)
// 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
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
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
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
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
// 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)
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
// 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
// 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 effects3. Metadata Best Practices
// 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 validation4. Query Optimization
// 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()
// 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
await types.initializeCommonTypes();
await statuses.initializeCommonStatuses();
// Then query
const images = await assets.getByType('image');Version tracking confusion
Solution: primaryVersionId always points to first version
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
| Method | Returns | Description |
|---|---|---|
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
| Export | Description |
|---|---|
AssetAssociation | Polymorphic join: assetId + metaType + metaId + role + sortOrder |
AssetType | Lookup table for asset type classification |
AssetStatus | Lookup table for lifecycle status |
AssetMetafield | Custom metadata field definitions with JSON validation rules |
Folder | STI subclass of Asset (typeSlug='folder') for hierarchical organization |
Collections
| Export | Description |
|---|---|
AssetCollection | CRUD for Asset |
AssetAssociationCollection | CRUD for AssetAssociation |
AssetTypeCollection | CRUD for AssetType |
AssetStatusCollection | CRUD for AssetStatus |
AssetMetafieldCollection | CRUD for AssetMetafield |
FolderCollection | CRUD for Folder |
Utilities
| Export | Description |
|---|---|
AssetStore | Provider-agnostic file I/O that writes buffers to storage and creates Asset records |