Architecture
Internal design of ObjectQL — the Trinity Architecture of Types, Core Engine, and Platform layer
Foundation Layer Overview
The Foundation Layer is the architectural backbone of ObjectQL, consisting of three tightly integrated packages that follow the "Trinity Architecture" pattern: Types → Core → Platform. This design ensures a clean dependency graph, prevents circular dependencies, and maintains a hallucination-free, type-safe ecosystem.
The Trinity Architecture
┌─────────────────────────────────────────────────────────┐
│ @objectql/types │
│ "The Constitution" (Zero Deps) │
│ Pure TypeScript Interfaces, Enums, Custom Errors │
└─────────────────────────────────────────────────────────┘
▲
│
│ imports
│
┌─────────────────────────────────────────────────────────┐
│ @objectql/core │
│ "The Runtime Engine" │
│ Validator, Repository, Formula, Business Logic │
│ (Platform Agnostic - No Node.js) │
└─────────────────────────────────────────────────────────┘
▲
│
│ imports
│
┌─────────────────────────────────────────────────────────┐
│ @objectql/platform-node │
│ "The Node.js Bridge" │
│ File System, YAML Loading, Plugin Management │
│ (Node.js-specific: fs, path, glob) │
└─────────────────────────────────────────────────────────┘Core Packages
@objectql/types
Role: The API Contract & Type System Constitution
Purpose: Defines the Single Source of Truth for all TypeScript types, interfaces, and custom errors used throughout ObjectQL.
Key Principle: Zero dependencies. This package MUST NEVER import from other packages.
What it contains:
- Object and field type definitions
- Validation rule interfaces
- Query and filter types
- Hook and action interfaces
- Driver abstraction layer
- Migration and schema evolution types
- Custom error classes
When to use:
- Creating new features or data models
- Defining validation rules
- Building custom drivers
- Extending the ObjectQL type system
import { ObjectConfig, FieldConfig, ValidationRule } from '@objectql/types';
const project: ObjectConfig = {
name: 'project',
fields: {
name: { type: 'text', required: true }
}
};@objectql/core
Role: The Runtime Engine & Business Logic Compiler
Purpose: Implements the core ORM functionality, validation engine, repository pattern, and formula computation. This is where abstract intent (defined in types) becomes executable logic.
Key Principle: Database Compiler, not ORM wrapper. Platform-agnostic with zero Node.js dependencies.
What it contains:
ObjectQL- Main ORM class with kernel-based architectureValidator- Metadata-driven validation engineObjectRepository- CRUD operations and queriesFormula Engine- Computed fields with dynamic formulasMetadataRegistry- Dynamic schema management- Hooks & Actions runtime
When to use:
- Building applications with ObjectQL
- Executing queries and CRUD operations
- Running validation rules programmatically
- Managing object repositories
- Implementing business logic
import { ObjectQL, Validator } from '@objectql/core';
const app = new ObjectQL({
datasources: { default: driver }
});
await app.init();
const ctx = app.createContext({ userId: 'u-1' });
const projects = await ctx.object('project').find({
filters: [['status', '=', 'active']]
});@objectql/platform-node
Role: The Node.js Bridge
Purpose: Bridges the platform-agnostic Core to Node.js-specific capabilities like file system access, YAML loading, and dynamic module discovery.
Key Principle: Runtime-specific adapter. Contains all Node.js native module dependencies (fs, path, glob).
What it contains:
ObjectLoader- File system metadata loader- YAML/YML file parsing
- Plugin discovery and loading
- Module system utilities
- Convention-based file scanning
When to use:
- Loading metadata from
.object.ymlfiles - Auto-discovering objects in directories
- Building Node.js-based ObjectQL applications
- Hot-reloading metadata in development
- Organizing metadata by feature modules
import { ObjectLoader } from '@objectql/platform-node';
const loader = new ObjectLoader(app.metadata);
loader.load('./src/objects');
loader.load('./src/modules/crm');
await app.init();Architectural Principles
1. Protocol-Driven Development
ObjectQL is protocol-driven, not code-driven. The architecture enforces:
- Intent First: Define structure in schemas/types before implementation
- Metadata as Code: YAML schemas are the source of truth
- Type Safety: Strict TypeScript with no
anytypes - AI-Friendly: Metadata includes
ai_contextfor LLM understanding
2. Separation of Concerns
Each layer has a single, well-defined responsibility:
| Layer | Responsibility | Dependencies |
|---|---|---|
| Types | Define contracts | None (zero deps) |
| Core | Execute business logic | Types only |
| Platform | Bridge to runtime | Types + Core |
3. Dependency Flow
Dependencies flow in one direction only:
Platform-Node → Core → Types
✅ ✅ ❌ (no imports allowed)CRITICAL RULE: The Types package enforces architectural integrity by preventing circular dependencies.
4. Compiler Philosophy
ObjectQL is a Database Compiler, not a runtime ORM:
- Compiles abstract intent (AST) into optimized queries
- Validates at compile-time (metadata level) and runtime
- Injects security (RBAC) and validation automatically
- Never forgets what developers might (permissions, validation)
How the Layers Work Together
Example: Creating a Validated Object
Step 1: Define the Type (in @objectql/types or your app)
import { ObjectConfig, CrossFieldValidationRule } from '@objectql/types';
const projectConfig: ObjectConfig = {
name: 'project',
fields: {
start_date: { type: 'date', required: true },
end_date: { type: 'date', required: true },
status: {
type: 'select',
options: [
{ label: 'Planning', value: 'planning' },
{ label: 'Active', value: 'active' },
{ label: 'Completed', value: 'completed' }
]
}
},
validation: {
rules: [
{
name: 'valid_date_range',
type: 'cross_field',
rule: {
field: 'end_date',
operator: '>=',
compare_to: 'start_date'
},
message: 'End date must be on or after start date'
}
]
}
};Step 2: Save as YAML (using @objectql/platform-node)
Save to src/objects/project.object.yml:
name: project
label: Project
fields:
start_date:
type: date
required: true
end_date:
type: date
required: true
status:
type: select
options:
- label: Planning
value: planning
- label: Active
value: active
- label: Completed
value: completed
validation:
rules:
- name: valid_date_range
type: cross_field
rule:
field: end_date
operator: '>='
compare_to: start_date
message: End date must be on or after start dateStep 3: Load and Use (using @objectql/core)
import { ObjectQL } from '@objectql/core';
import { SqlDriver } from '@objectql/driver-sql';
import { ObjectLoader } from '@objectql/platform-node';
// Initialize Core
const app = new ObjectQL({
datasources: { default: new SqlDriver({ /* config */ }) }
});
// Load Metadata from File System
const loader = new ObjectLoader(app.metadata);
loader.load('./src/objects');
await app.init();
// Use Repository (validation runs automatically)
const ctx = app.createContext({ userId: 'u-1' });
try {
const project = await ctx.object('project').create({
name: 'New Project',
start_date: '2024-12-31',
end_date: '2024-01-01', // ❌ Invalid: before start date
status: 'planning'
});
} catch (error) {
// ValidationError: End date must be on or after start date
console.error(error.message);
}When to Use Each Package
Use @objectql/types When...
- ✅ Defining new object schemas
- ✅ Creating custom validation rules
- ✅ Building type-safe query builders
- ✅ Extending the type system
- ✅ Creating custom drivers or plugins
- ❌ Running queries or business logic
- ❌ Loading metadata from files
Use @objectql/core When...
- ✅ Building ObjectQL applications
- ✅ Executing CRUD operations
- ✅ Running validation rules
- ✅ Computing formulas
- ✅ Managing transactions
- ✅ Implementing hooks and actions
- ❌ Loading YAML files
- ❌ Discovering plugins
Use @objectql/platform-node When...
- ✅ Loading metadata from
.yml/.yamlfiles - ✅ Auto-discovering objects in directories
- ✅ Building Node.js-based applications
- ✅ Implementing hot-reload in development
- ✅ Loading external plugins
- ❌ Building browser applications
- ❌ Using in edge runtimes (Cloudflare Workers, Deno)
Installation
Install All Foundation Packages
npm install @objectql/types @objectql/core @objectql/platform-nodeInstall Individually
# Just the types (for library authors)
npm install @objectql/types
# Core + Types (for platform-agnostic apps)
npm install @objectql/core @objectql/types
# Full stack (for Node.js apps)
npm install @objectql/platform-node @objectql/core @objectql/typesComplete Example
Project Structure
my-app/
├── src/
│ ├── objects/
│ │ ├── user.object.yml
│ │ └── project.object.yml
│ ├── validations/
│ │ └── project.validation.yml
│ └── index.ts
├── package.json
└── tsconfig.jsonindex.ts
import { ObjectQL } from '@objectql/core';
import { SqlDriver } from '@objectql/driver-sql';
import { ObjectLoader } from '@objectql/platform-node';
import * as path from 'path';
// Initialize Core with Driver
const app = new ObjectQL({
datasources: {
default: new SqlDriver({
client: 'pg',
connection: {
host: 'localhost',
database: 'myapp',
user: 'postgres',
password: 'password'
}
})
}
});
// Load Metadata from File System
const loader = new ObjectLoader(app.metadata);
loader.load(path.join(__dirname, 'objects'));
loader.load(path.join(__dirname, 'validations'));
// Initialize Kernel
await app.init();
// Use the Application
const ctx = app.createContext({ userId: 'admin' });
const projects = await ctx.object('project').find({
filters: [['status', '=', 'active']],
sort: [{ field: 'created_at', order: 'desc' }],
limit: 10
});
console.log(`Found ${projects.length} active projects`);Best Practices
1. Define Types First
Always start with type definitions before writing implementation:
// ✅ Good: Type-first approach
import { ObjectConfig } from '@objectql/types';
const userConfig: ObjectConfig = {
name: 'user',
fields: {
email: { type: 'email', required: true }
}
};
// ❌ Bad: Implementation-first
const user = { email: 'test@example.com' }; // No type safety2. Keep Types Pure
Never import from @objectql/core or @objectql/platform-node in type definitions:
// ✅ Good: Pure type definition
import { ObjectConfig } from '@objectql/types';
export const projectConfig: ObjectConfig = { /* ... */ };
// ❌ Bad: Circular dependency risk
import { ObjectQL } from '@objectql/core'; // Don't do this in type files3. Use YAML for Declarative Metadata
Prefer YAML files over TypeScript for object definitions:
# ✅ Good: Declarative, AI-friendly, version-controllable
name: project
label: Project
fields:
name:
type: text
required: true// ⚠️ Acceptable: But less maintainable for large schemas
const project: ObjectConfig = {
name: 'project',
label: 'Project',
fields: {
name: { type: 'text', required: true }
}
};4. Organize by Feature
Structure metadata files by business feature, not by type:
✅ Good: Feature-based
src/
modules/
crm/
objects/
validations/
project/
objects/
validations/
❌ Bad: Type-based
src/
all-objects/
all-validations/Related Documentation
- Types Reference - Complete type system documentation
- Core Reference - Runtime engine and APIs
- Platform Node Reference - File system and plugin utilities
- Driver Development - Building custom drivers
- Validation Guide - Metadata-driven validation
Next Steps
- Understand the Types → Read the Types Reference
- Build with Core → Explore the Core Reference
- Load Metadata → Learn about Platform Node
- Choose a Driver → Review Drivers Overview
- Deploy → Check the Deployment Guide