@objectql/core (Deprecated)
The Runtime Engine - Core ORM, validation, and business logic compiler for ObjectQL
@objectql/core
Deprecated
@objectql/core is deprecated. All core functionality is now available in @objectstack/objectql.
See the Migration Guide for step-by-step instructions.
The Runtime Engine: The core ORM and business logic compiler for ObjectQL. This package transforms abstract metadata (defined in @objectql/types) into executable database operations, validations, and business rules.
Overview
@objectql/core is the heart of ObjectQL, providing the runtime engine that:
- Executes CRUD operations via the Repository pattern
- Compiles abstract queries into database-specific commands
- Runs metadata-driven validation rules
- Computes formula fields dynamically
- Manages hooks, actions, and event-driven logic
- Orchestrates the plugin-based kernel architecture
Implementation Status: ✅ Production Ready - All core features fully implemented and tested.
Installation
npm install @objectql/core @objectql/typesYou'll also need a database driver:
# Choose one or more
npm install @objectql/driver-sql # SQL databases
npm install @objectql/driver-mongo # MongoDB
npm install @objectql/driver-memory # In-memory (testing)Architecture
As of version 4.0.0, @objectql/core wraps the ObjectStackKernel from @objectstack/runtime, providing:
- Kernel-based lifecycle: Initialization, startup, and shutdown
- Plugin system: Extensible architecture with
ObjectQLPlugin - Enhanced features: Repository, Validator, Formula, and AI capabilities as plugins
┌─────────────────────────────────────────────────────┐
│ ObjectQL (Main Class) │
│ Wraps ObjectStackKernel │
├─────────────────────────────────────────────────────┤
│ • createContext() - User context management │
│ • registerObject() - Dynamic object registration │
│ • init() - Kernel initialization │
│ • shutdown() - Graceful cleanup │
└─────────────────────────────────────────────────────┘
▲
│
├── ObjectRepository (CRUD)
├── Validator (Rules Engine)
├── Formula (Computed Fields)
├── MetadataRegistry (Dynamic Schema)
└── Hooks & Actions (Event Logic)Quick Start
Basic Setup
import { ObjectQL } from '@objectql/core';
import { SqlDriver } from '@objectql/driver-sql';
// Initialize with driver
const app = new ObjectQL({
datasources: {
default: new SqlDriver({
client: 'pg',
connection: {
host: 'localhost',
database: 'myapp',
user: 'postgres',
password: 'password'
}
})
}
});
// Initialize kernel
await app.init();
// Create user context
const ctx = app.createContext({ userId: 'u-1' });
// Query data
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`);With Metadata Loading
import { ObjectQL } from '@objectql/core';
import { SqlDriver } from '@objectql/driver-sql';
import { ObjectLoader } from '@objectql/platform-node';
import * as path from 'path';
const app = new ObjectQL({
datasources: { default: new SqlDriver({ /* ... */ }) }
});
// Load metadata from file system
const loader = new ObjectLoader(app.metadata);
loader.load(path.join(__dirname, 'objects'));
await app.init();Core Features
1. ObjectRepository - CRUD Operations
The Repository pattern provides type-safe CRUD operations:
const ctx = app.createContext({ userId: 'u-1' });
const projectRepo = ctx.object('project');
// Create
const newProject = await projectRepo.create({
name: 'Website Redesign',
status: 'planning',
budget: 50000,
owner: 'u-1'
});
// Read
const project = await projectRepo.findOne(newProject._id);
const activeProjects = await projectRepo.find({
filters: [
['status', '=', 'active'],
['budget', '>', 10000]
],
sort: [{ field: 'created_at', order: 'desc' }]
});
// Update
await projectRepo.update(project._id, {
status: 'active',
start_date: new Date()
});
// Delete
await projectRepo.delete(project._id);
// Bulk operations
await projectRepo.createMany([
{ name: 'Project A', status: 'planning' },
{ name: 'Project B', status: 'planning' }
]);
// Count
const activeCount = await projectRepo.count([
['status', '=', 'active']
]);
// Aggregations
const stats = await projectRepo.aggregate({
groupBy: ['status'],
aggregates: {
total: { function: 'count' },
avg_budget: { function: 'avg', field: 'budget' }
}
});2. Validator - Metadata-Driven Validation
The validation engine executes rules defined in object metadata:
Field-Level Validation
Built-in validation for field types:
import { ObjectConfig } from '@objectql/types';
const userObject: ObjectConfig = {
name: 'user',
fields: {
email: {
type: 'email',
required: true,
validation: {
format: 'email',
message: 'Please enter a valid email address'
}
},
age: {
type: 'number',
validation: {
min: 18,
max: 120,
message: 'Age must be between 18 and 120'
}
},
username: {
type: 'text',
required: true,
validation: {
min_length: 3,
max_length: 20,
pattern: '^[a-zA-Z0-9_]+$',
message: 'Username must be 3-20 alphanumeric characters'
}
}
}
};Cross-Field Validation
Compare fields with operators:
import { Validator } from '@objectql/core';
import { CrossFieldValidationRule, ValidationContext } from '@objectql/types';
const validator = new Validator();
const dateRangeRule: CrossFieldValidationRule = {
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',
error_code: 'INVALID_DATE_RANGE',
severity: 'error',
trigger: ['create', 'update']
};
const context: ValidationContext = {
record: {
start_date: '2024-01-01',
end_date: '2024-12-31'
},
operation: 'create'
};
const result = await validator.validate([dateRangeRule], context);
if (!result.valid) {
console.error('Validation failed:', result.errors);
}State Machine Validation
Enforce valid state transitions:
import { StateMachineValidationRule } from '@objectql/types';
const statusRule: StateMachineValidationRule = {
name: 'project_status_flow',
type: 'state_machine',
field: 'status',
transitions: {
planning: {
allowed_next: ['active', 'cancelled'],
conditions: [{
field: 'budget',
operator: '>',
value: 0
}]
},
active: {
allowed_next: ['on_hold', 'completed', 'cancelled']
},
on_hold: {
allowed_next: ['active', 'cancelled']
},
completed: {
allowed_next: [],
is_terminal: true
},
cancelled: {
allowed_next: [],
is_terminal: true
}
},
message: 'Invalid status transition from {{old_status}} to {{new_status}}',
error_code: 'INVALID_STATE_TRANSITION'
};
// Validate state transition on update
const updateContext: ValidationContext = {
record: { status: 'active' },
previousRecord: { status: 'planning', budget: 50000 },
operation: 'update',
changedFields: ['status']
};
const result = await validator.validate([statusRule], updateContext);Object-Level Validation
Add validation rules to object configuration:
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: {
ai_context: {
intent: 'Ensure project data integrity',
validation_strategy: 'Fail fast with clear error messages'
},
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'
},
{
name: 'status_transition',
type: 'state_machine',
field: 'status',
transitions: {
planning: { allowed_next: ['active', 'cancelled'] },
active: { allowed_next: ['completed', 'cancelled'] },
completed: { allowed_next: [], is_terminal: true }
},
message: 'Invalid status transition'
}
]
}
};Supported Validation Types:
| Type | Description | Status |
|---|---|---|
field | Built-in field validation | ✅ Fully Implemented |
cross_field | Cross-field comparisons | ✅ Fully Implemented |
state_machine | State transition enforcement | ✅ Fully Implemented |
unique | Uniqueness constraints | ⚠️ Stub (requires DB integration) |
business_rule | Complex business rules | ⚠️ Stub (requires expression eval) |
custom | Custom validation logic | ⚠️ Stub (requires safe execution) |
dependency | Related record validation | ⚠️ Stub (requires DB integration) |
Comparison Operators:
=,!=- Equality/inequality>,>=,<,<=- Comparisonin,not_in- Array membershipcontains,not_contains- String containmentstarts_with,ends_with- String prefix/suffix
Validation Triggers:
create- Run on record creationupdate- Run on record updatedelete- Run on record deletion
Severity Levels:
error- Blocks the operation (default)warning- Shows warning but allows operationinfo- Informational message only
3. Formula Engine
Computed fields with dynamic formulas:
const opportunityObject: ObjectConfig = {
name: 'opportunity',
fields: {
amount: { type: 'currency', required: true },
probability: { type: 'percent', required: true },
expected_revenue: {
type: 'formula',
formula: 'amount * (probability / 100)',
formula_type: 'currency',
readonly: true
}
}
};
// When you create/update a record, formulas compute automatically
const opp = await ctx.object('opportunity').create({
amount: 100000,
probability: 75
// expected_revenue: 75000 (computed automatically)
});4. Hooks & Actions
Event-driven logic with lifecycle hooks:
import { HookContext, ActionContext } from '@objectql/types';
// Register a before-create hook
app.registerHook('project', 'beforeCreate', async (context: HookContext) => {
// Auto-populate timestamps
context.doc.created_at = new Date();
context.doc.created_by = context.userId;
});
// Register an after-update hook
app.registerHook('project', 'afterUpdate', async (context: HookContext) => {
// Send notification on status change
if (context.changedFields?.includes('status')) {
await sendStatusChangeNotification(context.doc);
}
});
// Register a custom action
app.registerAction('project', 'approve', async (context: ActionContext) => {
const { record, params, userId } = context;
// Update status
await ctx.object('project').update(record._id, {
status: 'approved',
approved_by: userId,
approved_at: new Date()
});
// Send notification
await sendApprovalEmail(record);
return { success: true, message: 'Project approved' };
});
// Invoke action
const result = await ctx.object('project').executeAction('approve', projectId, {
comments: 'Looks good to proceed'
});5. MetadataRegistry
Dynamic schema management:
import { MetadataRegistry } from '@objectql/core';
import { ObjectConfig } from '@objectql/types';
const registry = new MetadataRegistry();
// Register object at runtime
registry.registerObject({
name: 'custom_entity',
fields: {
name: { type: 'text', required: true }
}
});
// Get object metadata
const objectMeta = registry.getObject('custom_entity');
// List all objects
const allObjects = registry.listObjects();
// Update object metadata
registry.updateObject('custom_entity', {
fields: {
name: { type: 'text', required: true },
description: { type: 'textarea' } // Added field
}
});
// Remove object
registry.removeObject('custom_entity');6. Unified Query Language
Database-agnostic query syntax:
import { UnifiedQuery } from '@objectql/types';
const query: UnifiedQuery = {
// Filters (AND logic by default)
filters: [
['status', '=', 'active'],
['budget', '>', 10000],
['owner.department', '=', 'Engineering'] // Relation traversal
],
// Sorting
sort: [
{ field: 'priority', order: 'desc' },
{ field: 'created_at', order: 'asc' }
],
// Field projection
fields: ['_id', 'name', 'status', 'budget', 'owner.name'],
// Pagination
limit: 50,
skip: 0
};
const results = await ctx.object('project').find(query);Advanced Filters:
// OR conditions
const query: UnifiedQuery = {
filters: [
{
or: [
['status', '=', 'active'],
['status', '=', 'on_hold']
]
},
['budget', '>', 0]
]
};
// IN operator
const query: UnifiedQuery = {
filters: [
['status', 'in', ['active', 'planning', 'on_hold']]
]
};
// LIKE / CONTAINS
const query: UnifiedQuery = {
filters: [
['name', 'contains', 'Website']
]
};
// NULL checks
const query: UnifiedQuery = {
filters: [
['end_date', '=', null] // IS NULL
]
};7. Transactions
Transaction support (driver-dependent):
// Using transaction context
await app.transaction(async (txCtx) => {
// Create project
const project = await txCtx.object('project').create({
name: 'New Project',
status: 'planning'
});
// Create related tasks
await txCtx.object('task').createMany([
{ project_id: project._id, name: 'Task 1' },
{ project_id: project._id, name: 'Task 2' }
]);
// If any operation fails, entire transaction rolls back
});API Reference
ObjectQL Class
Constructor:
new ObjectQL(config: ObjectQLConfig)Configuration:
interface ObjectQLConfig {
datasources: Record<string, Driver>;
registry?: MetadataRegistry;
plugins?: ObjectQLPlugin[];
}Methods:
init(): Promise<void>- Initialize kernel and pluginsshutdown(): Promise<void>- Graceful shutdowncreateContext(user: any): ObjectQLContext- Create user contextregisterObject(config: ObjectConfig): void- Register object dynamicallyregisterHook(object: string, event: string, handler: HookHandler): void- Register hookregisterAction(object: string, name: string, handler: ActionHandler): void- Register actiontransaction(callback: (ctx: ObjectQLContext) => Promise<void>): Promise<void>- Execute transactiongetKernel(): ObjectStackKernel- Access underlying kernel
ObjectQLContext
User-scoped context for operations:
interface ObjectQLContext {
userId: string;
user: any;
object(name: string): ObjectRepository;
transaction(callback: Function): Promise<void>;
}ObjectRepository
Methods:
find(query?: UnifiedQuery): Promise<any[]>- Query recordsfindOne(id: string): Promise<any>- Get record by IDcreate(data: any): Promise<any>- Create recordupdate(id: string, data: any): Promise<any>- Update recorddelete(id: string): Promise<boolean>- Delete recordcreateMany(records: any[]): Promise<any[]>- Bulk createupdateMany(filters: FilterExpression, data: any): Promise<number>- Bulk updatedeleteMany(filters: FilterExpression): Promise<number>- Bulk deletecount(filters?: FilterExpression): Promise<number>- Count recordsaggregate(config: AggregateConfig): Promise<any[]>- AggregationsexecuteAction(name: string, recordId: string, params?: any): Promise<any>- Execute custom action
Validator Class
Constructor:
new Validator(options?: ValidatorOptions)Options:
interface ValidatorOptions {
language?: string; // Default: 'en'
languageFallback?: string[]; // Default: ['en', 'zh-CN']
}Methods:
validate(rules: AnyValidationRule[], context: ValidationContext): Promise<ValidationResult>- Execute validation rulesvalidateField(fieldName: string, fieldConfig: FieldConfig, value: any, context: ValidationContext): Promise<ValidationRuleResult[]>- Validate single field
Accessing the Kernel
For advanced use cases, access the underlying ObjectStackKernel:
const kernel = app.getKernel();
// Use kernel methods
await kernel.start();
await kernel.stop();
// Access plugins
const plugins = kernel.getPlugins();Kernel Services
ObjectQL implements the ObjectStack protocol's kernel services architecture. The protocol defines 17 services with 57 total methods governed by the ObjectStackProtocol interface.
Architecture
┌─────────────────────────────────────────────────────────┐
│ Kernel Layer │
│ ┌──────────────────┐ ┌──────────────────────────────┐ │
│ │ metadata (⚠️) │ │ data + analytics (✅) │ │
│ │ In-memory only │ │ ObjectQL example kernel │ │
│ │ DB persistence │ │ Will be rebuilt as plugins │ │
│ │ pending │ │ │ │
│ └──────────────────┘ └──────────────────────────────┘ │
├─────────────────────────────────────────────────────────┤
│ Plugin Layer │
│ All other services: auth, automation, workflow, ui, │
│ realtime, notification, ai, i18n, graphql, search, │
│ file-storage, cache, queue, job │
│ │
│ Discovery API reports: enabled/unavailable/degraded │
│ per service so clients adapt their UI accordingly │
└─────────────────────────────────────────────────────────┘Service Status
The getDiscovery() method returns a per-service status map so clients know what is available:
const protocol = new ObjectStackProtocolImplementation(engine);
const discovery = await protocol.getDiscovery();
// discovery.services.metadata → { enabled: true, status: 'degraded', ... }
// discovery.services.data → { enabled: true, status: 'available', ... }
// discovery.services.auth → { enabled: false, status: 'unavailable', ... }Implemented Services (Kernel-Provided)
| Service | Methods | Status |
|---|---|---|
| metadata | 7 (getDiscovery, getMetaTypes, getMetaItems, getMetaItem, saveMetaItem, getMetaItemCached, getUiView) | ⚠️ Framework (in-memory) |
| data | 9 (findData, getData, createData, updateData, deleteData, batchData, createManyData, updateManyData, deleteManyData) | ✅ Implemented |
| analytics | 2 (analyticsQuery, getAnalyticsMeta) | ✅ Implemented |
Plugin-Required Services
| Service | Methods | Criticality |
|---|---|---|
| auth | 3 (checkPermission, getObjectPermissions, getEffectivePermissions) | required |
| ui | 5 (listViews, getView, createView, updateView, deleteView) | optional |
| workflow | 5 (getWorkflowConfig, getWorkflowState, workflowTransition, workflowApprove, workflowReject) | optional |
| automation | 1 (triggerAutomation) | optional |
| realtime | 6 (connect, disconnect, subscribe, unsubscribe, setPresence, getPresence) | optional |
| notification | 7 (registerDevice, unregisterDevice, getNotificationPreferences, updateNotificationPreferences, listNotifications, markNotificationsRead, markAllNotificationsRead) | optional |
| ai | 4 (aiNlq, aiChat, aiSuggest, aiInsights) | optional |
| i18n | 3 (getLocales, getTranslations, getFieldLabels) | optional |
| cache | — | core |
| queue | — | core |
| job | — | core |
Plugin Registration
When a plugin registers a service, the discovery endpoint automatically updates:
// In a plugin's install() method:
protocol.updateServiceStatus('auth', {
enabled: true,
status: 'available',
route: '/api/v1/auth',
provider: 'plugin-auth',
});For the complete kernel services specification, see: protocol.objectstack.ai/docs/guides/kernel-services
Shared Metadata
Share MetadataRegistry between multiple ObjectQL instances:
const registry = new MetadataRegistry();
// Pre-load metadata
registry.registerObject({ /* ... */ });
// Share registry
const app1 = new ObjectQL({
registry: registry,
datasources: { default: driver1 }
});
const app2 = new ObjectQL({
registry: registry,
datasources: { default: driver2 }
});Complete Example
import { ObjectQL, Validator } from '@objectql/core';
import { SqlDriver } from '@objectql/driver-sql';
import { ObjectLoader } from '@objectql/platform-node';
import { ObjectConfig, CrossFieldValidationRule } from '@objectql/types';
import * as path from 'path';
// Initialize ObjectQL
const app = new ObjectQL({
datasources: {
default: new SqlDriver({
client: 'pg',
connection: {
host: 'localhost',
database: 'myapp',
user: 'postgres',
password: 'password'
}
})
}
});
// Load metadata from files
const loader = new ObjectLoader(app.metadata);
loader.load(path.join(__dirname, 'objects'));
// Register hooks
app.registerHook('project', 'beforeCreate', async (context) => {
context.doc.created_at = new Date();
context.doc.created_by = context.userId;
});
app.registerHook('project', 'afterUpdate', async (context) => {
if (context.changedFields?.includes('status')) {
console.log(`Project ${context.doc.name} status changed to ${context.doc.status}`);
}
});
// Initialize
await app.init();
// Create context
const ctx = app.createContext({ userId: 'admin' });
// Execute operations
try {
const project = await ctx.object('project').create({
name: 'Website Redesign',
status: 'planning',
budget: 50000,
start_date: '2024-01-01',
end_date: '2024-12-31'
});
console.log('Project created:', project._id);
// Query projects
const activeProjects = await ctx.object('project').find({
filters: [['status', '=', 'active']],
sort: [{ field: 'created_at', order: 'desc' }]
});
console.log(`Found ${activeProjects.length} active projects`);
} catch (error) {
console.error('Operation failed:', error.message);
}
// Shutdown
await app.shutdown();Best Practices
1. Always Initialize
Call init() before using the app:
const app = new ObjectQL({ datasources: { default: driver } });
await app.init(); // Required!2. Use Contexts
Create user contexts for all operations:
// ✅ Good: User context
const ctx = app.createContext({ userId: 'u-1' });
await ctx.object('project').find();
// ❌ Bad: No user context
await app.object('project').find(); // Won't work3. Handle Validation Errors
Validation errors are thrown during CRUD operations:
try {
await ctx.object('project').create(data);
} catch (error) {
if (error.code === 'VALIDATION_FAILED') {
console.error('Validation errors:', error.details);
}
}4. Use Transactions for Atomicity
Wrap related operations in transactions:
await app.transaction(async (txCtx) => {
const order = await txCtx.object('order').create({ /* ... */ });
await txCtx.object('order_line').createMany(orderLines);
});5. Define Metadata Declaratively
Prefer YAML over programmatic registration:
# ✅ Good: project.object.yml
name: project
fields:
name:
type: text
required: true// ⚠️ Acceptable: But less maintainable
app.registerObject({
name: 'project',
fields: { name: { type: 'text', required: true } }
});Performance Tips
- Use field projection - Only fetch required fields
- Add indexes - Configure
indexed: trueon frequently queried fields - Batch operations - Use
createMany,updateManyinstead of loops - Cache metadata - Share
MetadataRegistryacross instances - Use transactions wisely - Only for operations requiring atomicity
Related Documentation
- Foundation Overview - Trinity architecture
- Types Reference - Type definitions
- Platform Node - Metadata loading
- Drivers Overview - Database drivers
- Validation Guide - Advanced validation patterns