β

ObjectQL v4.0 is currently in Beta.

ObjectStack LogoObjectQL
Architecture

@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/types

You'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:

TypeDescriptionStatus
fieldBuilt-in field validation✅ Fully Implemented
cross_fieldCross-field comparisons✅ Fully Implemented
state_machineState transition enforcement✅ Fully Implemented
uniqueUniqueness constraints⚠️ Stub (requires DB integration)
business_ruleComplex business rules⚠️ Stub (requires expression eval)
customCustom validation logic⚠️ Stub (requires safe execution)
dependencyRelated record validation⚠️ Stub (requires DB integration)

Comparison Operators:

  • =, != - Equality/inequality
  • >, >=, <, <= - Comparison
  • in, not_in - Array membership
  • contains, not_contains - String containment
  • starts_with, ends_with - String prefix/suffix

Validation Triggers:

  • create - Run on record creation
  • update - Run on record update
  • delete - Run on record deletion

Severity Levels:

  • error - Blocks the operation (default)
  • warning - Shows warning but allows operation
  • info - 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 plugins
  • shutdown(): Promise<void> - Graceful shutdown
  • createContext(user: any): ObjectQLContext - Create user context
  • registerObject(config: ObjectConfig): void - Register object dynamically
  • registerHook(object: string, event: string, handler: HookHandler): void - Register hook
  • registerAction(object: string, name: string, handler: ActionHandler): void - Register action
  • transaction(callback: (ctx: ObjectQLContext) => Promise<void>): Promise<void> - Execute transaction
  • getKernel(): 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 records
  • findOne(id: string): Promise<any> - Get record by ID
  • create(data: any): Promise<any> - Create record
  • update(id: string, data: any): Promise<any> - Update record
  • delete(id: string): Promise<boolean> - Delete record
  • createMany(records: any[]): Promise<any[]> - Bulk create
  • updateMany(filters: FilterExpression, data: any): Promise<number> - Bulk update
  • deleteMany(filters: FilterExpression): Promise<number> - Bulk delete
  • count(filters?: FilterExpression): Promise<number> - Count records
  • aggregate(config: AggregateConfig): Promise<any[]> - Aggregations
  • executeAction(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 rules
  • validateField(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)

ServiceMethodsStatus
metadata7 (getDiscovery, getMetaTypes, getMetaItems, getMetaItem, saveMetaItem, getMetaItemCached, getUiView)⚠️ Framework (in-memory)
data9 (findData, getData, createData, updateData, deleteData, batchData, createManyData, updateManyData, deleteManyData)✅ Implemented
analytics2 (analyticsQuery, getAnalyticsMeta)✅ Implemented

Plugin-Required Services

ServiceMethodsCriticality
auth3 (checkPermission, getObjectPermissions, getEffectivePermissions)required
ui5 (listViews, getView, createView, updateView, deleteView)optional
workflow5 (getWorkflowConfig, getWorkflowState, workflowTransition, workflowApprove, workflowReject)optional
automation1 (triggerAutomation)optional
realtime6 (connect, disconnect, subscribe, unsubscribe, setPresence, getPresence)optional
notification7 (registerDevice, unregisterDevice, getNotificationPreferences, updateNotificationPreferences, listNotifications, markNotificationsRead, markAllNotificationsRead)optional
ai4 (aiNlq, aiChat, aiSuggest, aiInsights)optional
i18n3 (getLocales, getTranslations, getFieldLabels)optional
cachecore
queuecore
jobcore

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 work

3. 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

  1. Use field projection - Only fetch required fields
  2. Add indexes - Configure indexed: true on frequently queried fields
  3. Batch operations - Use createMany, updateMany instead of loops
  4. Cache metadata - Share MetadataRegistry across instances
  5. Use transactions wisely - Only for operations requiring atomicity

Support

On this page