@objectstack/objectql 0.4.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/README.md +24 -2
- package/dist/index.d.ts +42 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +105 -47
- package/dist/plugin.d.ts +14 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +54 -0
- package/dist/protocol.d.ts +43 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +87 -0
- package/package.json +4 -2
- package/src/index.ts +115 -53
- package/src/plugin.ts +62 -0
- package/src/protocol.ts +104 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# @objectstack/objectql
|
|
2
2
|
|
|
3
|
+
## 0.6.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- b2df5f7: Unified version bump to 0.5.0
|
|
8
|
+
|
|
9
|
+
- Standardized all package versions to 0.5.0 across the monorepo
|
|
10
|
+
- Fixed driver-memory package.json paths for proper module resolution
|
|
11
|
+
- Ensured all packages are in sync for the 0.5.0 release
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- Updated dependencies [b2df5f7]
|
|
16
|
+
- @objectstack/spec@0.6.0
|
|
17
|
+
- @objectstack/types@0.6.0
|
|
18
|
+
- @objectstack/core@0.6.0
|
|
19
|
+
|
|
20
|
+
## 0.4.2
|
|
21
|
+
|
|
22
|
+
### Patch Changes
|
|
23
|
+
|
|
24
|
+
- Unify all package versions to 0.4.2
|
|
25
|
+
- Updated dependencies
|
|
26
|
+
- @objectstack/spec@0.4.2
|
|
27
|
+
|
|
3
28
|
## 0.4.1
|
|
4
29
|
|
|
5
30
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -12,16 +12,18 @@
|
|
|
12
12
|
|
|
13
13
|
## Usage
|
|
14
14
|
|
|
15
|
+
### 1. Standalone Usage
|
|
16
|
+
|
|
15
17
|
```typescript
|
|
16
18
|
import { ObjectQL } from '@objectstack/objectql';
|
|
17
|
-
import {
|
|
19
|
+
import { InMemoryDriver } from '@objectstack/driver-memory'; // Note: Package name might export InMemoryDriver now
|
|
18
20
|
|
|
19
21
|
async function main() {
|
|
20
22
|
// 1. Initialize Engine
|
|
21
23
|
const ql = new ObjectQL();
|
|
22
24
|
|
|
23
25
|
// 2. Register Drivers
|
|
24
|
-
const memDriver = new
|
|
26
|
+
const memDriver = new InMemoryDriver({ name: 'default' });
|
|
25
27
|
ql.registerDriver(memDriver, true);
|
|
26
28
|
|
|
27
29
|
// 3. Load Schema (via Plugin/Manifest)
|
|
@@ -61,6 +63,26 @@ async function main() {
|
|
|
61
63
|
}
|
|
62
64
|
```
|
|
63
65
|
|
|
66
|
+
### 2. Using with ObjectKernel (Recommended)
|
|
67
|
+
|
|
68
|
+
When building full applications, use the `ObjectKernel` to manage plugins and configuration.
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
import { ObjectKernel } from '@objectstack/core';
|
|
72
|
+
import { ObjectQLPlugin, DriverPlugin } from '@objectstack/runtime';
|
|
73
|
+
import { InMemoryDriver } from '@objectstack/driver-memory';
|
|
74
|
+
|
|
75
|
+
const kernel = new ObjectKernel();
|
|
76
|
+
|
|
77
|
+
// Register Engine and Drivers as Kernel Plugins
|
|
78
|
+
kernel.use(new ObjectQLPlugin())
|
|
79
|
+
.use(new DriverPlugin(new InMemoryDriver(), 'default'));
|
|
80
|
+
|
|
81
|
+
await kernel.bootstrap();
|
|
82
|
+
|
|
83
|
+
// The engine automatically discovers drivers and apps registered in the kernel.
|
|
84
|
+
```
|
|
85
|
+
|
|
64
86
|
## Architecture
|
|
65
87
|
|
|
66
88
|
- **SchemaRegistry**: Central store for all metadata (Objects, Apps, Config).
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { HookContext } from '@objectstack/spec/data';
|
|
2
|
-
import {
|
|
2
|
+
import { DriverOptions } from '@objectstack/spec/system';
|
|
3
|
+
import { DriverInterface, IDataEngine, DataEngineQueryOptions } from '@objectstack/core';
|
|
3
4
|
export { SchemaRegistry } from './registry';
|
|
5
|
+
export { ObjectStackProtocolImplementation } from './protocol';
|
|
4
6
|
export type HookHandler = (context: HookContext) => Promise<void> | void;
|
|
5
7
|
/**
|
|
6
8
|
* Host Context provided to plugins
|
|
@@ -12,8 +14,10 @@ export interface PluginContext {
|
|
|
12
14
|
}
|
|
13
15
|
/**
|
|
14
16
|
* ObjectQL Engine
|
|
17
|
+
*
|
|
18
|
+
* Implements the IDataEngine interface for data persistence.
|
|
15
19
|
*/
|
|
16
|
-
export declare class ObjectQL {
|
|
20
|
+
export declare class ObjectQL implements IDataEngine {
|
|
17
21
|
private drivers;
|
|
18
22
|
private defaultDriver;
|
|
19
23
|
private hooks;
|
|
@@ -30,6 +34,7 @@ export declare class ObjectQL {
|
|
|
30
34
|
*/
|
|
31
35
|
registerHook(event: string, handler: HookHandler): void;
|
|
32
36
|
private triggerHooks;
|
|
37
|
+
registerApp(manifestPart: any): void;
|
|
33
38
|
/**
|
|
34
39
|
* Register a new storage driver
|
|
35
40
|
*/
|
|
@@ -155,10 +160,40 @@ export declare class ObjectQL {
|
|
|
155
160
|
*/
|
|
156
161
|
init(): Promise<void>;
|
|
157
162
|
destroy(): Promise<void>;
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
+
/**
|
|
164
|
+
* Find records matching a query (IDataEngine interface)
|
|
165
|
+
*
|
|
166
|
+
* @param object - Object name
|
|
167
|
+
* @param query - Query options (IDataEngine format)
|
|
168
|
+
* @returns Promise resolving to array of records
|
|
169
|
+
*/
|
|
170
|
+
find(object: string, query?: DataEngineQueryOptions): Promise<any[]>;
|
|
171
|
+
findOne(object: string, idOrQuery: string | any, options?: DriverOptions): Promise<any>;
|
|
172
|
+
/**
|
|
173
|
+
* Insert a new record (IDataEngine interface)
|
|
174
|
+
*
|
|
175
|
+
* @param object - Object name
|
|
176
|
+
* @param data - Data to insert
|
|
177
|
+
* @returns Promise resolving to the created record
|
|
178
|
+
*/
|
|
179
|
+
insert(object: string, data: any): Promise<any>;
|
|
180
|
+
/**
|
|
181
|
+
* Update a record by ID (IDataEngine interface)
|
|
182
|
+
*
|
|
183
|
+
* @param object - Object name
|
|
184
|
+
* @param id - Record ID
|
|
185
|
+
* @param data - Updated data
|
|
186
|
+
* @returns Promise resolving to the updated record
|
|
187
|
+
*/
|
|
188
|
+
update(object: string, id: any, data: any): Promise<any>;
|
|
189
|
+
/**
|
|
190
|
+
* Delete a record by ID (IDataEngine interface)
|
|
191
|
+
*
|
|
192
|
+
* @param object - Object name
|
|
193
|
+
* @param id - Record ID
|
|
194
|
+
* @returns Promise resolving to true if deleted, false otherwise
|
|
195
|
+
*/
|
|
196
|
+
delete(object: string, id: any): Promise<boolean>;
|
|
163
197
|
}
|
|
198
|
+
export * from './plugin';
|
|
164
199
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAY,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAE/D,OAAO,EAAE,eAAe,EAAE,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { SchemaRegistry } from './registry';
|
|
2
2
|
// Export Registry for consumers
|
|
3
3
|
export { SchemaRegistry } from './registry';
|
|
4
|
+
export { ObjectStackProtocolImplementation } from './protocol';
|
|
4
5
|
/**
|
|
5
6
|
* ObjectQL Engine
|
|
7
|
+
*
|
|
8
|
+
* Implements the IDataEngine interface for data persistence.
|
|
6
9
|
*/
|
|
7
10
|
export class ObjectQL {
|
|
8
11
|
constructor(hostContext = {}) {
|
|
@@ -26,37 +29,7 @@ export class ObjectQL {
|
|
|
26
29
|
async use(manifestPart, runtimePart) {
|
|
27
30
|
// 1. Validate / Register Manifest
|
|
28
31
|
if (manifestPart) {
|
|
29
|
-
|
|
30
|
-
// If the passed object is a module namespace with a default export, use that.
|
|
31
|
-
const manifest = manifestPart.default || manifestPart;
|
|
32
|
-
// In a real scenario, we might strictly parse this using Zod
|
|
33
|
-
// For now, simple ID check
|
|
34
|
-
const id = manifest.id || manifest.name;
|
|
35
|
-
if (!id) {
|
|
36
|
-
console.warn(`[ObjectQL] Plugin manifest missing ID (keys: ${Object.keys(manifest)})`, manifest);
|
|
37
|
-
// Don't return, try to proceed if it looks like an App (Apps might use 'name' instead of 'id')
|
|
38
|
-
// return;
|
|
39
|
-
}
|
|
40
|
-
console.log(`[ObjectQL] Loading Plugin: ${id}`);
|
|
41
|
-
SchemaRegistry.registerPlugin(manifest);
|
|
42
|
-
// Register Objects from App/Plugin
|
|
43
|
-
if (manifest.objects) {
|
|
44
|
-
for (const obj of manifest.objects) {
|
|
45
|
-
// Ensure object name is registered globally
|
|
46
|
-
SchemaRegistry.registerObject(obj);
|
|
47
|
-
console.log(`[ObjectQL] Registered Object: ${obj.name}`);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
// Register contributions
|
|
51
|
-
if (manifest.contributes?.kinds) {
|
|
52
|
-
for (const kind of manifest.contributes.kinds) {
|
|
53
|
-
SchemaRegistry.registerKind(kind);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
// Register Data Seeding (Lazy execution or immediate?)
|
|
57
|
-
// We store it in a temporary registry or execute immediately if engine is ready.
|
|
58
|
-
// Since `use` is init time, we might need to store it and run later in `seed()`.
|
|
59
|
-
// For this MVP, let's attach it to the manifest object in registry so Kernel can find it.
|
|
32
|
+
this.registerApp(manifestPart);
|
|
60
33
|
}
|
|
61
34
|
// 2. Execute Runtime
|
|
62
35
|
if (runtimePart) {
|
|
@@ -94,6 +67,38 @@ export class ObjectQL {
|
|
|
94
67
|
await handler(context);
|
|
95
68
|
}
|
|
96
69
|
}
|
|
70
|
+
registerApp(manifestPart) {
|
|
71
|
+
// 1. Handle Module Imports (commonjs/esm interop)
|
|
72
|
+
// If the passed object is a module namespace with a default export, use that.
|
|
73
|
+
const raw = manifestPart.default || manifestPart;
|
|
74
|
+
// Support nested manifest property (Stack Definition)
|
|
75
|
+
// We merge the inner manifest metadata (id, version, etc) with the outer container (objects, apps)
|
|
76
|
+
const manifest = raw.manifest ? { ...raw, ...raw.manifest } : raw;
|
|
77
|
+
// In a real scenario, we might strictly parse this using Zod
|
|
78
|
+
// For now, simple ID check
|
|
79
|
+
const id = manifest.id || manifest.name;
|
|
80
|
+
if (!id) {
|
|
81
|
+
console.warn(`[ObjectQL] Plugin manifest missing ID (keys: ${Object.keys(manifest)})`, manifest);
|
|
82
|
+
// Don't return, try to proceed if it looks like an App (Apps might use 'name' instead of 'id')
|
|
83
|
+
// return;
|
|
84
|
+
}
|
|
85
|
+
console.log(`[ObjectQL] Loading App: ${id}`);
|
|
86
|
+
SchemaRegistry.registerPlugin(manifest);
|
|
87
|
+
// Register Objects from App/Plugin
|
|
88
|
+
if (manifest.objects) {
|
|
89
|
+
for (const obj of manifest.objects) {
|
|
90
|
+
// Ensure object name is registered globally
|
|
91
|
+
SchemaRegistry.registerObject(obj);
|
|
92
|
+
console.log(`[ObjectQL] Registered Object: ${obj.name}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// Register contributions
|
|
96
|
+
if (manifest.contributes?.kinds) {
|
|
97
|
+
for (const kind of manifest.contributes.kinds) {
|
|
98
|
+
SchemaRegistry.registerKind(kind);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
97
102
|
/**
|
|
98
103
|
* Register a new storage driver
|
|
99
104
|
*/
|
|
@@ -171,25 +176,54 @@ export class ObjectQL {
|
|
|
171
176
|
}
|
|
172
177
|
}
|
|
173
178
|
// ============================================
|
|
174
|
-
// Data Access Methods
|
|
179
|
+
// Data Access Methods (IDataEngine Interface)
|
|
175
180
|
// ============================================
|
|
176
|
-
|
|
181
|
+
/**
|
|
182
|
+
* Find records matching a query (IDataEngine interface)
|
|
183
|
+
*
|
|
184
|
+
* @param object - Object name
|
|
185
|
+
* @param query - Query options (IDataEngine format)
|
|
186
|
+
* @returns Promise resolving to array of records
|
|
187
|
+
*/
|
|
188
|
+
async find(object, query) {
|
|
177
189
|
const driver = this.getDriver(object);
|
|
178
|
-
//
|
|
179
|
-
let ast;
|
|
180
|
-
if (query
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
190
|
+
// Convert DataEngineQueryOptions to QueryAST
|
|
191
|
+
let ast = { object };
|
|
192
|
+
if (query) {
|
|
193
|
+
// Map DataEngineQueryOptions to QueryAST
|
|
194
|
+
if (query.filter) {
|
|
195
|
+
ast.where = query.filter;
|
|
196
|
+
}
|
|
197
|
+
if (query.select) {
|
|
198
|
+
ast.fields = query.select;
|
|
199
|
+
}
|
|
200
|
+
if (query.sort) {
|
|
201
|
+
// Convert sort Record to orderBy array
|
|
202
|
+
// sort: { createdAt: -1, name: 'asc' } => orderBy: [{ field: 'createdAt', order: 'desc' }, { field: 'name', order: 'asc' }]
|
|
203
|
+
ast.orderBy = Object.entries(query.sort).map(([field, order]) => ({
|
|
204
|
+
field,
|
|
205
|
+
order: (order === -1 || order === 'desc') ? 'desc' : 'asc'
|
|
206
|
+
}));
|
|
207
|
+
}
|
|
208
|
+
// Handle both limit and top (top takes precedence)
|
|
209
|
+
if (query.top !== undefined) {
|
|
210
|
+
ast.limit = query.top;
|
|
211
|
+
}
|
|
212
|
+
else if (query.limit !== undefined) {
|
|
213
|
+
ast.limit = query.limit;
|
|
214
|
+
}
|
|
215
|
+
if (query.skip !== undefined) {
|
|
216
|
+
ast.offset = query.skip;
|
|
217
|
+
}
|
|
185
218
|
}
|
|
219
|
+
// Set default limit if not specified
|
|
186
220
|
if (ast.limit === undefined)
|
|
187
221
|
ast.limit = 100;
|
|
188
222
|
// Trigger Before Hook
|
|
189
223
|
const hookContext = {
|
|
190
224
|
object,
|
|
191
225
|
event: 'beforeFind',
|
|
192
|
-
input: { ast, options },
|
|
226
|
+
input: { ast, options: undefined },
|
|
193
227
|
ql: this
|
|
194
228
|
};
|
|
195
229
|
await this.triggerHooks('beforeFind', hookContext);
|
|
@@ -229,7 +263,14 @@ export class ObjectQL {
|
|
|
229
263
|
ast.limit = 1;
|
|
230
264
|
return driver.findOne(object, ast, options);
|
|
231
265
|
}
|
|
232
|
-
|
|
266
|
+
/**
|
|
267
|
+
* Insert a new record (IDataEngine interface)
|
|
268
|
+
*
|
|
269
|
+
* @param object - Object name
|
|
270
|
+
* @param data - Data to insert
|
|
271
|
+
* @returns Promise resolving to the created record
|
|
272
|
+
*/
|
|
273
|
+
async insert(object, data) {
|
|
233
274
|
const driver = this.getDriver(object);
|
|
234
275
|
// 1. Get Schema
|
|
235
276
|
const schema = SchemaRegistry.getObject(object);
|
|
@@ -241,7 +282,7 @@ export class ObjectQL {
|
|
|
241
282
|
const hookContext = {
|
|
242
283
|
object,
|
|
243
284
|
event: 'beforeInsert',
|
|
244
|
-
input: { data, options },
|
|
285
|
+
input: { data, options: undefined },
|
|
245
286
|
ql: this
|
|
246
287
|
};
|
|
247
288
|
await this.triggerHooks('beforeInsert', hookContext);
|
|
@@ -253,12 +294,20 @@ export class ObjectQL {
|
|
|
253
294
|
await this.triggerHooks('afterInsert', hookContext);
|
|
254
295
|
return hookContext.result;
|
|
255
296
|
}
|
|
256
|
-
|
|
297
|
+
/**
|
|
298
|
+
* Update a record by ID (IDataEngine interface)
|
|
299
|
+
*
|
|
300
|
+
* @param object - Object name
|
|
301
|
+
* @param id - Record ID
|
|
302
|
+
* @param data - Updated data
|
|
303
|
+
* @returns Promise resolving to the updated record
|
|
304
|
+
*/
|
|
305
|
+
async update(object, id, data) {
|
|
257
306
|
const driver = this.getDriver(object);
|
|
258
307
|
const hookContext = {
|
|
259
308
|
object,
|
|
260
309
|
event: 'beforeUpdate',
|
|
261
|
-
input: { id, data, options },
|
|
310
|
+
input: { id, data, options: undefined },
|
|
262
311
|
ql: this
|
|
263
312
|
};
|
|
264
313
|
await this.triggerHooks('beforeUpdate', hookContext);
|
|
@@ -268,12 +317,19 @@ export class ObjectQL {
|
|
|
268
317
|
await this.triggerHooks('afterUpdate', hookContext);
|
|
269
318
|
return hookContext.result;
|
|
270
319
|
}
|
|
271
|
-
|
|
320
|
+
/**
|
|
321
|
+
* Delete a record by ID (IDataEngine interface)
|
|
322
|
+
*
|
|
323
|
+
* @param object - Object name
|
|
324
|
+
* @param id - Record ID
|
|
325
|
+
* @returns Promise resolving to true if deleted, false otherwise
|
|
326
|
+
*/
|
|
327
|
+
async delete(object, id) {
|
|
272
328
|
const driver = this.getDriver(object);
|
|
273
329
|
const hookContext = {
|
|
274
330
|
object,
|
|
275
331
|
event: 'beforeDelete',
|
|
276
|
-
input: { id, options },
|
|
332
|
+
input: { id, options: undefined },
|
|
277
333
|
ql: this
|
|
278
334
|
};
|
|
279
335
|
await this.triggerHooks('beforeDelete', hookContext);
|
|
@@ -281,6 +337,8 @@ export class ObjectQL {
|
|
|
281
337
|
hookContext.event = 'afterDelete';
|
|
282
338
|
hookContext.result = result;
|
|
283
339
|
await this.triggerHooks('afterDelete', hookContext);
|
|
340
|
+
// Driver.delete() already returns boolean per DriverInterface spec
|
|
284
341
|
return hookContext.result;
|
|
285
342
|
}
|
|
286
343
|
}
|
|
344
|
+
export * from './plugin';
|
package/dist/plugin.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ObjectQL } from './index';
|
|
2
|
+
import { Plugin, PluginContext } from '@objectstack/core';
|
|
3
|
+
export type { Plugin, PluginContext };
|
|
4
|
+
export declare class ObjectQLPlugin implements Plugin {
|
|
5
|
+
name: string;
|
|
6
|
+
type: "objectql";
|
|
7
|
+
version: string;
|
|
8
|
+
private ql;
|
|
9
|
+
private hostContext?;
|
|
10
|
+
constructor(ql?: ObjectQL, hostContext?: Record<string, any>);
|
|
11
|
+
init(ctx: PluginContext): Promise<void>;
|
|
12
|
+
start(ctx: PluginContext): Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=plugin.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAEnC,OAAO,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAE1D,YAAY,EAAE,MAAM,EAAE,aAAa,EAAE,CAAC;AAEtC,qBAAa,cAAe,YAAW,MAAM;IAC3C,IAAI,SAAqC;IACzC,IAAI,EAAG,UAAU,CAAU;IAC3B,OAAO,SAAW;IAElB,OAAO,CAAC,EAAE,CAAuB;IACjC,OAAO,CAAC,WAAW,CAAC,CAAsB;gBAE9B,EAAE,CAAC,EAAE,QAAQ,EAAE,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC;IAStD,IAAI,CAAC,GAAG,EAAE,aAAa;IAkBvB,KAAK,CAAC,GAAG,EAAE,aAAa;CAoB/B"}
|
package/dist/plugin.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { ObjectQL } from './index';
|
|
2
|
+
import { ObjectStackProtocolImplementation } from './protocol';
|
|
3
|
+
export class ObjectQLPlugin {
|
|
4
|
+
constructor(ql, hostContext) {
|
|
5
|
+
this.name = 'com.objectstack.engine.objectql';
|
|
6
|
+
this.type = 'objectql';
|
|
7
|
+
this.version = '1.0.0';
|
|
8
|
+
if (ql) {
|
|
9
|
+
this.ql = ql;
|
|
10
|
+
}
|
|
11
|
+
else {
|
|
12
|
+
this.hostContext = hostContext;
|
|
13
|
+
// Lazily created in init
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
async init(ctx) {
|
|
17
|
+
if (!this.ql) {
|
|
18
|
+
this.ql = new ObjectQL(this.hostContext);
|
|
19
|
+
}
|
|
20
|
+
ctx.registerService('objectql', this.ql);
|
|
21
|
+
if (ctx.logger)
|
|
22
|
+
ctx.logger.log(`[ObjectQLPlugin] ObjectQL engine registered as service`);
|
|
23
|
+
// Register Protocol Implementation
|
|
24
|
+
if (!this.ql) {
|
|
25
|
+
throw new Error('ObjectQL engine not initialized');
|
|
26
|
+
}
|
|
27
|
+
const protocolShim = new ObjectStackProtocolImplementation(this.ql);
|
|
28
|
+
ctx.registerService('protocol', protocolShim);
|
|
29
|
+
if (ctx.logger)
|
|
30
|
+
ctx.logger.log(`[ObjectQLPlugin] Protocol service registered`);
|
|
31
|
+
}
|
|
32
|
+
async start(ctx) {
|
|
33
|
+
if (ctx.logger)
|
|
34
|
+
ctx.logger.log(`[ObjectQLPlugin] ObjectQL engine initialized`);
|
|
35
|
+
// Discover features from Kernel Services
|
|
36
|
+
if (ctx.getServices && this.ql) {
|
|
37
|
+
const services = ctx.getServices();
|
|
38
|
+
for (const [name, service] of services.entries()) {
|
|
39
|
+
if (name.startsWith('driver.')) {
|
|
40
|
+
// Register Driver
|
|
41
|
+
this.ql.registerDriver(service);
|
|
42
|
+
if (ctx.logger)
|
|
43
|
+
ctx.logger.log(`[ObjectQLPlugin] Discovered and registered driver service: ${name}`);
|
|
44
|
+
}
|
|
45
|
+
if (name.startsWith('app.')) {
|
|
46
|
+
// Register App
|
|
47
|
+
this.ql.registerApp(service); // service is Manifest
|
|
48
|
+
if (ctx.logger)
|
|
49
|
+
ctx.logger.log(`[ObjectQLPlugin] Discovered and registered app service: ${name}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { IObjectStackProtocol } from '@objectstack/spec/api';
|
|
2
|
+
import { IDataEngine } from '@objectstack/core';
|
|
3
|
+
export declare class ObjectStackProtocolImplementation implements IObjectStackProtocol {
|
|
4
|
+
private engine;
|
|
5
|
+
constructor(engine: IDataEngine);
|
|
6
|
+
getDiscovery(): {
|
|
7
|
+
name: string;
|
|
8
|
+
version: string;
|
|
9
|
+
capabilities: {
|
|
10
|
+
metadata: boolean;
|
|
11
|
+
data: boolean;
|
|
12
|
+
ui: boolean;
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
getMetaTypes(): string[];
|
|
16
|
+
getMetaItems(type: string): unknown[];
|
|
17
|
+
getMetaItem(type: string, name: string): unknown;
|
|
18
|
+
getUiView(object: string, type: 'list' | 'form'): {
|
|
19
|
+
type: string;
|
|
20
|
+
object: string;
|
|
21
|
+
columns: {
|
|
22
|
+
field: string;
|
|
23
|
+
label: string;
|
|
24
|
+
}[];
|
|
25
|
+
sections?: undefined;
|
|
26
|
+
} | {
|
|
27
|
+
type: string;
|
|
28
|
+
object: string;
|
|
29
|
+
sections: {
|
|
30
|
+
label: string;
|
|
31
|
+
fields: {
|
|
32
|
+
field: string;
|
|
33
|
+
}[];
|
|
34
|
+
}[];
|
|
35
|
+
columns?: undefined;
|
|
36
|
+
};
|
|
37
|
+
findData(object: string, query: any): Promise<any[]>;
|
|
38
|
+
getData(object: string, id: string): Promise<any>;
|
|
39
|
+
createData(object: string, data: any): Promise<any>;
|
|
40
|
+
updateData(object: string, id: string, data: any): Promise<any>;
|
|
41
|
+
deleteData(object: string, id: string): Promise<boolean>;
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=protocol.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"protocol.d.ts","sourceRoot":"","sources":["../src/protocol.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAC7D,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAKhD,qBAAa,iCAAkC,YAAW,oBAAoB;IAC1E,OAAO,CAAC,MAAM,CAAc;gBAEhB,MAAM,EAAE,WAAW;IAI/B,YAAY;;;;;;;;;IAYZ,YAAY;IAIZ,YAAY,CAAC,IAAI,EAAE,MAAM;IAIzB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM;IAItC,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM;;;;;;;;;;;;;;;;;;;IA6B/C,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG;IAI7B,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM;IAsBxC,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG;IAIpC,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG;IAIhD,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM;CAGxC"}
|
package/dist/protocol.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// We import SchemaRegistry directly since this class lives in the same package
|
|
2
|
+
import { SchemaRegistry } from './registry';
|
|
3
|
+
export class ObjectStackProtocolImplementation {
|
|
4
|
+
constructor(engine) {
|
|
5
|
+
this.engine = engine;
|
|
6
|
+
}
|
|
7
|
+
getDiscovery() {
|
|
8
|
+
return {
|
|
9
|
+
name: 'ObjectStack API',
|
|
10
|
+
version: '1.0',
|
|
11
|
+
capabilities: {
|
|
12
|
+
metadata: true,
|
|
13
|
+
data: true,
|
|
14
|
+
ui: true
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
getMetaTypes() {
|
|
19
|
+
return SchemaRegistry.getRegisteredTypes();
|
|
20
|
+
}
|
|
21
|
+
getMetaItems(type) {
|
|
22
|
+
return SchemaRegistry.listItems(type);
|
|
23
|
+
}
|
|
24
|
+
getMetaItem(type, name) {
|
|
25
|
+
return SchemaRegistry.getItem(type, name);
|
|
26
|
+
}
|
|
27
|
+
getUiView(object, type) {
|
|
28
|
+
const schema = SchemaRegistry.getObject(object);
|
|
29
|
+
if (!schema)
|
|
30
|
+
throw new Error(`Object ${object} not found`);
|
|
31
|
+
if (type === 'list') {
|
|
32
|
+
return {
|
|
33
|
+
type: 'list',
|
|
34
|
+
object: object,
|
|
35
|
+
columns: Object.keys(schema.fields || {}).slice(0, 5).map(f => ({
|
|
36
|
+
field: f,
|
|
37
|
+
label: schema.fields[f].label || f
|
|
38
|
+
}))
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
return {
|
|
43
|
+
type: 'form',
|
|
44
|
+
object: object,
|
|
45
|
+
sections: [
|
|
46
|
+
{
|
|
47
|
+
label: 'General',
|
|
48
|
+
fields: Object.keys(schema.fields || {}).map(f => ({
|
|
49
|
+
field: f
|
|
50
|
+
}))
|
|
51
|
+
}
|
|
52
|
+
]
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
findData(object, query) {
|
|
57
|
+
return this.engine.find(object, query);
|
|
58
|
+
}
|
|
59
|
+
async getData(object, id) {
|
|
60
|
+
// IDataEngine doesn't have findOne, so we simulate it with find and limit 1
|
|
61
|
+
// Assuming the ID field is named '_id' or 'id'.
|
|
62
|
+
// For broad compatibility, we might need to know the ID field name.
|
|
63
|
+
// But traditionally it is _id in ObjectStack/mongo or id in others.
|
|
64
|
+
// Let's rely on finding by ID if the engine supports it via find?
|
|
65
|
+
// Actually, ObjectQL (the implementation) DOES have findOne.
|
|
66
|
+
// But we are programming against IDataEngine interface here.
|
|
67
|
+
// If the engine IS ObjectQL (which it practically is), we could cast it.
|
|
68
|
+
// But let's try to stick to interface.
|
|
69
|
+
const results = await this.engine.find(object, {
|
|
70
|
+
filter: { _id: id }, // Default Assumption: _id
|
|
71
|
+
limit: 1
|
|
72
|
+
});
|
|
73
|
+
if (results && results.length > 0) {
|
|
74
|
+
return results[0];
|
|
75
|
+
}
|
|
76
|
+
throw new Error(`Record ${id} not found in ${object}`);
|
|
77
|
+
}
|
|
78
|
+
createData(object, data) {
|
|
79
|
+
return this.engine.insert(object, data);
|
|
80
|
+
}
|
|
81
|
+
updateData(object, id, data) {
|
|
82
|
+
return this.engine.update(object, id, data);
|
|
83
|
+
}
|
|
84
|
+
deleteData(object, id) {
|
|
85
|
+
return this.engine.delete(object, id);
|
|
86
|
+
}
|
|
87
|
+
}
|
package/package.json
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@objectstack/objectql",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Isomorphic ObjectQL Engine for ObjectStack",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"types": "src/index.ts",
|
|
7
7
|
"dependencies": {
|
|
8
|
-
"@objectstack/
|
|
8
|
+
"@objectstack/core": "0.6.0",
|
|
9
|
+
"@objectstack/spec": "0.6.0",
|
|
10
|
+
"@objectstack/types": "0.6.0"
|
|
9
11
|
},
|
|
10
12
|
"devDependencies": {
|
|
11
13
|
"typescript": "^5.0.0",
|
package/src/index.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { QueryAST, HookContext } from '@objectstack/spec/data';
|
|
2
2
|
import { ObjectStackManifest } from '@objectstack/spec/system';
|
|
3
|
-
import {
|
|
3
|
+
import { DriverOptions } from '@objectstack/spec/system';
|
|
4
|
+
import { DriverInterface, IDataEngine, DataEngineQueryOptions } from '@objectstack/core';
|
|
4
5
|
import { SchemaRegistry } from './registry';
|
|
5
6
|
|
|
6
7
|
// Export Registry for consumers
|
|
7
8
|
export { SchemaRegistry } from './registry';
|
|
9
|
+
export { ObjectStackProtocolImplementation } from './protocol';
|
|
10
|
+
|
|
8
11
|
|
|
9
12
|
export type HookHandler = (context: HookContext) => Promise<void> | void;
|
|
10
13
|
|
|
@@ -20,8 +23,10 @@ export interface PluginContext {
|
|
|
20
23
|
|
|
21
24
|
/**
|
|
22
25
|
* ObjectQL Engine
|
|
26
|
+
*
|
|
27
|
+
* Implements the IDataEngine interface for data persistence.
|
|
23
28
|
*/
|
|
24
|
-
export class ObjectQL {
|
|
29
|
+
export class ObjectQL implements IDataEngine {
|
|
25
30
|
private drivers = new Map<string, DriverInterface>();
|
|
26
31
|
private defaultDriver: string | null = null;
|
|
27
32
|
|
|
@@ -47,42 +52,7 @@ export class ObjectQL {
|
|
|
47
52
|
async use(manifestPart: any, runtimePart?: any) {
|
|
48
53
|
// 1. Validate / Register Manifest
|
|
49
54
|
if (manifestPart) {
|
|
50
|
-
|
|
51
|
-
// If the passed object is a module namespace with a default export, use that.
|
|
52
|
-
const manifest = manifestPart.default || manifestPart;
|
|
53
|
-
|
|
54
|
-
// In a real scenario, we might strictly parse this using Zod
|
|
55
|
-
// For now, simple ID check
|
|
56
|
-
const id = manifest.id || manifest.name;
|
|
57
|
-
if (!id) {
|
|
58
|
-
console.warn(`[ObjectQL] Plugin manifest missing ID (keys: ${Object.keys(manifest)})`, manifest);
|
|
59
|
-
// Don't return, try to proceed if it looks like an App (Apps might use 'name' instead of 'id')
|
|
60
|
-
// return;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
console.log(`[ObjectQL] Loading Plugin: ${id}`);
|
|
64
|
-
SchemaRegistry.registerPlugin(manifest as ObjectStackManifest);
|
|
65
|
-
|
|
66
|
-
// Register Objects from App/Plugin
|
|
67
|
-
if (manifest.objects) {
|
|
68
|
-
for (const obj of manifest.objects) {
|
|
69
|
-
// Ensure object name is registered globally
|
|
70
|
-
SchemaRegistry.registerObject(obj);
|
|
71
|
-
console.log(`[ObjectQL] Registered Object: ${obj.name}`);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Register contributions
|
|
76
|
-
if (manifest.contributes?.kinds) {
|
|
77
|
-
for (const kind of manifest.contributes.kinds) {
|
|
78
|
-
SchemaRegistry.registerKind(kind);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Register Data Seeding (Lazy execution or immediate?)
|
|
83
|
-
// We store it in a temporary registry or execute immediately if engine is ready.
|
|
84
|
-
// Since `use` is init time, we might need to store it and run later in `seed()`.
|
|
85
|
-
// For this MVP, let's attach it to the manifest object in registry so Kernel can find it.
|
|
55
|
+
this.registerApp(manifestPart);
|
|
86
56
|
}
|
|
87
57
|
|
|
88
58
|
// 2. Execute Runtime
|
|
@@ -125,6 +95,44 @@ export class ObjectQL {
|
|
|
125
95
|
}
|
|
126
96
|
}
|
|
127
97
|
|
|
98
|
+
registerApp(manifestPart: any) {
|
|
99
|
+
// 1. Handle Module Imports (commonjs/esm interop)
|
|
100
|
+
// If the passed object is a module namespace with a default export, use that.
|
|
101
|
+
const raw = manifestPart.default || manifestPart;
|
|
102
|
+
|
|
103
|
+
// Support nested manifest property (Stack Definition)
|
|
104
|
+
// We merge the inner manifest metadata (id, version, etc) with the outer container (objects, apps)
|
|
105
|
+
const manifest = raw.manifest ? { ...raw, ...raw.manifest } : raw;
|
|
106
|
+
|
|
107
|
+
// In a real scenario, we might strictly parse this using Zod
|
|
108
|
+
// For now, simple ID check
|
|
109
|
+
const id = manifest.id || manifest.name;
|
|
110
|
+
if (!id) {
|
|
111
|
+
console.warn(`[ObjectQL] Plugin manifest missing ID (keys: ${Object.keys(manifest)})`, manifest);
|
|
112
|
+
// Don't return, try to proceed if it looks like an App (Apps might use 'name' instead of 'id')
|
|
113
|
+
// return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
console.log(`[ObjectQL] Loading App: ${id}`);
|
|
117
|
+
SchemaRegistry.registerPlugin(manifest as ObjectStackManifest);
|
|
118
|
+
|
|
119
|
+
// Register Objects from App/Plugin
|
|
120
|
+
if (manifest.objects) {
|
|
121
|
+
for (const obj of manifest.objects) {
|
|
122
|
+
// Ensure object name is registered globally
|
|
123
|
+
SchemaRegistry.registerObject(obj);
|
|
124
|
+
console.log(`[ObjectQL] Registered Object: ${obj.name}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Register contributions
|
|
129
|
+
if (manifest.contributes?.kinds) {
|
|
130
|
+
for (const kind of manifest.contributes.kinds) {
|
|
131
|
+
SchemaRegistry.registerKind(kind);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
128
136
|
/**
|
|
129
137
|
* Register a new storage driver
|
|
130
138
|
*/
|
|
@@ -211,27 +219,57 @@ export class ObjectQL {
|
|
|
211
219
|
}
|
|
212
220
|
|
|
213
221
|
// ============================================
|
|
214
|
-
// Data Access Methods
|
|
222
|
+
// Data Access Methods (IDataEngine Interface)
|
|
215
223
|
// ============================================
|
|
216
224
|
|
|
217
|
-
|
|
225
|
+
/**
|
|
226
|
+
* Find records matching a query (IDataEngine interface)
|
|
227
|
+
*
|
|
228
|
+
* @param object - Object name
|
|
229
|
+
* @param query - Query options (IDataEngine format)
|
|
230
|
+
* @returns Promise resolving to array of records
|
|
231
|
+
*/
|
|
232
|
+
async find(object: string, query?: DataEngineQueryOptions): Promise<any[]> {
|
|
218
233
|
const driver = this.getDriver(object);
|
|
219
234
|
|
|
220
|
-
//
|
|
221
|
-
let ast: QueryAST;
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
235
|
+
// Convert DataEngineQueryOptions to QueryAST
|
|
236
|
+
let ast: QueryAST = { object };
|
|
237
|
+
|
|
238
|
+
if (query) {
|
|
239
|
+
// Map DataEngineQueryOptions to QueryAST
|
|
240
|
+
if (query.filter) {
|
|
241
|
+
ast.where = query.filter;
|
|
242
|
+
}
|
|
243
|
+
if (query.select) {
|
|
244
|
+
ast.fields = query.select;
|
|
245
|
+
}
|
|
246
|
+
if (query.sort) {
|
|
247
|
+
// Convert sort Record to orderBy array
|
|
248
|
+
// sort: { createdAt: -1, name: 'asc' } => orderBy: [{ field: 'createdAt', order: 'desc' }, { field: 'name', order: 'asc' }]
|
|
249
|
+
ast.orderBy = Object.entries(query.sort).map(([field, order]) => ({
|
|
250
|
+
field,
|
|
251
|
+
order: (order === -1 || order === 'desc') ? 'desc' : 'asc'
|
|
252
|
+
}));
|
|
253
|
+
}
|
|
254
|
+
// Handle both limit and top (top takes precedence)
|
|
255
|
+
if (query.top !== undefined) {
|
|
256
|
+
ast.limit = query.top;
|
|
257
|
+
} else if (query.limit !== undefined) {
|
|
258
|
+
ast.limit = query.limit;
|
|
259
|
+
}
|
|
260
|
+
if (query.skip !== undefined) {
|
|
261
|
+
ast.offset = query.skip;
|
|
262
|
+
}
|
|
226
263
|
}
|
|
227
264
|
|
|
265
|
+
// Set default limit if not specified
|
|
228
266
|
if (ast.limit === undefined) ast.limit = 100;
|
|
229
267
|
|
|
230
268
|
// Trigger Before Hook
|
|
231
269
|
const hookContext: HookContext = {
|
|
232
270
|
object,
|
|
233
271
|
event: 'beforeFind',
|
|
234
|
-
input: { ast, options },
|
|
272
|
+
input: { ast, options: undefined },
|
|
235
273
|
ql: this
|
|
236
274
|
};
|
|
237
275
|
await this.triggerHooks('beforeFind', hookContext);
|
|
@@ -275,7 +313,14 @@ export class ObjectQL {
|
|
|
275
313
|
return driver.findOne(object, ast, options);
|
|
276
314
|
}
|
|
277
315
|
|
|
278
|
-
|
|
316
|
+
/**
|
|
317
|
+
* Insert a new record (IDataEngine interface)
|
|
318
|
+
*
|
|
319
|
+
* @param object - Object name
|
|
320
|
+
* @param data - Data to insert
|
|
321
|
+
* @returns Promise resolving to the created record
|
|
322
|
+
*/
|
|
323
|
+
async insert(object: string, data: any): Promise<any> {
|
|
279
324
|
const driver = this.getDriver(object);
|
|
280
325
|
|
|
281
326
|
// 1. Get Schema
|
|
@@ -290,7 +335,7 @@ export class ObjectQL {
|
|
|
290
335
|
const hookContext: HookContext = {
|
|
291
336
|
object,
|
|
292
337
|
event: 'beforeInsert',
|
|
293
|
-
input: { data, options },
|
|
338
|
+
input: { data, options: undefined },
|
|
294
339
|
ql: this
|
|
295
340
|
};
|
|
296
341
|
await this.triggerHooks('beforeInsert', hookContext);
|
|
@@ -306,13 +351,21 @@ export class ObjectQL {
|
|
|
306
351
|
return hookContext.result;
|
|
307
352
|
}
|
|
308
353
|
|
|
309
|
-
|
|
354
|
+
/**
|
|
355
|
+
* Update a record by ID (IDataEngine interface)
|
|
356
|
+
*
|
|
357
|
+
* @param object - Object name
|
|
358
|
+
* @param id - Record ID
|
|
359
|
+
* @param data - Updated data
|
|
360
|
+
* @returns Promise resolving to the updated record
|
|
361
|
+
*/
|
|
362
|
+
async update(object: string, id: any, data: any): Promise<any> {
|
|
310
363
|
const driver = this.getDriver(object);
|
|
311
364
|
|
|
312
365
|
const hookContext: HookContext = {
|
|
313
366
|
object,
|
|
314
367
|
event: 'beforeUpdate',
|
|
315
|
-
input: { id, data, options },
|
|
368
|
+
input: { id, data, options: undefined },
|
|
316
369
|
ql: this
|
|
317
370
|
};
|
|
318
371
|
await this.triggerHooks('beforeUpdate', hookContext);
|
|
@@ -326,13 +379,20 @@ export class ObjectQL {
|
|
|
326
379
|
return hookContext.result;
|
|
327
380
|
}
|
|
328
381
|
|
|
329
|
-
|
|
382
|
+
/**
|
|
383
|
+
* Delete a record by ID (IDataEngine interface)
|
|
384
|
+
*
|
|
385
|
+
* @param object - Object name
|
|
386
|
+
* @param id - Record ID
|
|
387
|
+
* @returns Promise resolving to true if deleted, false otherwise
|
|
388
|
+
*/
|
|
389
|
+
async delete(object: string, id: any): Promise<boolean> {
|
|
330
390
|
const driver = this.getDriver(object);
|
|
331
391
|
|
|
332
392
|
const hookContext: HookContext = {
|
|
333
393
|
object,
|
|
334
394
|
event: 'beforeDelete',
|
|
335
|
-
input: { id, options },
|
|
395
|
+
input: { id, options: undefined },
|
|
336
396
|
ql: this
|
|
337
397
|
};
|
|
338
398
|
await this.triggerHooks('beforeDelete', hookContext);
|
|
@@ -343,6 +403,8 @@ export class ObjectQL {
|
|
|
343
403
|
hookContext.result = result;
|
|
344
404
|
await this.triggerHooks('afterDelete', hookContext);
|
|
345
405
|
|
|
406
|
+
// Driver.delete() already returns boolean per DriverInterface spec
|
|
346
407
|
return hookContext.result;
|
|
347
408
|
}
|
|
348
409
|
}
|
|
410
|
+
export * from './plugin';
|
package/src/plugin.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { ObjectQL } from './index';
|
|
2
|
+
import { ObjectStackProtocolImplementation } from './protocol';
|
|
3
|
+
import { Plugin, PluginContext } from '@objectstack/core';
|
|
4
|
+
|
|
5
|
+
export type { Plugin, PluginContext };
|
|
6
|
+
|
|
7
|
+
export class ObjectQLPlugin implements Plugin {
|
|
8
|
+
name = 'com.objectstack.engine.objectql';
|
|
9
|
+
type = 'objectql' as const;
|
|
10
|
+
version = '1.0.0';
|
|
11
|
+
|
|
12
|
+
private ql: ObjectQL | undefined;
|
|
13
|
+
private hostContext?: Record<string, any>;
|
|
14
|
+
|
|
15
|
+
constructor(ql?: ObjectQL, hostContext?: Record<string, any>) {
|
|
16
|
+
if (ql) {
|
|
17
|
+
this.ql = ql;
|
|
18
|
+
} else {
|
|
19
|
+
this.hostContext = hostContext;
|
|
20
|
+
// Lazily created in init
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async init(ctx: PluginContext) {
|
|
25
|
+
if (!this.ql) {
|
|
26
|
+
this.ql = new ObjectQL(this.hostContext);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
ctx.registerService('objectql', this.ql);
|
|
30
|
+
if(ctx.logger) ctx.logger.log(`[ObjectQLPlugin] ObjectQL engine registered as service`);
|
|
31
|
+
|
|
32
|
+
// Register Protocol Implementation
|
|
33
|
+
if (!this.ql) {
|
|
34
|
+
throw new Error('ObjectQL engine not initialized');
|
|
35
|
+
}
|
|
36
|
+
const protocolShim = new ObjectStackProtocolImplementation(this.ql);
|
|
37
|
+
|
|
38
|
+
ctx.registerService('protocol', protocolShim);
|
|
39
|
+
if(ctx.logger) ctx.logger.log(`[ObjectQLPlugin] Protocol service registered`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async start(ctx: PluginContext) {
|
|
43
|
+
if(ctx.logger) ctx.logger.log(`[ObjectQLPlugin] ObjectQL engine initialized`);
|
|
44
|
+
|
|
45
|
+
// Discover features from Kernel Services
|
|
46
|
+
if (ctx.getServices && this.ql) {
|
|
47
|
+
const services = ctx.getServices();
|
|
48
|
+
for (const [name, service] of services.entries()) {
|
|
49
|
+
if (name.startsWith('driver.')) {
|
|
50
|
+
// Register Driver
|
|
51
|
+
this.ql.registerDriver(service);
|
|
52
|
+
if(ctx.logger) ctx.logger.log(`[ObjectQLPlugin] Discovered and registered driver service: ${name}`);
|
|
53
|
+
}
|
|
54
|
+
if (name.startsWith('app.')) {
|
|
55
|
+
// Register App
|
|
56
|
+
this.ql.registerApp(service); // service is Manifest
|
|
57
|
+
if(ctx.logger) ctx.logger.log(`[ObjectQLPlugin] Discovered and registered app service: ${name}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
package/src/protocol.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { IObjectStackProtocol } from '@objectstack/spec/api';
|
|
2
|
+
import { IDataEngine } from '@objectstack/core';
|
|
3
|
+
|
|
4
|
+
// We import SchemaRegistry directly since this class lives in the same package
|
|
5
|
+
import { SchemaRegistry } from './registry';
|
|
6
|
+
|
|
7
|
+
export class ObjectStackProtocolImplementation implements IObjectStackProtocol {
|
|
8
|
+
private engine: IDataEngine;
|
|
9
|
+
|
|
10
|
+
constructor(engine: IDataEngine) {
|
|
11
|
+
this.engine = engine;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
getDiscovery() {
|
|
15
|
+
return {
|
|
16
|
+
name: 'ObjectStack API',
|
|
17
|
+
version: '1.0',
|
|
18
|
+
capabilities: {
|
|
19
|
+
metadata: true,
|
|
20
|
+
data: true,
|
|
21
|
+
ui: true
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
getMetaTypes() {
|
|
27
|
+
return SchemaRegistry.getRegisteredTypes();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
getMetaItems(type: string) {
|
|
31
|
+
return SchemaRegistry.listItems(type);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
getMetaItem(type: string, name: string) {
|
|
35
|
+
return SchemaRegistry.getItem(type, name);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
getUiView(object: string, type: 'list' | 'form') {
|
|
39
|
+
const schema = SchemaRegistry.getObject(object);
|
|
40
|
+
if (!schema) throw new Error(`Object ${object} not found`);
|
|
41
|
+
|
|
42
|
+
if (type === 'list') {
|
|
43
|
+
return {
|
|
44
|
+
type: 'list',
|
|
45
|
+
object: object,
|
|
46
|
+
columns: Object.keys(schema.fields || {}).slice(0, 5).map(f => ({
|
|
47
|
+
field: f,
|
|
48
|
+
label: schema.fields[f].label || f
|
|
49
|
+
}))
|
|
50
|
+
};
|
|
51
|
+
} else {
|
|
52
|
+
return {
|
|
53
|
+
type: 'form',
|
|
54
|
+
object: object,
|
|
55
|
+
sections: [
|
|
56
|
+
{
|
|
57
|
+
label: 'General',
|
|
58
|
+
fields: Object.keys(schema.fields || {}).map(f => ({
|
|
59
|
+
field: f
|
|
60
|
+
}))
|
|
61
|
+
}
|
|
62
|
+
]
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
findData(object: string, query: any) {
|
|
68
|
+
return this.engine.find(object, query);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async getData(object: string, id: string) {
|
|
72
|
+
// IDataEngine doesn't have findOne, so we simulate it with find and limit 1
|
|
73
|
+
// Assuming the ID field is named '_id' or 'id'.
|
|
74
|
+
// For broad compatibility, we might need to know the ID field name.
|
|
75
|
+
// But traditionally it is _id in ObjectStack/mongo or id in others.
|
|
76
|
+
// Let's rely on finding by ID if the engine supports it via find?
|
|
77
|
+
// Actually, ObjectQL (the implementation) DOES have findOne.
|
|
78
|
+
// But we are programming against IDataEngine interface here.
|
|
79
|
+
|
|
80
|
+
// If the engine IS ObjectQL (which it practically is), we could cast it.
|
|
81
|
+
// But let's try to stick to interface.
|
|
82
|
+
|
|
83
|
+
const results = await this.engine.find(object, {
|
|
84
|
+
filter: { _id: id }, // Default Assumption: _id
|
|
85
|
+
limit: 1
|
|
86
|
+
});
|
|
87
|
+
if (results && results.length > 0) {
|
|
88
|
+
return results[0];
|
|
89
|
+
}
|
|
90
|
+
throw new Error(`Record ${id} not found in ${object}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
createData(object: string, data: any) {
|
|
94
|
+
return this.engine.insert(object, data);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
updateData(object: string, id: string, data: any) {
|
|
98
|
+
return this.engine.update(object, id, data);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
deleteData(object: string, id: string) {
|
|
102
|
+
return this.engine.delete(object, id);
|
|
103
|
+
}
|
|
104
|
+
}
|