Skip to main content

SMRT Framework Architecture

Core Design Philosophy

The SMRT framework follows a registry-driven architecture where:

  1. Single Source of Truth: The ObjectRegistry is the central metadata store for all SMRT objects
  2. Zero Configuration: Consuming applications get full functionality (CLI, API, MCP) without configuration
  3. Auto-Discovery: The framework discovers objects at runtime via the registry
  4. 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 smrt bin that auto-discovers @smrt() objects via the registry and exposes per-object commands with no manual wiring — did ship, largely as designed: packages/cli's smrt bin has a live smrt <object>:<action> CLI (e.g. smrt product:list, not the space-separated smrt products list shown below) alongside smrt objects/smrt ls to list what's registered, both auto-discovered from the manifest/ObjectRegistry (findObjectCommand/getObjectCommandsLazy/generateObjectCommands in packages/cli/src/cli-generator.ts). The literal smrt 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, aliased mcp, writes a runnable MCP server file via MCPGenerator.generateServer(); generate-routes, aliased routes, writes SvelteKit route files; generate-types). This local surface opens a real database connection directly (cli.database in smrt.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

  1. Phase 1: Enhance CLI to use pure ObjectRegistry discovery (no manual imports)
  2. Phase 2: Add smrt mcp subcommand that discovers objects at runtime
  3. Phase 3: Update documentation and examples to show zero-config pattern
  4. Phase 4 (done): retired the in-process CLIGenerator public 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

  1. Identical Discovery Pattern: All generators start with ObjectRegistry.getAllClasses()
  2. Consistent Config Access: All use ObjectRegistry.getConfig(name) for decorator options
  3. Unified Field Access: All use ObjectRegistry.getFields(name) for schema generation
  4. 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 foreignKey relationships
  • oneToMany and manyToMany require 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

  1. Nested Eager Loading: Load relationships of relationships in single query
  2. GraphQL Generator: Auto-generate GraphQL schema and resolvers from ObjectRegistry
  3. Event System: Pub/sub for object lifecycle events
  4. Query Builder: Fluent API for complex queries
  5. Migration System: Automatic schema migration detection and execution
  6. Relationship Inference: Auto-detect relationships from field names
  7. Validation Rules: Declarative validation with custom rules
  8. Computed Fields: Virtual fields calculated from other fields

Experimental Features

  1. Module Federation: Runtime component sharing across applications
  2. Real-time Sync: WebSocket-based real-time data synchronization
  3. Offline Support: Local-first architecture with sync
  4. 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:

  1. Registry-Driven Design: ObjectRegistry is the central hub
  2. Decorator-Based Configuration: @smrt() controls all generator behavior
  3. Static Factory Pattern: Collections use create() for guaranteed initialization
  4. Eager Loading: SQL JOINs for N+1 query elimination
  5. Zero-Config CLI: smrt command discovers objects automatically

This architecture enables the framework's core value proposition: Define once, generate everywhere.