| title | IDataEngine Contract |
|---|---|
| description | Reference for the Data Engine contract — the core data persistence layer for CRUD operations, queries, aggregations, and transactions |
The Data Engine is the core persistence layer of ObjectStack. Every data operation — inserts, finds, updates, deletes, counts, and aggregations — flows through this contract. The Kernel delegates to the Data Engine after applying security, validation, and hooks.
**Source:** `packages/spec/src/contracts/data-engine.ts` **Schema:** `packages/spec/src/data/data-engine.zod.ts` **Service name:** `data` (registered via `CoreServiceName`)The canonical IDataEngine interface uses QueryAST-aligned parameter names (where, fields, orderBy, limit, offset, expand) — no mechanical translation is needed between the Engine and Driver layers.
import type {
EngineQueryOptions,
DataEngineInsertOptions,
EngineUpdateOptions,
EngineDeleteOptions,
EngineAggregateOptions,
EngineCountOptions,
DataEngineRequest,
} from '@objectstack/spec/data';
export interface IDataEngine {
// Query
find(objectName: string, query?: EngineQueryOptions): Promise<any[]>;
findOne(objectName: string, query?: EngineQueryOptions): Promise<any>;
count(objectName: string, query?: EngineCountOptions): Promise<number>;
aggregate(objectName: string, query: EngineAggregateOptions): Promise<any[]>;
// Mutation (write ops also accept in-process WriteObservabilityOptions — see `update`)
insert(objectName: string, data: any | any[], options?: DataEngineInsertOptions & WriteObservabilityOptions): Promise<any>;
update(objectName: string, data: any, options?: EngineUpdateOptions & WriteObservabilityOptions): Promise<any>;
delete(objectName: string, options?: EngineDeleteOptions): Promise<any>;
// AI / Vector Search (optional)
vectorFind?(objectName: string, vector: number[], options?: {
where?: any; limit?: number; fields?: string[]; threshold?: number;
}): Promise<any[]>;
// Batch Operations (optional, transactional)
batch?(requests: DataEngineRequest[], options?: { transaction?: boolean }): Promise<any[]>;
// Raw Command Escape Hatch (optional)
execute?(command: any, options?: Record<string, any>): Promise<any>;
}All query methods use canonical QueryAST parameter names: where, fields, orderBy, limit, offset, expand.
Executes a structured query with filtering, sorting, pagination, and field selection. Returns an array of records.
const tasks = await engine.find('task', {
where: {
status: 'open',
priority: { $in: ['high', 'critical'] },
},
orderBy: [{ field: 'due_date', order: 'asc' }],
limit: 20,
offset: 0,
fields: ['id', 'title', 'status', 'priority', 'due_date'],
});Defined by EngineQueryOptionsSchema in @objectstack/spec:
interface EngineQueryOptions {
where?: FilterCondition; // WHERE clause — MongoDB-style $op
fields?: FieldNode[]; // SELECT fields
orderBy?: SortNode[]; // ORDER BY
limit?: number; // LIMIT
offset?: number; // OFFSET
top?: number; // Alias for limit (OData compat)
cursor?: Record<string, unknown>; // Keyset pagination
search?: FullTextSearch; // Full-text search
expand?: Record<string, QueryAST>; // Recursive relation loading
distinct?: boolean; // SELECT DISTINCT
context?: ExecutionContext; // Identity, tenant, transaction
}Filters use the canonical where + MongoDB-style $op object syntax from FilterConditionSchema:
// Implicit equality
where: { status: 'active' }
// Explicit operators
where: { amount: { $gt: 50000 } }
// Logical combinations
where: {
$and: [
{ status: 'open' },
{ priority: { $in: ['high', 'critical'] } },
],
}
// Logical OR
where: {
$or: [
{ role: 'admin' },
{ email: { $contains: '@company.com' } },
],
}
// Nested relation filter
where: {
account: { industry: 'tech' },
}Supported operators: $eq, $ne, $gt, $gte, $lt, $lte, $in, $nin, $between, $contains, $notContains, $startsWith, $endsWith, $null, $exists
Logical operators: $and, $or, $not
Convenience method that returns the first record matching a query, or null.
const task = await engine.findOne('task', {
where: { title: 'Implement login page' },
fields: ['id', 'title', 'status'],
});Returns the number of records matching a filter without fetching data.
const openTasks = await engine.count('task', {
where: { status: 'open' },
});
// 23Creates one or more records. Returns the inserted record(s) with system-generated fields (id, created_at, etc.).
const task = await engine.insert('task', {
title: 'Implement login page',
status: 'open',
priority: 'high',
assigned_to: 'usr_01HQ3V5K8N2M4P6R7T9W',
});
console.log(task.id); // "tsk_01HQ4A7B9D3F5G8J2K4L"
console.log(task.created_at); // "2025-01-20T10:30:00.000Z"Bulk insert:
const tasks = await engine.insert('task', [
{ title: 'Task 1', status: 'open', priority: 'high' },
{ title: 'Task 2', status: 'open', priority: 'medium' },
{ title: 'Task 3', status: 'open', priority: 'low' },
]);interface DataEngineInsertOptions {
returning?: boolean; // Return inserted record(s)? Default: true
context?: ExecutionContext; // Identity, tenant, transaction
}Updates specific fields on matched record(s). The where clause in options identifies target records.
const updated = await engine.update('task',
{ status: 'in_progress', estimated_hours: 12 },
{ where: { id: 'tsk_01HQ4A7B9D3F5G8J2K4L' } }
);interface EngineUpdateOptions {
where?: FilterCondition; // Filter to identify records (WHERE)
upsert?: boolean; // Insert if not found? Default: false
multi?: boolean; // Update multiple records? Default: false
returning?: boolean; // Return updated record(s)? Default: false
context?: ExecutionContext;
}The write methods (insert / update) additionally accept an in-process
onFieldsDropped listener. The engine invokes it when caller-supplied write
fields are legally stripped from the payload before the driver write — static
readonly fields or a TRUE readonlyWhen predicate. The write still succeeds;
the listener exists so callers that report per-field success (e.g. the flow
engine's update_record step) can surface a warning instead of a silent
success.
interface WriteObservabilityOptions {
onFieldsDropped?: (event: DroppedFieldsEvent) => void;
}
interface DroppedFieldsEvent {
object: string; // resolved object name
fields: string[]; // caller-supplied fields that were dropped
reason: 'readonly' | 'readonly_when'; // why they were dropped
}Deletes record(s) matching the where condition.
await engine.delete('task', {
where: { id: 'tsk_01HQ4A7B9D3F5G8J2K4L' },
});interface EngineDeleteOptions {
where?: FilterCondition; // Filter to identify records (WHERE)
multi?: boolean; // Delete multiple records? Default: false
context?: ExecutionContext;
}Runs aggregation queries with grouping for analytics and reporting.
const result = await engine.aggregate('task', {
where: { project: 'prj_01HQ3V5K8N' },
groupBy: ['status'],
aggregations: [
{ function: 'count', alias: 'task_count' },
{ function: 'sum', field: 'estimated_hours', alias: 'total_hours' },
{ function: 'avg', field: 'estimated_hours', alias: 'avg_hours' },
],
});
// result:
// [
// { status: 'open', task_count: 15, total_hours: 120, avg_hours: 8 },
// { status: 'in_progress', task_count: 8, total_hours: 96, avg_hours: 12 },
// { status: 'done', task_count: 24, total_hours: 192, avg_hours: 8 },
// ]interface EngineAggregateOptions {
where?: FilterCondition; // Pre-aggregation filter (WHERE)
groupBy?: string[]; // GROUP BY fields
aggregations?: AggregationNode[]; // Aggregation definitions
context?: ExecutionContext;
}
interface AggregationNode {
function: 'count' | 'sum' | 'avg' | 'min' | 'max'
| 'count_distinct' | 'array_agg' | 'string_agg';
field?: string; // Field to aggregate (optional for COUNT(*))
alias: string; // Result column alias
distinct?: boolean; // Apply DISTINCT before aggregation
filter?: FilterCondition; // Per-aggregation FILTER WHERE
}Perform similarity search using vector embeddings:
const results = await engine.vectorFind?.('document', embeddingVector, {
where: { category: 'technical' },
fields: ['title', 'content'],
limit: 5,
threshold: 0.8,
});Execute multiple operations in a single transaction:
const results = await engine.batch?.([
{ method: 'insert', object: 'project', data: { name: 'Website Redesign', status: 'active' } },
{ method: 'insert', object: 'task', data: { title: 'Design mockups', status: 'open' } },
{ method: 'insert', object: 'task', data: { title: 'Implement frontend', status: 'open' } },
], { transaction: true });Escape hatch for raw driver-specific commands:
const result = await engine.execute?.(
'SELECT * FROM tasks WHERE status = $1',
{ params: ['open'] }
);| Code | HTTP | Description |
|---|---|---|
RECORD_NOT_FOUND |
404 | No record exists with the given ID |
VALIDATION_FAILED |
400 | Data does not match the object schema |
DELETE_RESTRICTED |
409 | Cannot delete; dependent child records reference it via a restrict delete behavior |
**Deprecated Parameter Names**
The following legacy parameter names are accepted by the RPC layer for backward compatibility but should not be used in new code. The protocol normalizer resolves conflicts with canonical names taking precedence.
| Legacy (Deprecated) | Canonical | Notes |
|---|---|---|
filter |
where |
FilterCondition object |
select |
fields |
Array of FieldNode |
sort |
orderBy |
Array of { field, order } |
skip |
offset |
Number |
populate |
expand |
Record of field → QueryAST |
The deprecated DataEngineQueryOptionsSchema, DataEngineUpdateOptionsSchema, DataEngineDeleteOptionsSchema, and DataEngineAggregateOptionsSchema are maintained in @objectstack/spec for backward compatibility but will be removed in a future major version. Migrate to the QueryAST-aligned equivalents: EngineQueryOptionsSchema, EngineUpdateOptionsSchema, EngineDeleteOptionsSchema, EngineAggregateOptionsSchema.