s-m-r-t

SMRT Objects

SMRT Objects are the core persistence layer in the SMRT framework. They provide automatic database schema generation, AI-powered methods, and lifecycle hooks.

@smrt() Decorator

Register a class with the framework to enable auto-generation of APIs, CLI commands, and MCP tools.

import { SmrtObject, smrt } from '@happyvertical/smrt-core';

@smrt({
  api: { include: ['list', 'get', 'create'] },
  mcp: { include: ['list', 'get', 'analyze'] },
  cli: true,
  tableStrategy: 'cti'  // or 'sti' for Single Table Inheritance
})
class Product extends SmrtObject {
  name: string = '';
  price: number = 0.0;
}

Configuration Options

OptionTypeDescription
namestringCustom registration name (defaults to class name)
tableNamestringCustom database table name
tableStrategy'cti' | 'sti'Inheritance strategy (default: 'cti')
apiboolean | objectREST API generation config
mcpboolean | objectMCP tools generation config
cliboolean | objectCLI commands generation config
aiobjectAI-callable methods config
hooksobjectLifecycle hooks
embeddingsobjectSemantic search config

Field Types

SMRT supports two patterns for defining fields.

TypeScript Types (Primary Pattern)

Use TypeScript types for most properties. The AST scanner infers SQL types automatically.

class Product extends SmrtObject {
  name: string = '';              // TEXT
  description: string = '';       // TEXT
  quantity: number = 0;           // INTEGER (no decimal)
  price: number = 0.0;            // DECIMAL (has decimal)
  rating: number = 4.5;           // DECIMAL (has decimal)
  active: boolean = true;         // BOOLEAN
  tags: string[] = [];            // JSON
  metadata: Record<string, any> = {}; // JSON
  launchedAt: Date = new Date();  // DATETIME
}

The 0 vs 0.0 Heuristic

Numeric default values determine column type:

PatternColumn TypeReasoning
count: number = 0INTEGERNo decimal point
price: number = 0.0DECIMALHas decimal point
rating: number = 4.5DECIMALHas decimal point
quantity: number = 1.0DECIMALTrailing .0 counts

Field Decorators (When Required)

Use decorators for relationships, constraints, or nullable decimals.

import { foreignKey, oneToMany, manyToMany, field, meta } from '@happyvertical/smrt-core';

class Order extends SmrtObject {
  // Relationships
  customerId = foreignKey(Customer);
  items = oneToMany(OrderItem);
  tags = manyToMany(Tag, { through: 'order_tags' });

  // Constraints
  @field({ required: true, unique: true, maxLength: 100 })
  orderNumber: string = '';

  // Nullable decimal
  @field({ nullable: true })
  discount: number | null = null;

  // Meta field (STI)
  @meta()
  specialInstructions: string = '';
}

Inheritance

SMRT supports multi-level class inheritance with two strategies.

Class Table Inheritance (CTI)

Default strategy. Each class gets its own table.

@smrt()
class Event extends SmrtObject {
  title: string = '';
  date: Date = new Date();
}

@smrt()
class Meeting extends Event {
  roomNumber: string = '';  // Stored in 'meetings' table
}

Single Table Inheritance (STI)

All classes share one table with a discriminator column.

@smrt({ tableStrategy: 'sti' })
class Event extends SmrtObject {
  title: string = '';
  date: Date = new Date();
}

@smrt()  // Inherits STI from parent
class Meeting extends Event {
  @meta()  // Stored in _meta_data JSONB
  roomNumber: string = '';
}

@smrt()
class Concert extends Event {
  @meta()
  artist: string = '';
}

Polymorphic Queries

const collection = await EventCollection.create({ db: 'events.db' });

// Returns mixed Meeting, Concert instances
const events = await collection.list({ orderBy: 'date ASC' });

for (const event of events) {
  if (event instanceof Meeting) {
    console.log(event.roomNumber);
  } else if (event instanceof Concert) {
    console.log(event.artist);
  }
}

// Filter by type
const meetings = await collection.list({
  where: { _meta_type: 'Meeting' }
});

AI-Powered Methods

SmrtObject includes built-in AI methods for evaluation and transformation.

is()

Evaluate criteria against the object. Returns boolean.

const product = await products.get('widget-123');

const isValid = await product.is(`
  - Has a non-empty description
  - Price is greater than $10
  - Name does not contain profanity
`);
// Returns: true or false

do()

Perform an action based on instructions. Returns string result.

const summary = await product.do(`
  Write a 50-word marketing description.
  Highlight key features and target audience.
`);
// Returns: "Introducing the premium Widget..."

describe()

Generate a description of the object.

const description = await product.describe();
// Returns: "A high-quality widget for home improvement..."

const brief = await product.describe({ maxTokens: 50 });
// Returns: "Premium widget, steel construction"

Automatic Migrations

Schema evolves with your code. Add a field, SMRT handles the migration.

// Before
class Product extends SmrtObject {
  name: string = '';
}

// After - just add the field
class Product extends SmrtObject {
  name: string = '';
  description: string = '';  // New field
  price: number = 0.0;       // New field
}

// On next startup, SMRT automatically:
// - Detects schema changes
// - Generates ALTER TABLE statements
// - Applies migrations safely

Auto-Generated Interfaces

Define once, get REST APIs, CLI commands, and MCP tools automatically.

@smrt({
  api: { include: ['list', 'get', 'create', 'update'] },
  cli: true,
  mcp: true
})
class Product extends SmrtObject {
  name: string = '';
  price: number = 0.0;
}

// You now have:
// REST: GET /api/products, POST /api/products, etc.
// CLI:  smrt product:list, smrt product:create --name "Widget"
// MCP:  product_list, product_create tools for AI agents

Runtime Introspection

Query object metadata at runtime for dynamic behavior.

// Get field definitions
const fields = Product.getFields();
// { name: { type: 'string' }, price: { type: 'number', decimal: true } }

// Check relationships
const relationships = Product.getRelationships();
// { categoryId: { type: 'foreignKey', target: 'Category' } }

// Generate forms, validate input, build queries dynamically

Context Memory

Objects can remember and recall learned patterns with confidence scoring.

// Remember a successful parsing strategy
await document.remember({
  scope: 'parser/html',
  key: 'selector',
  value: { pattern: '.article-content p' },
  confidence: 0.9
});

// Later, recall the strategy
const strategy = await document.recall({
  scope: 'parser/html',
  key: 'selector',
  minConfidence: 0.7
});

// Hierarchical scopes - falls back to parent if specific not found
const strategy = await document.recall({
  scope: 'parser/html/news-site',
  key: 'selector',
  includeAncestors: true  // Checks parser/html/news-site, parser/html, parser
});

Find objects by meaning, not just keywords. Configure embeddings on your class.

@smrt({
  embeddings: {
    fields: ['title', 'content'],
    provider: 'openai',
    model: 'text-embedding-3-small'
  }
})
class Article extends SmrtObject {
  title: string = '';
  content: string = '';
}

// Search by meaning
const results = await articles.semanticSearch(
  'articles about machine learning trends',
  { limit: 10, minSimilarity: 0.7 }
);

// Find similar articles
const similar = await articles.findSimilar(article, {
  limit: 5,
  excludeSelf: true
});

Lifecycle Hooks

Configure hooks in the @smrt() decorator.

@smrt({
  hooks: {
    beforeSave: 'validateData',
    afterSave: async (instance) => {
      await notifySubscribers(instance);
    },
    beforeDelete: 'checkDependencies',
    afterDelete: 'cleanupRelated'
  }
})
class Document extends SmrtObject {
  title: string = '';
  wordCount: number = 0;

  async validateData() {
    this.wordCount = this.content.split(/\s+/).length;
  }

  async checkDependencies() {
    const refs = await this.getReferences();
    if (refs.length > 0) {
      throw new Error('Cannot delete: has references');
    }
  }
}

Available Hooks

HookTrigger
beforeSaveBefore save() executes
afterSaveAfter save() completes
beforeCreateBefore first save (new object)
afterCreateAfter first save (new object)
beforeUpdateBefore save (existing object)
afterUpdateAfter save (existing object)
beforeDeleteBefore delete() executes
afterDeleteAfter delete() completes

Serialization

toJSON()

Framework method handling STI, meta fields, and serialization. Do not override.

transformJSON() Hook

Safe customization point for adding computed fields.

@smrt()
class Article extends SmrtObject {
  title: string = '';
  body: string = '';

  protected transformJSON(data: any): any {
    return {
      ...data,
      wordCount: this.body.split(/\s+/).length,
      preview: this.body.substring(0, 100),
      readingTime: Math.ceil(this.body.split(/\s+/).length / 200)
    };
  }
}

Dangerous Pattern

// DO NOT DO THIS
class Article extends SmrtObject {
  toJSON() {
    return { id: this.id, title: this.title };
    // Missing: _meta_type, _meta_data, other fields
    // Breaks STI and causes "Missing _meta_type discriminator" errors
  }
}

// If you must override, call super first
class Article extends SmrtObject {
  toJSON() {
    const data = super.toJSON();
    return { ...data, custom: 'value' };
  }
}

Relationships

foreignKey

Many-to-one relationship. Creates a column with the referenced object's ID.

class Order extends SmrtObject {
  customerId = foreignKey(Customer);
}

// Usage
const order = await orders.get('order-123');
const customer = await order.loadRelated('customerId');
console.log(customer.name);

oneToMany

One-to-many relationship. No column created; queries via inverse foreign key.

class Customer extends SmrtObject {
  orders = oneToMany(Order, { foreignKey: 'customerId' });
}

// Usage
const customer = await customers.get('cust-456');
const orders = await customer.loadRelatedMany('orders');

manyToMany

Many-to-many relationship via join table.

class Product extends SmrtObject {
  tags = manyToMany(Tag, { through: 'product_tags' });
}

Best Practices

1. Use TypeScript Types by Default

// Preferred
class Product extends SmrtObject {
  name: string = '';
  price: number = 0.0;
  quantity: number = 0;
}

// Only when necessary
class Product extends SmrtObject {
  @field({ required: true, unique: true })
  sku: string = '';
  categoryId = foreignKey(Category);
}

2. Always Initialize Objects

const product = new Product({ name: 'Widget' });
await product.initialize();  // Required before database operations
await product.save();

// Or use collection.create() which calls initialize automatically
const product = await collection.create({ name: 'Widget' });

3. Keep STI Hierarchies Shallow

// Good: 2 levels
Event
├── Meeting
├── Conference
└── Concert

// Avoid: 3+ levels
Event
└── CorporateEvent
    ├── Meeting
    └── Training