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: true,
cli: true,
tableStrategy: 'sti' // or 'cti' (default) for Class Table Inheritance
})
class Product extends SmrtObject {
name: string = '';
price: number = 0.0;
} Configuration Options
| Option | Type | Description |
|---|---|---|
tableName | string | Custom database table name |
tableStrategy | 'cti' | 'sti' | Inheritance strategy (default: 'cti') |
conflictColumns | string[] | Natural key columns for upsert (required on junction tables) |
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 |
tenantScoped | boolean | Enable tenant scoping |
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 — these are decorators, not plain assignments
@foreignKey(Customer)
customerId: string = '';
@oneToMany(OrderItem)
items: OrderItem[] = [];
@manyToMany(Tag, { through: 'order_tags' })
tags: Tag[] = [];
// 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 three built-in AI methods: is() (boolean check), do() (freeform action), and describe() (generate a description). Each
resolves an AI client and sends a single prompt built from the string you pass.
is()
Evaluate natural-language criteria. 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 from open-ended instructions. Returns a string.
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 concise, human-readable description of the object. Returns a string.
Accepts optional AI message options (e.g. { maxTokens: 50 }).
const blurb = await product.describe();
// "A premium steel widget for home improvement..."
const short = await product.describe({ maxTokens: 50 }); 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 Relationships
@foreignKey()
Many-to-one relationship. Creates a column with the referenced object's ID.
@smrt()
class Order extends SmrtObject {
@foreignKey(Customer)
customerId: string = '';
}
// 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.
@smrt()
class Customer extends SmrtObject {
@oneToMany(Order)
orders: Order[] = [];
}
// Usage
const customer = await customers.get('cust-456');
const orders = await customer.loadRelatedMany('orders'); @manyToMany()
Many-to-many relationship via join table.
@smrt()
class Product extends SmrtObject {
@manyToMany(Tag, { through: 'product_tags' })
tags: Tag[] = [];
} Cross-Package References
For references across packages, use plain string IDs to avoid circular dependencies:
@smrt()
class Post extends SmrtObject {
// Cross-package: plain string, NOT @foreignKey()
authorId: string = '';
} 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 - breaks STI and causes "Missing _meta_type discriminator" errors
class Article extends SmrtObject {
toJSON() {
return { id: this.id, title: this.title };
// Missing: _meta_type, _meta_data, other fields
}
}
// INSTEAD: use transformJSON() to add computed fields
class Article extends SmrtObject {
protected transformJSON(data: any): any {
return { ...data, custom: 'value' };
}
} 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 |
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
}); Embedding Lifecycle
Each object exposes instance methods for managing its embedding vectors. When embeddings are
configured, save() regenerates stale embeddings automatically in the background
(unless embeddings.autoGenerate is false), so you rarely call these by
hand — but they are available for explicit control.
| Method | Returns | Purpose |
|---|---|---|
generateEmbeddings(options?) | Promise<void> | Compute and store vectors for configured fields. Content-hashed to skip unchanged fields;
pass { force: true } to regenerate or { fields: ['title'] } to scope. |
hasStaleEmbeddings() | Promise<boolean> | Whether any configured field's content has changed since its embedding was stored. |
getEmbedding(field, model?) | Promise<number[] | null> | Fetch the stored embedding vector for a field, or null if none exists. |
const article = await articles.get('article-123');
// Regenerate only if content changed
if (await article.hasStaleEmbeddings()) {
await article.generateEmbeddings();
}
// Force a full rebuild
await article.generateEmbeddings({ force: true });
// Inspect a stored vector
const vec = await article.getEmbedding('title');
if (vec) console.log(`${vec.length} dimensions`); Best Practices
1. Use TypeScript Types by Default
// Preferred — no decorators needed for plain fields
class Product extends SmrtObject {
name: string = '';
price: number = 0.0;
quantity: number = 0;
}
// Only use decorators when necessary
class Product extends SmrtObject {
@field({ required: true, unique: true })
sku: string = '';
@foreignKey(Category)
categoryId: string = '';
} 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