@objectstack/objectql 0.9.2 → 1.0.1

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.
@@ -0,0 +1,156 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { ObjectQL } from './engine';
3
+ import { SchemaRegistry } from './registry';
4
+ import { DriverInterface } from '@objectstack/spec/data';
5
+
6
+ // Mock the SchemaRegistry to avoid side effects between tests
7
+ vi.mock('./registry', () => {
8
+ const mockObjects = new Map();
9
+ return {
10
+ SchemaRegistry: {
11
+ getObject: vi.fn((name) => mockObjects.get(name)),
12
+ registerObject: vi.fn((obj) => mockObjects.set(obj.name, obj)),
13
+ registerKind: vi.fn(),
14
+ metadata: {
15
+ get: vi.fn(() => mockObjects) // Expose for verification if needed
16
+ }
17
+ }
18
+ };
19
+ });
20
+
21
+ describe('ObjectQL Engine', () => {
22
+ let engine: ObjectQL;
23
+ let mockDriver: DriverInterface;
24
+ let mockDriver2: DriverInterface;
25
+
26
+ beforeEach(() => {
27
+ // Clear Registry Mocks
28
+ vi.clearAllMocks();
29
+
30
+ // Setup Drivers
31
+ mockDriver = {
32
+ name: 'default-driver',
33
+ connect: vi.fn().mockResolvedValue(undefined),
34
+ disconnect: vi.fn().mockResolvedValue(undefined),
35
+ find: vi.fn().mockResolvedValue([{ id: '1', name: 'Test Record' }]),
36
+ findOne: vi.fn(),
37
+ create: vi.fn().mockResolvedValue({ id: '1', success: true }),
38
+ update: vi.fn(),
39
+ delete: vi.fn(),
40
+ count: vi.fn(),
41
+ capabilities: {} as any // Simplified
42
+ } as unknown as DriverInterface;
43
+
44
+ mockDriver2 = {
45
+ name: 'mongo',
46
+ connect: vi.fn().mockResolvedValue(undefined),
47
+ disconnect: vi.fn().mockResolvedValue(undefined),
48
+ find: vi.fn().mockResolvedValue([{ id: '2', name: 'Mongo Record' }]),
49
+ findOne: vi.fn(),
50
+ create: vi.fn().mockResolvedValue({ id: '2', success: true }),
51
+ update: vi.fn(),
52
+ delete: vi.fn(),
53
+ count: vi.fn(),
54
+ capabilities: {} as any
55
+ } as unknown as DriverInterface;
56
+
57
+ engine = new ObjectQL();
58
+ });
59
+
60
+ describe('Initialization', () => {
61
+ it('should initialize with default logger', () => {
62
+ expect(engine).toBeDefined();
63
+ expect(engine.getStatus().status).toBe('running');
64
+ });
65
+
66
+ it('should register and connect drivers on init', async () => {
67
+ engine.registerDriver(mockDriver, true);
68
+ await engine.init();
69
+ expect(mockDriver.connect).toHaveBeenCalled();
70
+ });
71
+ });
72
+
73
+ describe('Metadata Registration', () => {
74
+ it('should register objects from app manifest', () => {
75
+ const manifest = {
76
+ id: 'com.example.app',
77
+ objects: [
78
+ { name: 'task', fields: {} }
79
+ ]
80
+ };
81
+
82
+ engine.registerApp(manifest);
83
+ expect(SchemaRegistry.registerObject).toHaveBeenCalledWith(expect.objectContaining({ name: 'task' }));
84
+ });
85
+
86
+ it('should register kinds from app manifest', () => {
87
+ const manifest = {
88
+ id: 'com.example.app',
89
+ contributes: {
90
+ kinds: [{ id: 'test.kind', description: 'Test Kind' }]
91
+ }
92
+ };
93
+
94
+ engine.registerApp(manifest);
95
+ expect(SchemaRegistry.registerKind).toHaveBeenCalledWith(expect.objectContaining({ id: 'test.kind' }));
96
+ });
97
+ });
98
+
99
+ describe('Driver Routing', () => {
100
+ beforeEach(async () => {
101
+ // Setup:
102
+ // - Default Driver: mockDriver
103
+ // - Specific Driver: mockDriver2 (named 'mongo')
104
+ engine.registerDriver(mockDriver, true);
105
+ engine.registerDriver(mockDriver2);
106
+ await engine.init();
107
+ });
108
+
109
+ it('should route to default driver when no datasource is specified', async () => {
110
+ // Mock Schema: Object uses default datasource
111
+ vi.mocked(SchemaRegistry.getObject).mockReturnValue({ name: 'task', datasource: 'default', fields: {} });
112
+
113
+ await engine.find('task', { filters: [] });
114
+
115
+ expect(mockDriver.find).toHaveBeenCalled();
116
+ expect(mockDriver2.find).not.toHaveBeenCalled();
117
+ });
118
+
119
+ it('should route to specific driver when datasource is specified', async () => {
120
+ // Mock Schema: Object uses 'mongo' datasource
121
+ vi.mocked(SchemaRegistry.getObject).mockReturnValue({ name: 'log', datasource: 'mongo', fields: {} });
122
+
123
+ await engine.find('log', { filters: [] });
124
+
125
+ expect(mockDriver.find).not.toHaveBeenCalled();
126
+ expect(mockDriver2.find).toHaveBeenCalled();
127
+ });
128
+
129
+ it('should throw error if datasource is not found', async () => {
130
+ // Mock Schema: Object uses unknown datasource
131
+ vi.mocked(SchemaRegistry.getObject).mockReturnValue({ name: 'old_data', datasource: 'legacy_sql', fields: {} });
132
+
133
+ await expect(engine.find('old_data', {})).rejects.toThrow("Datasource 'legacy_sql' configured for object 'old_data' is not registered");
134
+ });
135
+ });
136
+
137
+ describe('CRUD Operations', () => {
138
+ beforeEach(async () => {
139
+ engine.registerDriver(mockDriver, true);
140
+ await engine.init();
141
+ vi.mocked(SchemaRegistry.getObject).mockReturnValue({ name: 'task', fields: {} });
142
+ });
143
+
144
+ it('should execute insert operation', async () => {
145
+ const result = await engine.insert('task', { title: 'New Task' });
146
+ expect(mockDriver.create).toHaveBeenCalledWith('task', { title: 'New Task' }, undefined);
147
+ expect(result).toEqual({ id: '1', success: true });
148
+ });
149
+
150
+ it('should execute find operation', async () => {
151
+ const result = await engine.find('task', {});
152
+ expect(mockDriver.find).toHaveBeenCalled();
153
+ expect(result).toHaveLength(1);
154
+ });
155
+ });
156
+ });
package/src/engine.ts CHANGED
@@ -8,6 +8,7 @@ import {
8
8
  DataEngineCountOptions
9
9
  } from '@objectstack/spec/data';
10
10
  import { DriverInterface, IDataEngine, Logger, createLogger } from '@objectstack/core';
11
+ import { CoreServiceName } from '@objectstack/spec/system';
11
12
  import { SchemaRegistry } from './registry.js';
12
13
 
13
14
  export type HookHandler = (context: HookContext) => Promise<void> | void;
@@ -26,6 +27,9 @@ export interface ObjectQLHostContext {
26
27
  * ObjectQL Engine
27
28
  *
28
29
  * Implements the IDataEngine interface for data persistence.
30
+ * Acts as the reference implementation for:
31
+ * - CoreServiceName.data (CRUD)
32
+ * - CoreServiceName.metadata (Schema Registry)
29
33
  */
30
34
  export class ObjectQL implements IDataEngine {
31
35
  private drivers = new Map<string, DriverInterface>();
@@ -50,6 +54,19 @@ export class ObjectQL implements IDataEngine {
50
54
  this.logger.info('ObjectQL Engine Instance Created');
51
55
  }
52
56
 
57
+ /**
58
+ * Service Status Report
59
+ * Used by Kernel to verify health and capabilities.
60
+ */
61
+ getStatus() {
62
+ return {
63
+ name: CoreServiceName.enum.data,
64
+ status: 'running',
65
+ version: '0.9.0',
66
+ features: ['crud', 'query', 'aggregate', 'transactions', 'metadata']
67
+ };
68
+ }
69
+
53
70
  /**
54
71
  * Expose the SchemaRegistry for plugins to register metadata
55
72
  */
package/src/plugin.ts CHANGED
@@ -21,7 +21,7 @@ export class ObjectQLPlugin implements Plugin {
21
21
  }
22
22
  }
23
23
 
24
- async init(ctx: PluginContext) {
24
+ init = async (ctx: PluginContext) => {
25
25
  if (!this.ql) {
26
26
  this.ql = new ObjectQL(this.hostContext);
27
27
  }
@@ -39,7 +39,7 @@ export class ObjectQLPlugin implements Plugin {
39
39
  ctx.logger.info('Protocol service registered');
40
40
  }
41
41
 
42
- async start(ctx: PluginContext) {
42
+ start = async (ctx: PluginContext) => {
43
43
  ctx.logger.info('ObjectQL engine initialized');
44
44
 
45
45
  // Discover features from Kernel Services
package/src/protocol.ts CHANGED
@@ -36,8 +36,19 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol {
36
36
  return {
37
37
  version: '1.0',
38
38
  apiName: 'ObjectStack API',
39
- capabilities: ['metadata', 'data', 'ui'],
40
- endpoints: {}
39
+ capabilities: {
40
+ graphql: false,
41
+ search: false,
42
+ websockets: false,
43
+ files: true,
44
+ analytics: false,
45
+ hub: false
46
+ },
47
+ endpoints: {
48
+ data: '/api/data',
49
+ metadata: '/api/meta',
50
+ auth: '/api/auth'
51
+ }
41
52
  };
42
53
  }
43
54
 
@@ -228,6 +239,30 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol {
228
239
  throw new Error('updateManyData not implemented');
229
240
  }
230
241
 
242
+ async analyticsQuery(_request: any): Promise<any> {
243
+ throw new Error('analyticsQuery not implemented');
244
+ }
245
+
246
+ async getAnalyticsMeta(_request: any): Promise<any> {
247
+ throw new Error('getAnalyticsMeta not implemented');
248
+ }
249
+
250
+ async triggerAutomation(_request: any): Promise<any> {
251
+ throw new Error('triggerAutomation not implemented');
252
+ }
253
+
254
+ async listSpaces(_request: any): Promise<any> {
255
+ throw new Error('listSpaces not implemented');
256
+ }
257
+
258
+ async createSpace(_request: any): Promise<any> {
259
+ throw new Error('createSpace not implemented');
260
+ }
261
+
262
+ async installPlugin(_request: any): Promise<any> {
263
+ throw new Error('installPlugin not implemented');
264
+ }
265
+
231
266
  async deleteManyData(request: DeleteManyDataRequest): Promise<any> {
232
267
  // This expects deleting by IDs.
233
268
  return this.engine.delete(request.object, {
@@ -236,4 +271,15 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol {
236
271
  });
237
272
  }
238
273
 
274
+ async saveMetaItem(request: { type: string, name: string, item?: any }) {
275
+ if (!request.item) {
276
+ throw new Error('Item data is required');
277
+ }
278
+ // Default implementation saves to Memory Registry
279
+ SchemaRegistry.registerItem(request.type, request.item, 'name');
280
+ return {
281
+ success: true,
282
+ message: 'Saved to memory registry'
283
+ };
284
+ }
239
285
  }
package/src/registry.ts CHANGED
@@ -1,5 +1,6 @@
1
- import { ServiceObject } from '@objectstack/spec/data';
2
- import { ObjectStackManifest } from '@objectstack/spec/system';
1
+ import { ServiceObject, ObjectSchema } from '@objectstack/spec/data';
2
+ import { ObjectStackManifest, ManifestSchema } from '@objectstack/spec/kernel';
3
+ import { AppSchema } from '@objectstack/spec/ui';
3
4
 
4
5
  /**
5
6
  * Global Schema Registry
@@ -22,6 +23,15 @@ export class SchemaRegistry {
22
23
  const collection = this.metadata.get(type)!;
23
24
  const key = String(item[keyField]);
24
25
 
26
+ // Validation Hook
27
+ try {
28
+ this.validate(type, item);
29
+ } catch (e: any) {
30
+ console.error(`[Registry] Validation failed for ${type} ${key}: ${e.message}`);
31
+ // For now, warn but don't crash, allowing partial/legacy loads
32
+ // throw e;
33
+ }
34
+
25
35
  if (collection.has(key)) {
26
36
  console.warn(`[Registry] Overwriting ${type}: ${key}`);
27
37
  }
@@ -29,6 +39,24 @@ export class SchemaRegistry {
29
39
  console.log(`[Registry] Registered ${type}: ${key}`);
30
40
  }
31
41
 
42
+ /**
43
+ * Validate Metadata against Spec Zod Schemas
44
+ */
45
+ static validate(type: string, item: any) {
46
+ if (type === 'object') {
47
+ return ObjectSchema.parse(item);
48
+ }
49
+ if (type === 'app') {
50
+ // AppSchema might rely on Zod, imported via UI protocol
51
+ return AppSchema.parse(item);
52
+ }
53
+ if (type === 'plugin') {
54
+ return ManifestSchema.parse(item);
55
+ }
56
+ // Add more validations as needed
57
+ return true;
58
+ }
59
+
32
60
  /**
33
61
  * Universal Unregister Method
34
62
  */