s-m-r-t

@happyvertical/smrt-products

Product catalog and inventory management with variants, pricing, and stock tracking.

v0.19.0CatalogInventory

Overview

smrt-products is a comprehensive product catalog management system built on the SMRT framework. It provides a complete solution for managing products, categories, inventory, and specifications with auto-generated REST APIs, MCP tools for AI integration, and reactive Svelte 5 components.

The module serves three purposes: (1) standalone application with its own dev server, (2) federated modules that can be consumed by other micro-frontends, and (3) NPM package for direct imports. All three consumption modes use the same source code, ensuring consistency and maintainability.

Architecture:

┌─────────────────────────────────────────┐
│     smrt-products Module                 │
├─────────────────────────────────────────┤
│  Models (Decorated with @smrt)           │
│  • Product (specifications, tags, price) │
│  • Category (hierarchical, counts)       │
├─────────────────────────────────────────┤
│  Auto-Generated (Vite Plugin)            │
│  • REST APIs                             │
│  • TypeScript Client                     │
│  • MCP Tools                             │
│  • Type Definitions                      │
├─────────────────────────────────────────┤
│  UI Components (Svelte 5)                │
│  • ProductCard, ProductForm              │
│  • ProductCatalog, CategoryManager       │
│  • Stores with runes                     │
├─────────────────────────────────────────┤
│  Consumption Modes                       │
│  • Standalone: npm run dev:standalone    │
│  • Federation: npm run dev:federation    │
│  • Library: npm package imports          │
└─────────────────────────────────────────┘

Installation

bash
# Using pnpm (recommended)
pnpm add @happyvertical/smrt-products

# Using npm
npm install @happyvertical/smrt-products

The module depends on @happyvertical/smrt-core for base classes, @happyvertical/ai for AI operations, and @happyvertical/sql for database operations.

Quick Start (5 Minutes)

1. Initialize Product Store

typescript
import { ProductStoreClass } from '@happyvertical/smrt-products';

// Create singleton store instance
const productStore = new ProductStoreClass();

// Load products from API
await productStore.loadProducts();

console.log('Products loaded:', productStore.items.length);
console.log('In stock:', productStore.inStockCount);
console.log('Total value:', productStore.totalValue);

2. Display Product Catalog

svelte
<script lang="ts">
  import { ProductCatalog } from '@happyvertical/smrt-products';
</script>

<!-- Complete catalog with search, filtering, CRUD -->
<ProductCatalog
  showCreateForm={true}
  readonly={false}
/>

<!-- Auto-displays: search, category filter, stats, product grid -->
<!-- Auto-handles: create, edit, delete, loading, error states -->

3. Create Product with Form

svelte
<script lang="ts">
  import { ProductForm } from '@happyvertical/smrt-products';

  let isSaving = false;

  async function handleSubmit(product) {
    isSaving = true;
    const result = await productStore.createProduct(product);
    if (result.success) {
      console.log('Product created:', result.data);
    } else {
      console.error('Error:', result.error);
    }
    isSaving = false;
  }
</script>

<ProductForm
  onSubmit={handleSubmit}
  loading={isSaving}
/>

<!-- Form includes: name, description, price, category, tags, inStock checkbox -->
<!-- Features: client-side validation, error display, loading state -->

4. Search and Filter Products

typescript
// Search by text (name, description, tags)
const results = productStore.searchProducts('laptop');

// Filter by category
const electronics = productStore.filterByCategory('Electronics');

// Filter in-stock only
const available = productStore.filterInStock();

// Access derived state
console.log('Total products:', productStore.items.length);
console.log('In stock:', productStore.inStockCount);
console.log('Total value:', productStore.totalValue);
console.log('Categories:', productStore.categories);

Core Concepts

Product Model Structure

The Product class extends SmrtObject with comprehensive fields for e-commerce and inventory management:

FieldTypePurposeExample
namestringProduct identifier"USB-C Hub"
descriptionstringDetailed info"7-port hub with PD..."
categorystringCategory name"Electronics"
manufacturerstringMaker/brand"TechCorp"
modelstringModel number"TC-HUB-7P"
pricenumberDecimal price39.99
inStockbooleanAvailabilitytrue
specificationsRecordExtensible attributes{ weight: "1kg", ports: 7 }
tagsstring[]Searchable keywords["usb", "hub", "adapter"]

Category System

Categories form a hierarchical structure for organizing products:

typescript
class Category extends SmrtObject {
  name: string
  description: string
  parentId: string | null       // Self-referencing hierarchy
  level: number                 // Depth in hierarchy (0 = root)
  productCount: number          // Cached count for performance
  active: boolean               // Soft delete flag

  // Methods
  async getProducts(): Promise<Product[]>
  async getSubcategories(): Promise<Category[]>
  async updateProductCount(): Promise<void>
  static async getRootCategories(): Promise<Category[]>
}

Specifications System

Products have an extensible specifications field for storing arbitrary attributes:

typescript
// Store specifications
await product.updateSpecification('weight', '1.2kg');
await product.updateSpecification('warranty', 24); // months
await product.updateSpecification('colors', ['black', 'silver', 'gold']);

// Retrieve specifications
const weight = await product.getSpecification('weight');
const warranty = await product.getSpecification('warranty');

// Example specifications by product type:
// Electronics: weight, dimensions, power, warranty
// Clothing: size, material, care_instructions, colors
// Furniture: dimensions, weight_capacity, assembly_required, materials

Reactive Store (Svelte 5 Runes)

The ProductStoreClass uses Svelte 5's runes for reactive state management:

typescript
export class ProductStoreClass {
  // Reactive state with $state rune
  items = $state<ProductData[]>([]);
  loading = $state(false);
  error = $state<string | null>(null);
  selectedProduct = $state<ProductData | null>(null);

  // Derived state with $derived rune
  inStockCount = $derived(
    this.items.filter(p => p.inStock).length
  );

  totalValue = $derived(
    this.items.reduce((sum, p) => sum + (p.price || 0), 0)
  );

  categories = $derived(
    [...new Set(this.items.map(p => p.category).filter(Boolean))]
  );

  // Actions
  async loadProducts(): Promise<void>
  async createProduct(data): Promise<ApiResponse>
  async updateProduct(id, updates): Promise<ApiResponse>
  async deleteProduct(id): Promise<void>

  // Filters (return derived arrays)
  filterByCategory(category): ProductData[]
  filterInStock(): ProductData[]
  searchProducts(query): ProductData[]
}

API Reference

Auto-Generated REST APIs

The @smrt decorator automatically generates REST endpoints:

MethodEndpointPurpose
GET/api/v1/productsList all products
POST/api/v1/productsCreate new product
GET/api/v1/products/:idGet single product
PUT/api/v1/products/:idUpdate product
GET/api/v1/categoriesList all categories

TypeScript Client

typescript
import { createClient } from '@happyvertical/smrt-products/client';

const client = createClient('/api/v1');

// List products
const products = await client.products.list();

// Get single product
const product = await client.products.get(productId);

// Create product
const newProduct = await client.products.create({
  name: 'New Product',
  price: 29.99,
  inStock: true
});

// Update product
const updated = await client.products.update(productId, {
  price: 24.99,
  inStock: false
});

Components API

ProductCard

svelte
<ProductCard
  product={productData}
  onEdit={handleEdit}
  onDelete={handleDelete}
/>

<!-- Props:
  - product: ProductData (required)
  - onEdit?: (product: ProductData) => void
  - onDelete?: (product: ProductData) => void

  Displays: name, manufacturer, model, category, tags
  Actions: Edit/Delete buttons (if handlers provided)
-->

ProductForm

svelte
<ProductForm
  product={existingProduct}
  onSubmit={handleSubmit}
  onCancel={handleCancel}
  loading={isSaving}
/>

<!-- Props:
  - product?: ProductData (optional, for editing)
  - onSubmit: (product: Partial<ProductData>) => void | Promise<void>
  - onCancel?: () => void
  - loading?: boolean

  Fields: name (required), description, price (required, non-negative),
          category, tags (comma-separated), inStock (checkbox)
  Features: client-side validation, error display, loading state
-->

ProductCatalog

svelte
<ProductCatalog
  readonly={false}
  showCreateForm={true}
/>

<!-- Props:
  - readonly?: boolean (default: false)
  - showCreateForm?: boolean (default: false)

  Features:
  - Search bar (filters name, description, tags)
  - Category dropdown filter
  - Stats display (total, in stock, total value)
  - Product grid with ProductCard components
  - Create/Edit/Delete operations
  - Loading and error states
-->

Tutorials

Tutorial 1: Creating and Managing Your Product Catalog (45-60 min)

Build a complete product catalog from scratch:

  • Initialize ProductStore singleton in your app
  • Create first product using ProductForm component
  • Display catalog with ProductCatalog component
  • Implement add/edit/delete operations
  • Handle error states gracefully
  • Deploy to production with persistence

Tutorial 2: Advanced Product Organization with Categories (45-60 min)

Organize products with hierarchical categories:

  • Set up hierarchical category structure (Electronics > Accessories > Cables)
  • Assign products to categories
  • Display category-filtered product views
  • Implement category-specific landing pages
  • Calculate and display product counts per category
  • Build category navigation menu

Tutorial 3: Product Search and Filtering (30-45 min)

Implement advanced search and filtering:

  • Full-text search across name, description, and tags
  • Category filters (single and multi-select)
  • Availability filter (in-stock only checkbox)
  • Combine multiple filters for precise results
  • Display result counts and stats
  • Clear all filters button

Tutorial 4: Specifications and Product Variants (45-60 min)

Handle product variants using specifications:

  • Model product variants using specifications field
  • Store variant combinations (size, color, material)
  • Display variant selector UI in ProductCard
  • Update inventory per variant
  • Support dynamic specification schema per category
  • Validate specification values before saving

Real-World Examples

Example 1: E-Commerce Product Catalog

typescript
// Initialize store
const store = new ProductStoreClass();
await store.loadProducts();

// Create category
const electronics = await Category.create({
  name: 'Electronics',
  description: 'Electronic devices and accessories',
  level: 0
});

// Create product in category
const hub = await store.createProduct({
  name: 'USB-C Hub',
  description: '7-port hub with 100W Power Delivery',
  category: 'Electronics',
  manufacturer: 'TechCorp',
  model: 'TC-HUB-7P',
  price: 39.99,
  inStock: true,
  tags: ['usb', 'hub', 'adapter', 'usb-c'],
  specifications: {
    ports: 7,
    power_delivery: '100W',
    weight: '120g',
    warranty: 24
  }
});

// Search and filter
const hubs = store.searchProducts('hub');
const available = store.filterInStock();
const electronicsProducts = store.filterByCategory('Electronics');

console.log('Total products:', store.items.length);
console.log('In stock:', store.inStockCount);
console.log('Catalog value:', store.totalValue);

Example 2: Product Inventory Dashboard

svelte
<script lang="ts">
  import { ProductCatalog } from '@happyvertical/smrt-products';
  import { onMount } from 'svelte';

  onMount(() => {
    // Catalog auto-loads products on mount
  });
</script>

<div class="dashboard">
  <h1>Inventory Management</h1>

  <ProductCatalog
    showCreateForm={true}
    readonly={false}
  />

  <!-- Auto-displays:
    - Search bar (filters by text)
    - Category filter dropdown
    - Stats: total products, in stock, total value
    - Product grid with cards
    - Create/Edit/Delete operations
    - Loading and error states
  -->
</div>

<style>
  .dashboard {
    max-width: 1200px;
    margin: 0 auto;
    padding: 2rem;
  }
</style>

Example 3: Admin Product Editor

svelte
<script lang="ts">
  import { ProductForm } from '@happyvertical/smrt-products';
  import { productStore } from './stores/product-store';

  let selectedProduct = productStore.selectedProduct;
  let isSaving = false;

  async function handleSave(productData) {
    isSaving = true;

    try {
      if (selectedProduct?.id) {
        // Update existing
        await productStore.updateProduct(selectedProduct.id, productData);
      } else {
        // Create new
        await productStore.createProduct(productData);
      }
      handleCancel();
    } catch (error) {
      console.error('Save failed:', error);
    } finally {
      isSaving = false;
    }
  }

  function handleCancel() {
    productStore.selectProduct(null);
  }
</script>

<div class="editor">
  <h2>{selectedProduct ? 'Edit' : 'Create'} Product</h2>

  <ProductForm
    product={selectedProduct}
    onSubmit={handleSave}
    onCancel={handleCancel}
    loading={isSaving}
  />
</div>

Example 4: Manufacturer-Based Search

typescript
import { Product } from '@happyvertical/smrt-products';

// Find all products by manufacturer
const techCorpProducts = await Product.findByManufacturer('TechCorp');

console.log('Found', techCorpProducts.length, 'TechCorp products');

// Compare specifications across products
for (const product of techCorpProducts) {
  const warranty = await product.getSpecification('warranty');
  const weight = await product.getSpecification('weight');

  console.log(product.name);
  console.log('  Price:', product.price);
  console.log('  Warranty:', warranty, 'months');
  console.log('  Weight:', weight);
  console.log('  In stock:', product.inStock);
  console.log('---');
}

// Calculate average price
const avgPrice = techCorpProducts.reduce((sum, p) => sum + p.price, 0) / techCorpProducts.length;
console.log('Average TechCorp product price:', avgPrice.toFixed(2));

Example 5: Product Specifications System

typescript
// Create product with specifications
const laptop = await Product.create({
  name: 'Pro Laptop 15"',
  manufacturer: 'TechCorp',
  model: 'PL-15-2024',
  price: 1299.99,
  inStock: true,
  category: 'Computers',
  specifications: {
    screen_size: '15.6 inches',
    resolution: '1920x1080',
    processor: 'Intel Core i7',
    ram: '16GB',
    storage: '512GB SSD',
    weight: '1.8kg',
    battery_life: '10 hours',
    warranty: 36
  }
});

// Update specifications
await laptop.updateSpecification('ram', '32GB');
await laptop.updateSpecification('storage', '1TB SSD');

// Retrieve specifications
const ram = await laptop.getSpecification('ram');
const processor = await laptop.getSpecification('processor');

console.log('Upgraded RAM:', ram);
console.log('Processor:', processor);

// Search products with specific specification
const allLaptops = await Product.findByCategory('Computers');
const highRamLaptops = [];

for (const product of allLaptops) {
  const ram = await product.getSpecification('ram');
  if (ram && parseInt(ram) >= 32) {
    highRamLaptops.push(product);
  }
}

console.log('Laptops with 32GB+ RAM:', highRamLaptops.length);

Integration Patterns

With smrt-core

  • Product and Category extend SmrtObject for ORM/persistence
  • @smrt decorator auto-generates REST APIs, CLI commands, MCP tools
  • Inherits AI methods: is(), do(), describe()
  • Database schema auto-generated from TypeScript types
  • Built-in validation and lifecycle hooks

With smrt-commerce

  • Products feed into shopping cart and checkout
  • Orders reference products via foreign keys
  • Inventory sync with order transactions
  • Price updates trigger commerce events
  • Product availability affects checkout flow

With smrt-assets

  • Product images/videos managed as assets
  • ALT text auto-generated via AI for accessibility
  • Asset versions for product photo updates
  • Asset tagging aligns with product tags
  • Derivatives (thumbnails, webp) generated automatically

With smrt-ledgers

  • Product prices feed into accounting entries
  • Revenue recognized per product/category
  • Cost of goods sold tracking
  • Inventory valuation for balance sheet
  • Tax calculations based on product category

With smrt-tenancy

  • Products isolated per tenant automatically
  • Category hierarchies per tenant
  • Separate product inventories
  • Tenant-specific pricing and visibility
  • Shared schema, separate data

Module Federation

  • ProductCard exported for consumption in other micro-frontends
  • ProductCatalog embeddable in dashboards
  • Product model definitions shared across services
  • ProductStore accessible in federated apps
  • Type safety maintained across boundaries

Best Practices

DOs

  • Use categories for organization (don't rely on tags alone)
  • Cache derived data (inStockCount, totalValue) via store
  • Validate product data before submission (form handles this)
  • Use tags for searchable keywords and filtering
  • Keep specifications schema consistent per product type
  • Lazy-load products for large catalogs (implement pagination)
  • Use the store for reactive state (not direct API calls)
  • Implement pagination for large product lists (1000+ items)
  • Use STI pattern for product variants/types

DON'Ts

  • Don't store large files in specifications (use smrt-assets)
  • Don't create deep category hierarchies (> 5 levels)
  • Don't hard-code prices (use dynamic fields)
  • Don't skip validation on forms
  • Don't duplicate product data across services (use federation)
  • Don't mutate store state directly (use provided actions)
  • Don't fetch all products on page load without pagination
  • Don't leave error states unhandled in UI

Common Issues and Troubleshooting

Issue: Products not appearing in catalog

Cause: Store not initialized or loadProducts() not called

Solution: Call loadProducts() in onMount or component initialization.

typescript
onMount(() => {
  productStore.loadProducts();
});

Issue: Form validation errors

Cause: Required fields empty (name, price) or invalid values

Solution: ProductForm component handles validation. Check console for errors. Ensure name is non-empty and price is a positive number.

Issue: Specifications not saving

Cause: Using wrong method or not awaiting promise

Solution: Always await updateSpecification(), pass correct key/value types.

typescript
// Correct
await product.updateSpecification('weight', '1.2kg');

// Wrong: not awaiting
product.updateSpecification('weight', '1.2kg'); // Promise not resolved

Issue: Search not finding results

Cause: Search only looks at name, description, and tags

Solution: Add relevant keywords to tags array for better searchability.

Issue: Category hierarchy confusion

Cause: Not setting parentId correctly or level incorrect

Solution: Use getRootCategories() for level=0. Set parentId for subcategories.

Issue: Performance with large catalogs

Cause: Loading all products at once without pagination

Solution: Implement API pagination. Filter/search client-side for small datasets (<1000 items), server-side for larger.

Issue: Module federation components not loading

Cause: Expose config not exporting components

Solution: Verify federation/expose.config.ts includes ProductCard, ProductCatalog exports.

Related Modules