@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-assetsUsing npm
bash
npm install @happyvertical/smrt-assetsSetup
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); // true3. 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
| 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:
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 effects3. 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 validation4. 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
| 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 |
Image Class
| Property/Method | Type/Returns | Description |
|---|---|---|
width | number | Image width in pixels |
height | number | Image height in pixels |
aspectRatio | number | Computed width / height |
isLandscape | boolean | Computed width > height |
generateAltText() | Promise<string> | AI alt text generation |
AssetCollection
| Method | Returns | Description |
|---|---|---|
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 |