Skip to content

Commit 27be77f

Browse files
authored
Merge pull request #351 from objectstack-ai/copilot/adjust-objectql-for-dataengine
2 parents caf6ff7 + a2199cc commit 27be77f

File tree

14 files changed

+486
-139
lines changed

14 files changed

+486
-139
lines changed
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
---
2+
title: Data Engine
3+
description: Data Engine protocol schemas
4+
---
5+
6+
# Data Engine
7+
8+
<Callout type="info">
9+
**Source:** `packages/spec/src/system/data-engine.zod.ts`
10+
</Callout>
11+
12+
## TypeScript Usage
13+
14+
```typescript
15+
import { DataEngineSchema, DataEngineFilterSchema, DataEngineQueryOptionsSchema } from '@objectstack/spec/system';
16+
import type { DataEngine, DataEngineFilter, DataEngineQueryOptions } from '@objectstack/spec/system';
17+
18+
// Validate data
19+
const result = DataEngineSchema.parse(data);
20+
```
21+
22+
---
23+
24+
## DataEngine
25+
26+
Data Engine Interface
27+
28+
### Properties
29+
30+
| Property | Type | Required | Description |
31+
| :--- | :--- | :--- | :--- |
32+
33+
---
34+
35+
## DataEngineFilter
36+
37+
Data Engine query filter conditions
38+
39+
---
40+
41+
## DataEngineQueryOptions
42+
43+
Query options for IDataEngine.find() operations
44+
45+
### Properties
46+
47+
| Property | Type | Required | Description |
48+
| :--- | :--- | :--- | :--- |
49+
| **filter** | `Record<string, any>` | optional | Data Engine query filter conditions |
50+
| **select** | `string[]` | optional | |
51+
| **sort** | `Record<string, Enum<'1' \| '-1' \| 'asc' \| 'desc'>>` | optional | |
52+
| **limit** | `number` | optional | |
53+
| **skip** | `number` | optional | |
54+
| **top** | `number` | optional | |
55+

content/docs/references/system/index.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ This section contains all protocol schemas for the system layer of ObjectStack.
1010
<Cards>
1111
<Card href="./audit" title="Audit" description="Source: packages/spec/src/system/audit.zod.ts" />
1212
<Card href="./context" title="Context" description="Source: packages/spec/src/system/context.zod.ts" />
13+
<Card href="./data-engine" title="Data Engine" description="Source: packages/spec/src/system/data-engine.zod.ts" />
1314
<Card href="./datasource" title="Datasource" description="Source: packages/spec/src/system/datasource.zod.ts" />
1415
<Card href="./driver" title="Driver" description="Source: packages/spec/src/system/driver.zod.ts" />
1516
<Card href="./events" title="Events" description="Source: packages/spec/src/system/events.zod.ts" />

content/docs/references/system/meta.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"pages": [
44
"audit",
55
"context",
6+
"data-engine",
67
"datasource",
78
"driver",
89
"events",

packages/objectql/src/index.ts

Lines changed: 72 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { QueryAST, HookContext } from '@objectstack/spec/data';
22
import { ObjectStackManifest } from '@objectstack/spec/system';
33
import { DriverInterface, DriverOptions } from '@objectstack/spec/system';
4+
import { IDataEngine, DataEngineQueryOptions } from '@objectstack/spec/system';
45
import { SchemaRegistry } from './registry';
56

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

2122
/**
2223
* ObjectQL Engine
24+
*
25+
* Implements the IDataEngine interface for data persistence.
2326
*/
24-
export class ObjectQL {
27+
export class ObjectQL implements IDataEngine {
2528
private drivers = new Map<string, DriverInterface>();
2629
private defaultDriver: string | null = null;
2730

@@ -211,27 +214,57 @@ export class ObjectQL {
211214
}
212215

213216
// ============================================
214-
// Data Access Methods
217+
// Data Access Methods (IDataEngine Interface)
215218
// ============================================
216219

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

220-
// Normalize QueryAST
221-
let ast: QueryAST;
222-
if (query.where || query.fields || query.orderBy || query.limit) {
223-
ast = { object, ...query } as QueryAST;
224-
} else {
225-
ast = { object, where: query } as QueryAST;
230+
// Convert DataEngineQueryOptions to QueryAST
231+
let ast: QueryAST = { object };
232+
233+
if (query) {
234+
// Map DataEngineQueryOptions to QueryAST
235+
if (query.filter) {
236+
ast.where = query.filter;
237+
}
238+
if (query.select) {
239+
ast.fields = query.select;
240+
}
241+
if (query.sort) {
242+
// Convert sort Record to orderBy array
243+
// sort: { createdAt: -1, name: 'asc' } => orderBy: [{ field: 'createdAt', order: 'desc' }, { field: 'name', order: 'asc' }]
244+
ast.orderBy = Object.entries(query.sort).map(([field, order]) => ({
245+
field,
246+
order: (order === -1 || order === 'desc') ? 'desc' : 'asc'
247+
}));
248+
}
249+
// Handle both limit and top (top takes precedence)
250+
if (query.top !== undefined) {
251+
ast.limit = query.top;
252+
} else if (query.limit !== undefined) {
253+
ast.limit = query.limit;
254+
}
255+
if (query.skip !== undefined) {
256+
ast.offset = query.skip;
257+
}
226258
}
227259

260+
// Set default limit if not specified
228261
if (ast.limit === undefined) ast.limit = 100;
229262

230263
// Trigger Before Hook
231264
const hookContext: HookContext = {
232265
object,
233266
event: 'beforeFind',
234-
input: { ast, options }, // Hooks can modify AST here
267+
input: { ast, options: undefined },
235268
ql: this
236269
};
237270
await this.triggerHooks('beforeFind', hookContext);
@@ -275,7 +308,14 @@ export class ObjectQL {
275308
return driver.findOne(object, ast, options);
276309
}
277310

278-
async insert(object: string, data: Record<string, any>, options?: DriverOptions) {
311+
/**
312+
* Insert a new record (IDataEngine interface)
313+
*
314+
* @param object - Object name
315+
* @param data - Data to insert
316+
* @returns Promise resolving to the created record
317+
*/
318+
async insert(object: string, data: any): Promise<any> {
279319
const driver = this.getDriver(object);
280320

281321
// 1. Get Schema
@@ -290,7 +330,7 @@ export class ObjectQL {
290330
const hookContext: HookContext = {
291331
object,
292332
event: 'beforeInsert',
293-
input: { data, options },
333+
input: { data, options: undefined },
294334
ql: this
295335
};
296336
await this.triggerHooks('beforeInsert', hookContext);
@@ -306,13 +346,21 @@ export class ObjectQL {
306346
return hookContext.result;
307347
}
308348

309-
async update(object: string, id: string | number, data: Record<string, any>, options?: DriverOptions) {
349+
/**
350+
* Update a record by ID (IDataEngine interface)
351+
*
352+
* @param object - Object name
353+
* @param id - Record ID
354+
* @param data - Updated data
355+
* @returns Promise resolving to the updated record
356+
*/
357+
async update(object: string, id: any, data: any): Promise<any> {
310358
const driver = this.getDriver(object);
311359

312360
const hookContext: HookContext = {
313361
object,
314362
event: 'beforeUpdate',
315-
input: { id, data, options },
363+
input: { id, data, options: undefined },
316364
ql: this
317365
};
318366
await this.triggerHooks('beforeUpdate', hookContext);
@@ -326,13 +374,20 @@ export class ObjectQL {
326374
return hookContext.result;
327375
}
328376

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

332387
const hookContext: HookContext = {
333388
object,
334389
event: 'beforeDelete',
335-
input: { id, options },
390+
input: { id, options: undefined },
336391
ql: this
337392
};
338393
await this.triggerHooks('beforeDelete', hookContext);
@@ -343,6 +398,7 @@ export class ObjectQL {
343398
hookContext.result = result;
344399
await this.triggerHooks('afterDelete', hookContext);
345400

401+
// Driver.delete() already returns boolean per DriverInterface spec
346402
return hookContext.result;
347403
}
348404
}
Lines changed: 3 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -1,122 +1,8 @@
11
/**
22
* IDataEngine - Standard Data Engine Interface
33
*
4-
* Abstract interface for data persistence capabilities.
5-
* This allows plugins to interact with data engines without knowing
6-
* the underlying implementation (SQL, MongoDB, Memory, etc.).
7-
*
8-
* Follows Dependency Inversion Principle - plugins depend on this interface,
9-
* not on concrete database implementations.
10-
*/
11-
12-
/**
13-
* Query filter conditions
14-
*/
15-
export interface QueryFilter {
16-
[field: string]: any;
17-
}
18-
19-
/**
20-
* Query options for find operations
4+
* Re-exports the data engine interface from the spec package.
5+
* This provides backward compatibility for imports from @objectstack/runtime.
216
*/
22-
export interface QueryOptions {
23-
/** Filter conditions */
24-
filter?: QueryFilter;
25-
/** Fields to select */
26-
select?: string[];
27-
/** Sort order */
28-
sort?: Record<string, 1 | -1 | 'asc' | 'desc'>;
29-
/** Limit number of results (alternative name for top, used by some drivers) */
30-
limit?: number;
31-
/** Skip number of results (for pagination) */
32-
skip?: number;
33-
/** Maximum number of results (OData-style, takes precedence over limit if both specified) */
34-
top?: number;
35-
}
367

37-
/**
38-
* IDataEngine - Data persistence capability interface
39-
*
40-
* Defines the contract for data engine implementations.
41-
* Concrete implementations (ObjectQL, Prisma, TypeORM) should implement this interface.
42-
*/
43-
export interface IDataEngine {
44-
/**
45-
* Insert a new record
46-
*
47-
* @param objectName - Name of the object/table (e.g., 'user', 'order')
48-
* @param data - Data to insert
49-
* @returns Promise resolving to the created record (including generated ID)
50-
*
51-
* @example
52-
* ```ts
53-
* const user = await engine.insert('user', {
54-
* name: 'John Doe',
55-
* email: 'john@example.com'
56-
* });
57-
* console.log(user.id); // Auto-generated ID
58-
* ```
59-
*/
60-
insert(objectName: string, data: any): Promise<any>;
61-
62-
/**
63-
* Find records matching a query
64-
*
65-
* @param objectName - Name of the object/table
66-
* @param query - Query conditions (optional)
67-
* @returns Promise resolving to an array of matching records
68-
*
69-
* @example
70-
* ```ts
71-
* // Find all users
72-
* const allUsers = await engine.find('user');
73-
*
74-
* // Find with filter
75-
* const activeUsers = await engine.find('user', {
76-
* filter: { status: 'active' }
77-
* });
78-
*
79-
* // Find with limit and sort
80-
* const recentUsers = await engine.find('user', {
81-
* sort: { createdAt: -1 },
82-
* limit: 10
83-
* });
84-
* ```
85-
*/
86-
find(objectName: string, query?: QueryOptions): Promise<any[]>;
87-
88-
/**
89-
* Update a record by ID
90-
*
91-
* @param objectName - Name of the object/table
92-
* @param id - Record ID
93-
* @param data - Updated data (partial update)
94-
* @returns Promise resolving to the updated record
95-
*
96-
* @example
97-
* ```ts
98-
* const updatedUser = await engine.update('user', '123', {
99-
* name: 'Jane Doe',
100-
* email: 'jane@example.com'
101-
* });
102-
* ```
103-
*/
104-
update(objectName: string, id: any, data: any): Promise<any>;
105-
106-
/**
107-
* Delete a record by ID
108-
*
109-
* @param objectName - Name of the object/table
110-
* @param id - Record ID
111-
* @returns Promise resolving to true if deleted, false otherwise
112-
*
113-
* @example
114-
* ```ts
115-
* const deleted = await engine.delete('user', '123');
116-
* if (deleted) {
117-
* console.log('User deleted successfully');
118-
* }
119-
* ```
120-
*/
121-
delete(objectName: string, id: any): Promise<boolean>;
122-
}
8+
export type { IDataEngine, DataEngineQueryOptions, DataEngineFilter } from '@objectstack/spec/system';

packages/runtime/src/objectql-plugin.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,13 @@ export class ObjectQLPlugin implements Plugin {
4444
* Init phase - Register ObjectQL as a service
4545
*/
4646
async init(ctx: PluginContext) {
47-
// Register ObjectQL engine as a service
47+
// Register ObjectQL engine as 'objectql' service (legacy name)
4848
ctx.registerService('objectql', this.ql);
49-
ctx.logger.log('[ObjectQLPlugin] ObjectQL engine registered as service');
49+
50+
// Register ObjectQL engine as 'data-engine' service (IDataEngine interface)
51+
ctx.registerService('data-engine', this.ql);
52+
53+
ctx.logger.log('[ObjectQLPlugin] ObjectQL engine registered as services: objectql, data-engine');
5054
}
5155

5256
/**

packages/runtime/src/test-interfaces.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* and IDataEngine interfaces without depending on concrete implementations.
66
*/
77

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

1010
/**
1111
* Example: Mock HTTP Server Plugin
@@ -77,7 +77,7 @@ class MockDataEngine implements IDataEngine {
7777
return record;
7878
}
7979

80-
async find(objectName: string, query?: QueryOptions): Promise<any[]> {
80+
async find(objectName: string, query?: DataEngineQueryOptions): Promise<any[]> {
8181
const objectStore = this.store.get(objectName);
8282
if (!objectStore) {
8383
return [];

0 commit comments

Comments
 (0)