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
| Option | Type | Description |
|---|---|---|
name | string | Custom registration name (defaults to class name) |
tableName | string | Custom database table name |
tableStrategy | 'cti' | 'sti' | Inheritance strategy (default: 'cti') |
api | boolean | object | REST API generation config |
mcp | boolean | object | MCP tools generation config |
cli | boolean | object | CLI commands generation config |
ai | object | AI-callable methods config |
hooks | object | Lifecycle hooks |
embeddings | object | Semantic 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:
| Pattern | Column Type | Reasoning |
|---|---|---|
count: number = 0 | INTEGER | No decimal point |
price: number = 0.0 | DECIMAL | Has decimal point |
rating: number = 4.5 | DECIMAL | Has decimal point |
quantity: number = 1.0 | DECIMAL | Trailing .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
}); Semantic Search
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
| Hook | Trigger |
|---|---|
beforeSave | Before save() executes |
afterSave | After save() completes |
beforeCreate | Before first save (new object) |
afterCreate | After first save (new object) |
beforeUpdate | Before save (existing object) |
afterUpdate | After save (existing object) |
beforeDelete | Before delete() executes |
afterDelete | After 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