SMRT Framework Architecture
Core Design Philosophy
The SMRT framework follows a registry-driven architecture where:
- Single Source of Truth: The
ObjectRegistryis the central metadata store for all SMRT objects - Zero Configuration: Consuming applications get full functionality (CLI, API, MCP) without configuration
- Auto-Discovery: The framework discovers objects at runtime via the registry
- Generator Consistency: All code generators (CLI, API, MCP, Swagger) use identical ObjectRegistry patterns
ObjectRegistry: The Central Hub
Architecture Overview
┌─────────────────────────────────────────────────────────────┐
│ Application Code │
│ │
│ @smrt({ api: {...}, mcp: {...}, cli: true }) │
│ class Product extends SmrtObject { ... } │
└─────────────────┬───────────────────────────────────────────┘
│ @smrt decorator registers class
↓
┌─────────────────────────────────────────────────────────────┐
│ ObjectRegistry │
│ │
│ Stores: │
│ • Class constructors │
│ • Field definitions │
│ • Decorator configurations (@smrt options) │
│ • Relationship metadata │
│ • Collection instances (cached) │
└─────────────────┬───────────────────────────────────────────┘
│ All generators query registry
↓
┌─────────────────────────────────────────────────────────────┐
│ Code Generators │
│ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ APIGenerator │ │ MCPGenerator │ │
│ │ │ │ │ │
│ │ Uses: │ │ Uses: │ │
│ │ • getAll() │ │ • getAll() │ │
│ │ • getConfig()│ │ • getConfig()│ │
│ │ • getFields()│ │ • getFields()│ │
│ └──────────────┘ └──────────────┘ │
│ (core's in-process CLIGenerator was retired, #2664 — │
│ the live local CLI is packages/cli's CLIGenerator) │
│ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │SwaggerGen │ │ Runtime │ │
│ │ │ │ │ │
│ │ Uses: │ │ Uses: │ │
│ │ • getAll() │ │ • getColl() │ │
│ │ • getConfig()│ │ • getClass() │ │
│ │ • getFields()│ │ │ │
│ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────────┘
Registry API Surface
Registration APIs:
ObjectRegistry.register(constructor, config) // Manual registration
ObjectRegistry.registerCollection(name, ctor) // Register collection class
Query APIs:
ObjectRegistry.getClass(name) // Get class metadata
ObjectRegistry.getAllClasses() // Get all registered classes
ObjectRegistry.getFields(name) // Get field definitions
ObjectRegistry.getConfig(name) // Get decorator config
ObjectRegistry.getCollection(name, options) // Get/create collection instance (cached)
Lifecycle APIs:
ObjectRegistry.clear() // Clear all registrations
Auto-Registration Flow
1. Developer writes SMRT class
↓
@smrt({ api: {...}, mcp: {...}, cli: true })
class Product extends SmrtObject { ... }
2. Decorator execution (design-time)
↓
ObjectRegistry.register(Product, config)
3. Instance creation (runtime)
↓
new Product({ name: 'Widget' })
↓
super() calls ObjectRegistry.register() if not already registered
4. Generators discover at runtime
↓
MCPGenerator.generateTools() (this example; APIGenerator is analogous —
core's in-process CLIGenerator described here historically was retired
as unused public API, #2664)
├─ ObjectRegistry.getAllClasses()
├─ For each class:
│ ├─ ObjectRegistry.getConfig(name) → Get @smrt() options
│ ├─ ObjectRegistry.getFields(name) → Get field definitions
│ └─ Resolve tools based on config.mcp
└─ Tools served directly (no files written to disk)
CLI Architecture: Zero-Config Consumption
This section documents a design proposal from Issue #211's era. Its core idea — a
smrtbin that auto-discovers@smrt()objects via the registry and exposes per-object commands with no manual wiring — did ship, largely as designed:packages/cli'ssmrtbin has a livesmrt <object>:<action>CLI (e.g.smrt product:list, not the space-separatedsmrt products listshown below) alongsidesmrt objects/smrt lsto list what's registered, both auto-discovered from the manifest/ObjectRegistry(findObjectCommand/getObjectCommandsLazy/generateObjectCommandsinpackages/cli/src/cli-generator.ts). The literalsmrt mcp --port 3000(a live server with a port flag) /smrt generate <type>router shown below did not ship as such — the real commands are separately named and write files rather than bind a port (generate-mcp, aliasedmcp, writes a runnable MCP server file viaMCPGenerator.generateServer();generate-routes, aliasedroutes, writes SvelteKit route files;generate-types). This local surface opens a real database connection directly (cli.databaseinsmrt.config) and is meant for a developer/operator machine with that access, which is different from the separate@happyvertical/smrt-app-cli: a distributable application CLI that discovers commands from a running, deployed application over HTTP (GET /api/_resources) and needs no database access at all — see Choosing a transport for how the two relate. The design intent and exact command shapes below are kept for historical context; do not copy the literal syntax.
Design Principle
User's requirement: "consuming applications shouldn't have to do anything to have cli commands, the 'smrt' command should be exposed by the sdk and discover all their project objects"
Current Implementation (Before)
❌ Consuming Application Pattern (OLD, pre-#211):
packages/products/src/cli.ts
├─ Import Product, Category models
├─ Hand-write a command per object (list/get/create/update/delete)
└─ Wire each command's argument parsing and DB calls by hand
Problems:
• Boilerplate in every consuming app
• Manual model imports required
• Easy to forget models
• No consistency across apps
Target Implementation (After Issue #211)
✅ Zero-Config Pattern (NEW):
1. Developer installs SDK:
npm install @smrt/core
2. Developer defines models:
@smrt({ cli: true })
class Product extends SmrtObject { ... }
3. Developer runs CLI (no config needed):
npx smrt objects # Auto-discovers Product via registry
npx smrt products list # Auto-generated command
npx smrt products create --name "Widget"
npx smrt mcp # MCP server as subcommand (NEW)
How it works:
• SDK package.json exposes "smrt" bin
• smrt CLI discovers objects via ObjectRegistry
• No manual imports or configuration required
• Works in ANY project with SMRT objects
CLI Command Structure
smrt # Main CLI entry point
├─ objects # Discovery command
│ └─ Lists all registered SMRT objects
├─ mcp # MCP server subcommand (NEW)
│ ├─ --port 3000 # Port configuration
│ └─ --objects Product,Category # Optional object filter
├─ <object-name> <action> [options] # Auto-generated commands
│ ├─ products list # List products
│ ├─ products get <id> # Get product by ID
│ ├─ products create --name "Widget" # Create product
│ ├─ products update <id> --price 99 # Update product
│ └─ products delete <id> # Delete product (if enabled)
└─ generate <type> [options] # Code generation
├─ generate api # Generate REST API
├─ generate mcp # Generate MCP server
└─ generate swagger # Generate OpenAPI spec
Examples:
$ smrt objects # Show all registered objects
$ smrt products list --limit 10 # List 10 products
$ smrt products create --name "Widget" --price 29.99
$ smrt categories get electronics
$ smrt mcp --port 3001 # Start MCP server
$ smrt generate api --output ./api # Generate REST API
MCP Server as Subcommand
Before (consuming apps create their own MCP servers):
// packages/products/src/mcp.ts
import { Product, Category } from './lib/models';
import { createMCPServer } from '@smrt/core';
const mcp = createMCPServer([Product, Category]);
// Custom MCP server setup for each app
After (built-in subcommand):
# No code required - just run the command
npx smrt mcp
# Or in package.json script
{
"scripts": {
"mcp": "smrt mcp --port 3001"
}
}
# Auto-discovers all SMRT objects via ObjectRegistry
# Generates MCP tools for objects with mcp: { include: [...] }
Implementation Plan
- Phase 1: Enhance CLI to use pure ObjectRegistry discovery (no manual imports)
- Phase 2: Add
smrt mcpsubcommand that discovers objects at runtime - Phase 3: Update documentation and examples to show zero-config pattern
- Phase 4 (done): retired the in-process
CLIGeneratorpublic API entirely rather than merely deprecating it — see #2664 and the note at the top of this section.
Generator Consistency Pattern
All generators follow the same ObjectRegistry usage pattern:
Common Generator Pattern
export class BaseGenerator {
generate() {
// 1. Discover all registered classes
const registeredClasses = ObjectRegistry.getAllClasses();
for (const [name, classInfo] of registeredClasses) {
// 2. Get decorator configuration
const config = ObjectRegistry.getConfig(name);
// 3. Check generator-specific config (api, mcp, cli, swagger)
const generatorConfig = config[this.type] || {};
// 4. Determine which endpoints/tools/commands to generate
const included = generatorConfig.include;
const excluded = generatorConfig.exclude || [];
const shouldInclude = (endpoint: string) => {
if (included && !included.includes(endpoint)) return false;
if (excluded.includes(endpoint)) return false;
return true;
};
// 5. Get field definitions for schema generation
const fields = ObjectRegistry.getFields(name);
// 6. Generate code based on fields and config
this.generateForObject(name, fields, shouldInclude);
}
}
}
Generator-Specific Usage
CLIGenerator (HISTORICAL — retired, #2664). Core no longer ships a
CLIGenerator or a src/generators/cli.ts file; both were removed as unused
public API. The snippet below is kept only to show the shape of the deleted
class's listCommands() method — do not copy it, and do not look for this
file in the current source tree. The live local CLI transport is
packages/cli/src/cli-generator.ts's (differently-implemented) CLIGenerator.
// HISTORICAL: this method existed on core's now-deleted CLIGenerator.
async listCommands(): Promise<string[]> {
const commands: string[] = [];
const registeredClasses = ObjectRegistry.getAllClasses();
for (const [name, classInfo] of registeredClasses) {
const config = ObjectRegistry.getConfig(name);
if (config.cli === false) continue; // Skip if CLI disabled
// Push '<name>:<verb>' for each exposed CRUD verb, then '<name>:<method>'
// for each exposed public custom method.
}
return commands.sort();
}
APIGenerator (src/generators/rest.ts):
generateRoutes(): RouteDefinition[] {
const routes: RouteDefinition[] = [];
const registeredClasses = ObjectRegistry.getAllClasses();
for (const [name] of registeredClasses) {
const config = ObjectRegistry.getConfig(name);
const apiConfig = config.api || {};
if (apiConfig.include?.includes('list')) {
routes.push(this.createListRoute(name));
}
if (apiConfig.include?.includes('get')) {
routes.push(this.createGetRoute(name));
}
// ... etc
}
return routes;
}
MCPGenerator (src/generators/mcp.ts):
generateTools(): MCPTool[] {
const tools: MCPTool[] = [];
const registeredClasses = ObjectRegistry.getAllClasses();
for (const [name] of registeredClasses) {
const config = ObjectRegistry.getConfig(name);
const mcpConfig = config.mcp || {};
const shouldInclude = (endpoint: string) => {
if (mcpConfig.include && !mcpConfig.include.includes(endpoint)) {
return false;
}
if (mcpConfig.exclude?.includes(endpoint)) return false;
return true;
};
const fields = ObjectRegistry.getFields(name);
tools.push(...this.generateObjectTools(name, fields, shouldInclude));
}
return tools;
}
SwaggerGenerator (src/generators/swagger.ts):
generateSchemas(): Record<string, any> {
const schemas: Record<string, any> = {};
const registeredClasses = ObjectRegistry.getAllClasses();
for (const [name] of registeredClasses) {
const fields = ObjectRegistry.getFields(name);
schemas[name] = this.generateObjectSchema(name, fields);
schemas[`${name}List`] = this.generateListSchema(name);
}
return schemas;
}
Key Observations
- Identical Discovery Pattern: All generators start with
ObjectRegistry.getAllClasses() - Consistent Config Access: All use
ObjectRegistry.getConfig(name)for decorator options - Unified Field Access: All use
ObjectRegistry.getFields(name)for schema generation - Standard shouldInclude Logic: Include/exclude logic is consistent across generators
Singleton Collection Pattern (Phase 4)
Problem
Creating collection instances is expensive:
- Database connection setup
- Schema initialization
- AI client configuration
- Relationship metadata loading
Solution
ObjectRegistry caches collection instances using a singleton pattern:
// Cache key based on className + configuration
const cacheKey = `${className}:${JSON.stringify({
persistence: options.persistence,
db: options.db ? 'present' : undefined,
ai: options.ai ? 'present' : undefined
})}`;
// Get or create cached instance
if (!this._collectionCache.has(cacheKey)) {
const CollectionClass = this._collections.get(className);
const collection = await CollectionClass.create(options);
this._collectionCache.set(cacheKey, collection);
}
return this._collectionCache.get(cacheKey);
Benefits
- 60-80% reduction in collection initialization overhead
- Automatic caching for relationship loading
- Shared database connections across operations
- Reduced memory footprint for relationship-heavy models
Usage
// Automatic - Used internally by relationship loading
const customer = await order.loadRelated('customerId');
// ObjectRegistry.getCollection() reuses cached CustomerCollection
// Manual - For advanced use cases
const productCollection = await ObjectRegistry.getCollection('Product', {
persistence: { type: 'sql', url: 'products.db' },
ai: { provider: 'openai', apiKey: process.env.OPENAI_API_KEY }
});
Eager Loading with JOINs (Phase 5)
Problem: N+1 Query Problem
Loading relationships one at a time creates N+1 queries:
// ❌ N+1 queries: 1 main query + 100 relationship queries
const orders = await orderCollection.list({ limit: 100 });
for (const order of orders) {
await order.loadRelated('customerId'); // 100 separate queries!
await order.loadRelated('productId'); // 100 more queries!
}
// Total: 201 database queries
Solution: Eager Loading with SQL JOINs
Pre-load relationships in a single query using SQL JOINs:
// ✅ Single query with JOINs
const orders = await orderCollection.list({
limit: 100,
include: ['customerId', 'productId'] // Eager load relationships
});
for (const order of orders) {
const customer = order.getRelated('customerId'); // Already loaded!
const product = order.getRelated('productId'); // Already loaded!
}
// Total: 1 database query
Architecture
1. Application calls list() with include option
↓
orderCollection.list({ include: ['customerId', 'productId'] })
2. SQL Adapter generates JOIN query
↓
SELECT
t0.id as t0_id,
t0.customer_id as t0_customer_id,
t0.product_id as t0_product_id,
t1.id as t1_id,
t1.name as t1_name,
t2.id as t2_id,
t2.name as t2_name
FROM orders t0
LEFT JOIN customers t1 ON t0.customer_id = t1.id
LEFT JOIN products t2 ON t0.product_id = t2.id
3. Hydrator unpacks flat result into nested objects
↓
order: { id: '123', customer_id: '456', product_id: '789' }
_related: {
customerId: { id: '456', name: 'Acme Corp' },
productId: { id: '789', name: 'Widget' }
}
4. Application accesses pre-loaded relationships
↓
order.getRelated('customerId') // No query - returns cached object
Performance Benefits
- 40-70% faster for relationship-heavy queries
- Reduces database round trips from N+1 to 1
- Lower latency for list operations with relationships
- More efficient database resource utilization
Limitations
- Only works with
foreignKeyrelationships oneToManyandmanyToManyrequire separate queries- Nested eager loading (relationships of relationships) not yet supported
- SQL adapters get JOINs, REST adapters get batch queries (less efficient)
Data Flow Diagrams
Object Creation Flow
Developer Code Framework Database
│ │ │
│ new Product({...}) │ │
├──────────────────────────────> │
│ │ │
│ ObjectRegistry │
│ .register() │
│ │ │
│ Field Analysis │
│ (if not cached) │
│ │ │
│ await product.initialize() │ │
├──────────────────────────────> │
│ │ │
│ Setup Database │
│ ├─────────────────────────>
│ │ CREATE TABLE │
│ │<─────────────────────────
│ │ │
│ Setup AI/FS │
│ │ │
│<────────────────────────────── │
│ Initialized Product │ │
│ │ │
│ await product.save() │ │
├──────────────────────── ──────> │
│ ├─────────────────────────>
│ │ INSERT/UPDATE │
│ │<─────────────────────────
│<────────────────────────────── │
│ Saved │ │
Generator Execution Flow
CLI Command ObjectRegistry Code Generation
│ │ │
│ npx smrt generate api │ │
├──────────────────────────────> │
│ │ │
│ Load Project │
│ (import models) │
│ │ │
│ @smrt decorators │
│ auto-register │
│ │ │
│ APIGenerator │
│ │ │
│ .getAllClasses() │
│ ├───────────> │
│ │<────────── │
│ [Product, Category] │
│ │ │
│ For each class: │
│ • getConfig(name) │
│ • getFields(name) │
│ │ │
│ Generate routes │
│ ├─────────────────────────>
│ │ Write route files │
│ │<─────────────────────────
│ │ │
│<────────────────────────────── │
│ API generated │ │
Collection Caching Flow (Phase 4)
Application ObjectRegistry Collection
│ │ │
│ order.loadRelated() │ │
├─────────────────────────> │
│ │ │
│ Check cache: │
│ "Customer:{config}" │
│ │ │
│ Cache MISS │
│ │ │
│ Create collection │
│ ├──────────────────────────>
│ │ initialize() │
│ │<──────────────────────────
│ │ │
│ Store in cache │
│ │ │
│<───────────────────────── │
│ Customer object │ │
│ │ │
│ order2.loadRelated() │ │
├─────────────────────────> │
│ │ │
│ Check cache: │
│ "Customer:{config}" │
│ │ │
│ Cache HIT │
│<───────────────────────── │
│ Customer object │ │
│ (same instance) │ │
Eager Loading Flow (Phase 5)
Application Collection SQL Adapter Database
│ │ │ │
│ list({ include }) │ │ │
├─────────────────────────> │ │
│ │ │ │
│ Build JOIN query │ │
│ ├──────────────────────> │
│ │ │ │
│ │ Generate SQL JOINs │
│ │ ├───────────────────>
│ │ │ SELECT with JOINs│
│ │ │<───────────────────
│ │ │ Flat result set │
│ │<────────────────────── │
│ │ │ │
│ Hydrate objects │ │
│ (unpack flat → nested) │ │
│ │ │ │
│<───────────────────────── │ │
│ Objects with _related │ │ │
│ │ │ │
│ order.getRelated() │ │ │
│ (no DB query needed) │ │ │
Configuration-Driven Design
Decorator Configuration Schema
The @smrt() decorator accepts a configuration object that controls all generator behavior:
interface SmrtDecoratorConfig {
// API endpoint generation
api?: {
include?: ('list' | 'get' | 'create' | 'update' | 'delete' | string)[];
exclude?: ('list' | 'get' | 'create' | 'update' | 'delete' | string)[];
};
// MCP tool generation
mcp?: {
include?: ('list' | 'get' | 'create' | 'update' | 'delete' | string)[];
exclude?: ('list' | 'get' | 'create' | 'update' | 'delete' | string)[];
};
// CLI command generation
cli?: boolean | {
include?: ('list' | 'get' | 'create' | 'update' | 'delete' | string)[];
exclude?: ('list' | 'get' | 'create' | 'update' | 'delete' | string)[];
};
// Swagger/OpenAPI generation
swagger?: boolean | {
include?: ('list' | 'get' | 'create' | 'update' | 'delete' | string)[];
exclude?: ('list' | 'get' | 'create' | 'update' | 'delete' | string)[];
};
// Custom collection name (overrides auto-pluralization)
collection?: string;
// Lifecycle hooks
hooks?: {
beforeSave?: string | ((instance: any) => Promise<void>);
afterSave?: string | ((instance: any) => Promise<void>);
beforeDelete?: string | ((instance: any) => Promise<void>);
afterDelete?: string | ((instance: any) => Promise<void>);
};
}
Example Configurations
Public API with Read-Only MCP:
@smrt({
api: {
include: ['list', 'get', 'create', 'update', 'delete']
},
mcp: {
include: ['list', 'get'] // AI can only read, not modify
},
cli: true
})
class Product extends SmrtObject { }
Admin-Only with Full CLI:
@smrt({
api: {
exclude: ['delete'] // No public delete endpoint
},
mcp: {
exclude: ['create', 'update', 'delete'] // AI read-only
},
cli: true // Admin has full access via CLI
})
class User extends SmrtObject { }
Custom Actions:
@smrt({
api: {
include: ['list', 'get', 'analyze', 'summarize'] // Custom actions
},
mcp: {
include: ['list', 'get', 'analyze'] // AI can analyze
}
})
class Document extends SmrtObject {
async analyze() { /* implementation */ }
async summarize() { /* implementation */ }
}
Consistency Guarantees
The framework provides several consistency guarantees across all generators:
1. Decorator Config Consistency
Guarantee: All generators respect the same decorator configuration.
Implementation: All generators use ObjectRegistry.getConfig(name) and check their specific key (api, mcp, cli, swagger).
Test Coverage:
// Integration test ensures consistency: all three generators resolve their
// exposed surface from the SAME @smrt() config for a class.
// APIGenerator resolves inclusion via the same static helper preflight uses.
expect(isApiActionEnabledForObject('Product', 'delete')).toBe(false);
// The live local CLI (packages/cli/src/cli-generator.ts) resolves its own
// command surface from the same config, but has no public method shaped
// like this example -- core's in-process CLIGenerator, whose
// `listCommands()` this snippet historically called, was retired as unused
// public API (#2664). See packages/cli's own test suite for how it asserts
// command exposure today.
// MCPGenerator's tool list reflects the same mcp.include/exclude config.
const tools = await new MCPGenerator().generateTools();
expect(tools.some((t) => t.name === 'product_create')).toBe(false);
2. Field Schema Consistency
Guarantee: All generators use identical field definitions.
Implementation: All generators call ObjectRegistry.getFields(name) and use the same field-to-schema conversion logic.
Example:
// Product model defines fields
class Product extends SmrtObject {
@field({ required: true, maxLength: 100 })
name = '';
@field({ min: 0 })
price = 0.0;
}
// All generators see identical field definitions
const fields = ObjectRegistry.getFields('Product');
// API generates validation
apiGen.validate(data, fields); // name required, price >= 0
// Swagger generates schema
swaggerGen.schema(fields); // name: string(maxLength: 100), price: number(min: 0)
// CLI generates prompts
cliGen.prompt(fields); // "name (required, max 100 chars)", "price (min: 0)"
// MCP generates parameters
mcpGen.parameters(fields); // name: { type: "string", maxLength: 100 }, price: { type: "number", minimum: 0 }
TypeScript Types vs Field Helpers:
The framework supports two approaches for defining properties. Both are processed identically by all generators:
// Field decorators (when constraints or relationships are needed)
class ProductWithConstraints extends SmrtObject {
@field({ required: true, maxLength: 100 })
name = ''; // Validation needed
@field({ min: 0 })
price = 0.0; // Constraint needed
@foreignKey(Category)
categoryId = ''; // Relationship
}
// TypeScript types (when no constraints)
class ProductSimple extends SmrtObject {
name: string = ''; // → TEXT
description: string = ''; // → TEXT
quantity: number = 0; // → INTEGER (no decimal point)
price: number = 0.0; // → DECIMAL (has decimal point)
active: boolean = true; // → BOOLEAN
tags: string[] = []; // → JSON
}
// Mixed approach (common in practice)
class ProductMixed extends SmrtObject {
// TypeScript types for simple fields
name: string = '';
description: string = '';
quantity: number = 0; // INTEGER
price: number = 0.0; // DECIMAL
// Decorators only where needed
@field({ required: true, unique: true })
sku = ''; // Constraint
@foreignKey(Category)
categoryId = ''; // Relationship
}
All three approaches produce identical field definitions in ObjectRegistry.getFields() and are processed consistently by all generators (CLI, API, MCP, Swagger).
The 0 vs 0.0 Heuristic: Numeric literals without decimal point (0, 1, 42) → INTEGER; with decimal point (0.0, 4.5, 1.0) → DECIMAL.
3. Collection Name Consistency
Guarantee: All generators use identical collection names (pluralization).
Implementation: All generators use the same pluralize() utility or respect config.collection override.
4. Custom Action Consistency
Guarantee: Custom actions defined on classes are consistently available across generators.
Implementation: All generators detect custom methods by checking class prototype for non-standard methods.
Performance Optimizations
1. Registry Caching
- Class metadata: Cached on first access
- Field definitions: Analyzed once, reused everywhere
- Collection instances: Singleton pattern (Phase 4)
- Relationship metadata: Loaded once per class
2. Lazy Initialization
- Database tables created on-demand (first save/list)
- AI clients initialized only when used
- Filesystem adapters created only when needed
3. Query Optimization
- Eager loading with JOINs (Phase 5)
- Index creation for frequently queried fields
- Prepared statement reuse (SQL adapters)
- Result set streaming for large queries
4. Code Generation Caching
- Manifest generated once at build time
- Virtual modules cached by Vite
- Type declarations cached in
node_modules/.vite
Extension Points
The framework provides several extension points for custom behavior:
1. Custom Generators
Create custom generators that use ObjectRegistry:
export class CustomGenerator {
generate() {
const registeredClasses = ObjectRegistry.getAllClasses();
for (const [name] of registeredClasses) {
const config = ObjectRegistry.getConfig(name);
const fields = ObjectRegistry.getFields(name);
// Generate your custom output
this.generateCustomCode(name, config, fields);
}
}
}
2. Custom Fields
Extend the field system with custom types:
export function geopoint(options: FieldOptions = {}): FieldDefinition {
return {
type: 'geopoint',
dbType: 'TEXT', // Store as JSON
options,
validate: (value) => {
// Custom validation
return typeof value === 'object' && 'lat' in value && 'lng' in value;
},
serialize: (value) => JSON.stringify(value),
deserialize: (value) => JSON.parse(value)
};
}
3. Custom Lifecycle Hooks
Add hooks via decorator or ObjectRegistry:
@smrt({
hooks: {
beforeSave: async (instance) => {
instance.updatedAt = new Date();
},
afterDelete: async (instance) => {
await auditLog.record('delete', instance);
}
}
})
class Product extends SmrtObject { }
4. Custom Persistence Adapters
Implement custom storage backends:
export class RedisAdapter implements PersistenceAdapter {
async save(object: SmrtObject): Promise<void> {
// Custom Redis implementation
}
async list(options: ListOptions): Promise<SmrtObject[]> {
// Custom Redis query
}
}
Future Enhancements
Planned Features
- Nested Eager Loading: Load relationships of relationships in single query
- GraphQL Generator: Auto-generate GraphQL schema and resolvers from ObjectRegistry
- Event System: Pub/sub for object lifecycle events
- Query Builder: Fluent API for complex queries
- Migration System: Automatic schema migration detection and execution
- Relationship Inference: Auto-detect relationships from field names
- Validation Rules: Declarative validation with custom rules
- Computed Fields: Virtual fields calculated from other fields
Experimental Features
- Module Federation: Runtime component sharing across applications
- Real-time Sync: WebSocket-based real-time data synchronization
- Offline Support: Local-first architecture with sync
- Multi-tenancy: Built-in tenant isolation and querying
Summary
The SMRT framework's architecture centers on the ObjectRegistry as the single source of truth:
- Zero Configuration: Consuming apps get full functionality without setup
- Auto-Discovery: Runtime discovery of objects via registry
- Generator Consistency: All generators use identical registry patterns
- Performance: Singleton collections (60-80% faster) and eager loading (40-70% faster)
- Extensibility: Multiple extension points for custom behavior
Key Architectural Decisions:
- Registry-Driven Design: ObjectRegistry is the central hub
- Decorator-Based Configuration:
@smrt()controls all generator behavior - Static Factory Pattern: Collections use
create()for guaranteed initialization - Eager Loading: SQL JOINs for N+1 query elimination
- Zero-Config CLI:
smrtcommand discovers objects automatically
This architecture enables the framework's core value proposition: Define once, generate everywhere.