Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions content/docs/references/system/data-engine.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
---
title: Data Engine
description: Data Engine protocol schemas
---

# Data Engine

<Callout type="info">
**Source:** `packages/spec/src/system/data-engine.zod.ts`
</Callout>

## TypeScript Usage

```typescript
import { DataEngineSchema, DataEngineFilterSchema, DataEngineQueryOptionsSchema } from '@objectstack/spec/system';
import type { DataEngine, DataEngineFilter, DataEngineQueryOptions } from '@objectstack/spec/system';
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation references a type DataEngine that doesn't exist in the schema file. The schema file exports DataEngineSchema but doesn't export a corresponding type derived from it using z.infer.

Following the established pattern in other schema files (like driver.zod.ts which exports export type DriverInterface = z.infer<typeof DriverInterfaceSchema>), you should add:

export type DataEngine = z.infer<typeof DataEngineSchema>;

after line 98 in the schema file. However, note that the manually defined IDataEngine interface (lines 102-107) serves the same purpose but with better type safety for optional parameters. Consider whether you need both or if the documentation should reference IDataEngine instead.

Copilot uses AI. Check for mistakes.

// Validate data
const result = DataEngineSchema.parse(data);
```

---

## DataEngine

Data Engine Interface

### Properties

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |

---

## DataEngineFilter

Data Engine query filter conditions

---

## DataEngineQueryOptions

Query options for IDataEngine.find() operations

### Properties

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **filter** | `Record<string, any>` | optional | Data Engine query filter conditions |
| **select** | `string[]` | optional | |
| **sort** | `Record<string, Enum<'1' \| '-1' \| 'asc' \| 'desc'>>` | optional | |
| **limit** | `number` | optional | |
| **skip** | `number` | optional | |
| **top** | `number` | optional | |

1 change: 1 addition & 0 deletions content/docs/references/system/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ This section contains all protocol schemas for the system layer of ObjectStack.
<Cards>
<Card href="./audit" title="Audit" description="Source: packages/spec/src/system/audit.zod.ts" />
<Card href="./context" title="Context" description="Source: packages/spec/src/system/context.zod.ts" />
<Card href="./data-engine" title="Data Engine" description="Source: packages/spec/src/system/data-engine.zod.ts" />
<Card href="./datasource" title="Datasource" description="Source: packages/spec/src/system/datasource.zod.ts" />
<Card href="./driver" title="Driver" description="Source: packages/spec/src/system/driver.zod.ts" />
<Card href="./events" title="Events" description="Source: packages/spec/src/system/events.zod.ts" />
Expand Down
1 change: 1 addition & 0 deletions content/docs/references/system/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"pages": [
"audit",
"context",
"data-engine",
"datasource",
"driver",
"events",
Expand Down
88 changes: 72 additions & 16 deletions packages/objectql/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { QueryAST, HookContext } from '@objectstack/spec/data';
import { ObjectStackManifest } from '@objectstack/spec/system';
import { DriverInterface, DriverOptions } from '@objectstack/spec/system';
import { IDataEngine, DataEngineQueryOptions } from '@objectstack/spec/system';
import { SchemaRegistry } from './registry';

// Export Registry for consumers
Expand All @@ -20,8 +21,10 @@ export interface PluginContext {

/**
* ObjectQL Engine
*
* Implements the IDataEngine interface for data persistence.
*/
export class ObjectQL {
export class ObjectQL implements IDataEngine {
private drivers = new Map<string, DriverInterface>();
private defaultDriver: string | null = null;

Expand Down Expand Up @@ -211,27 +214,57 @@ export class ObjectQL {
}

// ============================================
// Data Access Methods
// Data Access Methods (IDataEngine Interface)
// ============================================

async find(object: string, query: any = {}, options?: DriverOptions) {
/**
* Find records matching a query (IDataEngine interface)
*
* @param object - Object name
* @param query - Query options (IDataEngine format)
* @returns Promise resolving to array of records
*/
async find(object: string, query?: DataEngineQueryOptions): Promise<any[]> {
const driver = this.getDriver(object);

// Normalize QueryAST
let ast: QueryAST;
if (query.where || query.fields || query.orderBy || query.limit) {
ast = { object, ...query } as QueryAST;
} else {
ast = { object, where: query } as QueryAST;
// Convert DataEngineQueryOptions to QueryAST
let ast: QueryAST = { object };

if (query) {
// Map DataEngineQueryOptions to QueryAST
if (query.filter) {
ast.where = query.filter;
}
if (query.select) {
ast.fields = query.select;
}
if (query.sort) {
// Convert sort Record to orderBy array
// sort: { createdAt: -1, name: 'asc' } => orderBy: [{ field: 'createdAt', order: 'desc' }, { field: 'name', order: 'asc' }]
ast.orderBy = Object.entries(query.sort).map(([field, order]) => ({
field,
order: (order === -1 || order === 'desc') ? 'desc' : 'asc'
}));
}
// Handle both limit and top (top takes precedence)
if (query.top !== undefined) {
ast.limit = query.top;
} else if (query.limit !== undefined) {
ast.limit = query.limit;
}
if (query.skip !== undefined) {
ast.offset = query.skip;
}
}

// Set default limit if not specified
if (ast.limit === undefined) ast.limit = 100;

// Trigger Before Hook
const hookContext: HookContext = {
object,
event: 'beforeFind',
input: { ast, options }, // Hooks can modify AST here
input: { ast, options: undefined },
ql: this
};
await this.triggerHooks('beforeFind', hookContext);
Expand Down Expand Up @@ -275,7 +308,14 @@ export class ObjectQL {
return driver.findOne(object, ast, options);
}

async insert(object: string, data: Record<string, any>, options?: DriverOptions) {
/**
* Insert a new record (IDataEngine interface)
*
* @param object - Object name
* @param data - Data to insert
* @returns Promise resolving to the created record
*/
async insert(object: string, data: any): Promise<any> {
const driver = this.getDriver(object);

// 1. Get Schema
Expand All @@ -290,7 +330,7 @@ export class ObjectQL {
const hookContext: HookContext = {
object,
event: 'beforeInsert',
input: { data, options },
input: { data, options: undefined },
ql: this
};
await this.triggerHooks('beforeInsert', hookContext);
Expand All @@ -306,13 +346,21 @@ export class ObjectQL {
return hookContext.result;
}

async update(object: string, id: string | number, data: Record<string, any>, options?: DriverOptions) {
/**
* Update a record by ID (IDataEngine interface)
*
* @param object - Object name
* @param id - Record ID
* @param data - Updated data
* @returns Promise resolving to the updated record
*/
async update(object: string, id: any, data: any): Promise<any> {
const driver = this.getDriver(object);

const hookContext: HookContext = {
object,
event: 'beforeUpdate',
input: { id, data, options },
input: { id, data, options: undefined },
ql: this
};
await this.triggerHooks('beforeUpdate', hookContext);
Expand All @@ -326,13 +374,20 @@ export class ObjectQL {
return hookContext.result;
}

async delete(object: string, id: string | number, options?: DriverOptions) {
/**
* Delete a record by ID (IDataEngine interface)
*
* @param object - Object name
* @param id - Record ID
* @returns Promise resolving to true if deleted, false otherwise
*/
async delete(object: string, id: any): Promise<boolean> {
const driver = this.getDriver(object);

const hookContext: HookContext = {
object,
event: 'beforeDelete',
input: { id, options },
input: { id, options: undefined },
ql: this
};
await this.triggerHooks('beforeDelete', hookContext);
Expand All @@ -343,6 +398,7 @@ export class ObjectQL {
hookContext.result = result;
await this.triggerHooks('afterDelete', hookContext);

// Driver.delete() already returns boolean per DriverInterface spec
return hookContext.result;
}
}
120 changes: 3 additions & 117 deletions packages/runtime/src/interfaces/data-engine.ts
Original file line number Diff line number Diff line change
@@ -1,122 +1,8 @@
/**
* IDataEngine - Standard Data Engine Interface
*
* Abstract interface for data persistence capabilities.
* This allows plugins to interact with data engines without knowing
* the underlying implementation (SQL, MongoDB, Memory, etc.).
*
* Follows Dependency Inversion Principle - plugins depend on this interface,
* not on concrete database implementations.
*/

/**
* Query filter conditions
*/
export interface QueryFilter {
[field: string]: any;
}

/**
* Query options for find operations
* Re-exports the data engine interface from the spec package.
* This provides backward compatibility for imports from @objectstack/runtime.
*/
export interface QueryOptions {
/** Filter conditions */
filter?: QueryFilter;
/** Fields to select */
select?: string[];
/** Sort order */
sort?: Record<string, 1 | -1 | 'asc' | 'desc'>;
/** Limit number of results (alternative name for top, used by some drivers) */
limit?: number;
/** Skip number of results (for pagination) */
skip?: number;
/** Maximum number of results (OData-style, takes precedence over limit if both specified) */
top?: number;
}

/**
* IDataEngine - Data persistence capability interface
*
* Defines the contract for data engine implementations.
* Concrete implementations (ObjectQL, Prisma, TypeORM) should implement this interface.
*/
export interface IDataEngine {
/**
* Insert a new record
*
* @param objectName - Name of the object/table (e.g., 'user', 'order')
* @param data - Data to insert
* @returns Promise resolving to the created record (including generated ID)
*
* @example
* ```ts
* const user = await engine.insert('user', {
* name: 'John Doe',
* email: 'john@example.com'
* });
* console.log(user.id); // Auto-generated ID
* ```
*/
insert(objectName: string, data: any): Promise<any>;

/**
* Find records matching a query
*
* @param objectName - Name of the object/table
* @param query - Query conditions (optional)
* @returns Promise resolving to an array of matching records
*
* @example
* ```ts
* // Find all users
* const allUsers = await engine.find('user');
*
* // Find with filter
* const activeUsers = await engine.find('user', {
* filter: { status: 'active' }
* });
*
* // Find with limit and sort
* const recentUsers = await engine.find('user', {
* sort: { createdAt: -1 },
* limit: 10
* });
* ```
*/
find(objectName: string, query?: QueryOptions): Promise<any[]>;

/**
* Update a record by ID
*
* @param objectName - Name of the object/table
* @param id - Record ID
* @param data - Updated data (partial update)
* @returns Promise resolving to the updated record
*
* @example
* ```ts
* const updatedUser = await engine.update('user', '123', {
* name: 'Jane Doe',
* email: 'jane@example.com'
* });
* ```
*/
update(objectName: string, id: any, data: any): Promise<any>;

/**
* Delete a record by ID
*
* @param objectName - Name of the object/table
* @param id - Record ID
* @returns Promise resolving to true if deleted, false otherwise
*
* @example
* ```ts
* const deleted = await engine.delete('user', '123');
* if (deleted) {
* console.log('User deleted successfully');
* }
* ```
*/
delete(objectName: string, id: any): Promise<boolean>;
}
export type { IDataEngine, DataEngineQueryOptions, DataEngineFilter } from '@objectstack/spec/system';
8 changes: 6 additions & 2 deletions packages/runtime/src/objectql-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,13 @@ export class ObjectQLPlugin implements Plugin {
* Init phase - Register ObjectQL as a service
*/
async init(ctx: PluginContext) {
// Register ObjectQL engine as a service
// Register ObjectQL engine as 'objectql' service (legacy name)
ctx.registerService('objectql', this.ql);
ctx.logger.log('[ObjectQLPlugin] ObjectQL engine registered as service');

// Register ObjectQL engine as 'data-engine' service (IDataEngine interface)
ctx.registerService('data-engine', this.ql);

ctx.logger.log('[ObjectQLPlugin] ObjectQL engine registered as services: objectql, data-engine');
}

/**
Expand Down
4 changes: 2 additions & 2 deletions packages/runtime/src/test-interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* and IDataEngine interfaces without depending on concrete implementations.
*/

import { IHttpServer, IDataEngine, RouteHandler, IHttpRequest, IHttpResponse, Middleware, QueryOptions } from './index.js';
import { IHttpServer, IDataEngine, RouteHandler, IHttpRequest, IHttpResponse, Middleware, DataEngineQueryOptions } from './index.js';

Check notice

Code scanning / CodeQL

Unused variable, import, function or class Note test

Unused imports IHttpRequest, IHttpResponse.

Copilot Autofix

AI about 8 hours ago

To fix the problem, we should remove the unused imports IHttpRequest and IHttpResponse from the import list in packages/runtime/src/test-interfaces.ts. This eliminates the unused program elements without changing any runtime or type-checking behavior, since they are not referenced elsewhere in the file.

Concretely, in packages/runtime/src/test-interfaces.ts, edit the import on line 8 so that only the actually used symbols remain. Replace:

import { IHttpServer, IDataEngine, RouteHandler, IHttpRequest, IHttpResponse, Middleware, DataEngineQueryOptions } from './index.js';

with:

import { IHttpServer, IDataEngine, RouteHandler, Middleware, DataEngineQueryOptions } from './index.js';

No additional methods, imports, or definitions are needed. This is a minimal change that only removes unused imports and preserves all existing functionality.

Suggested changeset 1
packages/runtime/src/test-interfaces.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/packages/runtime/src/test-interfaces.ts b/packages/runtime/src/test-interfaces.ts
--- a/packages/runtime/src/test-interfaces.ts
+++ b/packages/runtime/src/test-interfaces.ts
@@ -5,7 +5,7 @@
  * and IDataEngine interfaces without depending on concrete implementations.
  */
 
-import { IHttpServer, IDataEngine, RouteHandler, IHttpRequest, IHttpResponse, Middleware, DataEngineQueryOptions } from './index.js';
+import { IHttpServer, IDataEngine, RouteHandler, Middleware, DataEngineQueryOptions } from './index.js';
 
 /**
  * Example: Mock HTTP Server Plugin
EOF
@@ -5,7 +5,7 @@
* and IDataEngine interfaces without depending on concrete implementations.
*/

import { IHttpServer, IDataEngine, RouteHandler, IHttpRequest, IHttpResponse, Middleware, DataEngineQueryOptions } from './index.js';
import { IHttpServer, IDataEngine, RouteHandler, Middleware, DataEngineQueryOptions } from './index.js';

/**
* Example: Mock HTTP Server Plugin
Copilot is powered by AI and may make mistakes. Always verify output.
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused imports IHttpRequest, IHttpResponse.

Suggested change
import { IHttpServer, IDataEngine, RouteHandler, IHttpRequest, IHttpResponse, Middleware, DataEngineQueryOptions } from './index.js';
import { IHttpServer, IDataEngine, RouteHandler, Middleware, DataEngineQueryOptions } from './index.js';

Copilot uses AI. Check for mistakes.

/**
* Example: Mock HTTP Server Plugin
Expand Down Expand Up @@ -77,7 +77,7 @@
return record;
}

async find(objectName: string, query?: QueryOptions): Promise<any[]> {
async find(objectName: string, query?: DataEngineQueryOptions): Promise<any[]> {
const objectStore = this.store.get(objectName);
if (!objectStore) {
return [];
Expand Down
Loading
Loading