@objectql/core 4.0.4 → 4.0.6
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/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +63 -0
- package/dist/app.js +29 -7
- package/dist/app.js.map +1 -1
- package/dist/gateway.d.ts +36 -0
- package/dist/gateway.js +89 -0
- package/dist/gateway.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/plugin.js +8 -0
- package/dist/plugin.js.map +1 -1
- package/dist/protocol.d.ts +180 -0
- package/dist/protocol.js +260 -0
- package/dist/protocol.js.map +1 -0
- package/jest.config.js +3 -3
- package/package.json +8 -8
- package/src/app.ts +28 -7
- package/src/gateway.ts +101 -0
- package/src/index.ts +2 -0
- package/src/plugin.ts +10 -0
- package/src/protocol.ts +291 -0
- package/test/__mocks__/@objectstack/core.ts +27 -0
- package/test/__mocks__/@objectstack/objectql.ts +45 -0
- package/test/gateway.test.ts +88 -0
- package/test/protocol.test.ts +143 -0
- package/tsconfig.tsbuildinfo +1 -1
package/src/protocol.ts
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectStack Protocol Implementation
|
|
3
|
+
* Copyright (c) 2026-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { ObjectStackProtocol } from '@objectstack/spec/api';
|
|
10
|
+
import { IObjectQL } from '@objectql/types';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Bridges the ObjectStack Protocol (Specification) to the ObjectQL Engine (Implementation)
|
|
14
|
+
*/
|
|
15
|
+
export class ObjectStackProtocolImplementation implements ObjectStackProtocol {
|
|
16
|
+
constructor(private engine: IObjectQL) {}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Get API Discovery Document
|
|
20
|
+
*/
|
|
21
|
+
async getDiscovery(args: any): Promise<any> {
|
|
22
|
+
return {
|
|
23
|
+
name: 'ObjectQL Engine',
|
|
24
|
+
version: '4.0.0',
|
|
25
|
+
protocols: ['rest', 'graphql', 'json-rpc', 'odata'],
|
|
26
|
+
auth: {
|
|
27
|
+
type: 'bearer',
|
|
28
|
+
url: '/auth/token'
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Get Metadata Types (e.g., ['object', 'action'])
|
|
35
|
+
*/
|
|
36
|
+
async getMetaTypes(args: any): Promise<{ types: string[] }> {
|
|
37
|
+
let types = ['object'];
|
|
38
|
+
if (this.engine.metadata && typeof this.engine.metadata.getTypes === 'function') {
|
|
39
|
+
types = this.engine.metadata.getTypes();
|
|
40
|
+
}
|
|
41
|
+
return { types };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Get Metadata Items for a Type
|
|
46
|
+
*/
|
|
47
|
+
async getMetaItems(args: { type: string }): Promise<{ type: string; items: any[] }> {
|
|
48
|
+
const { type } = args;
|
|
49
|
+
let items: any[] = [];
|
|
50
|
+
if (this.engine.metadata && typeof this.engine.metadata.list === 'function') {
|
|
51
|
+
items = this.engine.metadata.list(type);
|
|
52
|
+
}
|
|
53
|
+
return { type, items };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Get Metadata Item
|
|
58
|
+
*/
|
|
59
|
+
async getMetaItem(args: { type: string; name: string }): Promise<any> {
|
|
60
|
+
const { type, name } = args;
|
|
61
|
+
if (this.engine.metadata && typeof this.engine.metadata.get === 'function') {
|
|
62
|
+
return this.engine.metadata.get(type, name);
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Get Cached Metadata Item
|
|
69
|
+
*/
|
|
70
|
+
async getMetaItemCached(args: { type: string; name: string }): Promise<any> {
|
|
71
|
+
// Fallback to non-cached version for now
|
|
72
|
+
return this.getMetaItem(args);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Get UI View
|
|
77
|
+
*/
|
|
78
|
+
async getUiView(args: { object: string; type: 'list' | 'form' }): Promise<any> {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Find Data Records
|
|
84
|
+
*/
|
|
85
|
+
async findData(args: { object: string; query?: any }): Promise<any> {
|
|
86
|
+
const { object, query } = args;
|
|
87
|
+
|
|
88
|
+
// Use direct kernel method if available (preferred)
|
|
89
|
+
if (typeof (this.engine as any).find === 'function') {
|
|
90
|
+
const result = await (this.engine as any).find(object, query || {});
|
|
91
|
+
return result;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Fallback to createContext (if engine is IObjectQL)
|
|
95
|
+
if (typeof (this.engine as any).createContext === 'function') {
|
|
96
|
+
const ctx = (this.engine as any).createContext({ isSystem: true });
|
|
97
|
+
try {
|
|
98
|
+
const repo = ctx.object(object);
|
|
99
|
+
return await repo.find(query || {});
|
|
100
|
+
} catch (error: any) {
|
|
101
|
+
throw new Error(`Data access failed: ${error.message}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
throw new Error('Engine does not support find operation');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Count Data Records
|
|
110
|
+
*/
|
|
111
|
+
async countData(args: { object: string; query?: any }): Promise<number> {
|
|
112
|
+
const { object, query } = args;
|
|
113
|
+
// Basic fallback
|
|
114
|
+
const result = await this.findData(args);
|
|
115
|
+
return Array.isArray(result) ? result.length : (result.value ? result.value.length : 0);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Get Single Data Record
|
|
121
|
+
*/
|
|
122
|
+
async getData(args: { object: string; id: string }): Promise<any> {
|
|
123
|
+
const { object, id } = args;
|
|
124
|
+
|
|
125
|
+
if (typeof (this.engine as any).get === 'function') {
|
|
126
|
+
return await (this.engine as any).get(object, id);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (typeof (this.engine as any).createContext === 'function') {
|
|
130
|
+
const ctx = (this.engine as any).createContext({ isSystem: true });
|
|
131
|
+
try {
|
|
132
|
+
const repo = ctx.object(object);
|
|
133
|
+
return await repo.findOne(id);
|
|
134
|
+
} catch (error: any) {
|
|
135
|
+
throw new Error(`Data retrieval failed: ${error.message}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
throw new Error('Engine does not support get operation');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Create Data Record
|
|
144
|
+
*/
|
|
145
|
+
async createData(args: { object: string; data: any }): Promise<any> {
|
|
146
|
+
const { object, data } = args;
|
|
147
|
+
|
|
148
|
+
if (typeof (this.engine as any).create === 'function') {
|
|
149
|
+
return await (this.engine as any).create(object, data);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (typeof (this.engine as any).createContext === 'function') {
|
|
153
|
+
const ctx = (this.engine as any).createContext({ isSystem: true });
|
|
154
|
+
try {
|
|
155
|
+
const repo = ctx.object(object);
|
|
156
|
+
// Protocol expects returned data
|
|
157
|
+
return await repo.create(data);
|
|
158
|
+
} catch (error: any) {
|
|
159
|
+
throw new Error(`Data creation failed: ${error.message}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
throw new Error('Engine does not support create operation');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Update Data Record
|
|
168
|
+
*/
|
|
169
|
+
async updateData(args: { object: string; id: string; data: any }): Promise<any> {
|
|
170
|
+
const { object, id, data } = args;
|
|
171
|
+
|
|
172
|
+
if (typeof (this.engine as any).update === 'function') {
|
|
173
|
+
return await (this.engine as any).update(object, id, data);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (typeof (this.engine as any).createContext === 'function') {
|
|
177
|
+
const ctx = (this.engine as any).createContext({ isSystem: true });
|
|
178
|
+
try {
|
|
179
|
+
const repo = ctx.object(object);
|
|
180
|
+
return await repo.update(id, data);
|
|
181
|
+
} catch (error: any) {
|
|
182
|
+
throw new Error(`Data update failed: ${error.message}`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
throw new Error('Engine does not support update operation');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Delete Data Record
|
|
191
|
+
*/
|
|
192
|
+
async deleteData(args: { object: string; id: string }): Promise<{ object: string; id: string; success: boolean }> {
|
|
193
|
+
const { object, id } = args;
|
|
194
|
+
|
|
195
|
+
if (typeof (this.engine as any).delete === 'function') {
|
|
196
|
+
const success = await (this.engine as any).delete(object, id);
|
|
197
|
+
return { object, id, success: !!success };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (typeof (this.engine as any).createContext === 'function') {
|
|
201
|
+
const ctx = (this.engine as any).createContext({ isSystem: true });
|
|
202
|
+
try {
|
|
203
|
+
const repo = ctx.object(object);
|
|
204
|
+
await repo.delete(id);
|
|
205
|
+
return { object, id, success: true };
|
|
206
|
+
} catch (error: any) {
|
|
207
|
+
throw new Error(`Data deletion failed: ${error.message}`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
throw new Error('Engine does not support delete operation');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Create Many Data Records
|
|
216
|
+
*/
|
|
217
|
+
async createManyData(args: { object: string; records: any[]; options?: any }): Promise<any> {
|
|
218
|
+
throw new Error('createManyData not implemented');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Update Many Data Records
|
|
223
|
+
*/
|
|
224
|
+
async updateManyData(args: { object: string; records: { id: string; data: any }[]; options?: any }): Promise<any> {
|
|
225
|
+
throw new Error('updateManyData not implemented');
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Delete Many Data Records
|
|
230
|
+
*/
|
|
231
|
+
async deleteManyData(args: { object: string; ids: string[]; options?: any }): Promise<any> {
|
|
232
|
+
throw new Error('deleteManyData not implemented');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Batch Operations
|
|
237
|
+
*/
|
|
238
|
+
async batchData(args: { object: string; request: { operation: 'create' | 'update' | 'delete' | 'upsert'; records: any[] } }): Promise<any> {
|
|
239
|
+
throw new Error('batchData not implemented');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Execute Action/Operation
|
|
244
|
+
*/
|
|
245
|
+
async performAction(args: { object: string; id?: string; action: string; args?: any }): Promise<any> {
|
|
246
|
+
// Not implemented in this shim yet
|
|
247
|
+
throw new Error('Action execution not implemented in protocol shim');
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Analytics Query - Execute analytics query
|
|
252
|
+
*/
|
|
253
|
+
async analyticsQuery(args: any): Promise<any> {
|
|
254
|
+
throw new Error('analyticsQuery not implemented');
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Get Analytics Metadata
|
|
259
|
+
*/
|
|
260
|
+
async getAnalyticsMeta(args: any): Promise<any> {
|
|
261
|
+
throw new Error('getAnalyticsMeta not implemented');
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Trigger Automation
|
|
266
|
+
*/
|
|
267
|
+
async triggerAutomation(args: { trigger: string; payload: Record<string, any> }): Promise<{ success: boolean; jobId?: string; result?: any }> {
|
|
268
|
+
throw new Error('triggerAutomation not implemented');
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* List Spaces (Hub/Workspace Management)
|
|
273
|
+
*/
|
|
274
|
+
async listSpaces(args: any): Promise<any> {
|
|
275
|
+
throw new Error('listSpaces not implemented');
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Create Space (Hub/Workspace Management)
|
|
280
|
+
*/
|
|
281
|
+
async createSpace(args: any): Promise<any> {
|
|
282
|
+
throw new Error('createSpace not implemented');
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Install Plugin (Hub/Extension Management)
|
|
287
|
+
*/
|
|
288
|
+
async installPlugin(args: any): Promise<any> {
|
|
289
|
+
throw new Error('installPlugin not implemented');
|
|
290
|
+
}
|
|
291
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectQL
|
|
3
|
+
* Copyright (c) 2026-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Mock for @objectstack/core to enable Jest testing
|
|
11
|
+
*
|
|
12
|
+
* Since @objectstack/core@0.9.2 uses ES modules with import.meta,
|
|
13
|
+
* which Jest doesn't support well, we provide this mock for testing.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export const createLogger = jest.fn(() => ({
|
|
17
|
+
trace: jest.fn(),
|
|
18
|
+
debug: jest.fn(),
|
|
19
|
+
info: jest.fn(),
|
|
20
|
+
warn: jest.fn(),
|
|
21
|
+
error: jest.fn(),
|
|
22
|
+
fatal: jest.fn(),
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
export const ObjectKernel = jest.fn();
|
|
26
|
+
export const LiteKernel = jest.fn();
|
|
27
|
+
export const createApiRegistryPlugin = jest.fn();
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectQL
|
|
3
|
+
* Copyright (c) 2026-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Mock for @objectstack/objectql to enable Jest testing
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export class ObjectQL {
|
|
14
|
+
constructor(public config: any) {}
|
|
15
|
+
async connect() {}
|
|
16
|
+
async disconnect() {}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const mockStore = new Map<string, Map<string, any>>();
|
|
20
|
+
|
|
21
|
+
export const SchemaRegistry = {
|
|
22
|
+
register: jest.fn(),
|
|
23
|
+
get: jest.fn(),
|
|
24
|
+
registerItem: jest.fn((type: string, item: any, keyField: string = 'name') => {
|
|
25
|
+
if (!mockStore.has(type)) {
|
|
26
|
+
mockStore.set(type, new Map());
|
|
27
|
+
}
|
|
28
|
+
const key = item[keyField];
|
|
29
|
+
mockStore.get(type)!.set(key, item);
|
|
30
|
+
}),
|
|
31
|
+
unregisterItem: jest.fn((type: string, name: string) => {
|
|
32
|
+
const collection = mockStore.get(type);
|
|
33
|
+
if (collection) {
|
|
34
|
+
collection.delete(name);
|
|
35
|
+
}
|
|
36
|
+
}),
|
|
37
|
+
getItem: jest.fn((type: string, name: string) => {
|
|
38
|
+
return mockStore.get(type)?.get(name);
|
|
39
|
+
}),
|
|
40
|
+
listItems: jest.fn((type: string) => {
|
|
41
|
+
const items = mockStore.get(type);
|
|
42
|
+
return items ? Array.from(items.values()) : [];
|
|
43
|
+
}),
|
|
44
|
+
metadata: mockStore,
|
|
45
|
+
};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectQL
|
|
3
|
+
* Copyright (c) 2026-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { ObjectGateway } from '../src/gateway';
|
|
10
|
+
import { ApiRequest, ApiResponse, GatewayProtocol } from '@objectql/types';
|
|
11
|
+
|
|
12
|
+
describe('ObjectGateway', () => {
|
|
13
|
+
let gateway: ObjectGateway;
|
|
14
|
+
let mockProtocol: GatewayProtocol;
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
gateway = new ObjectGateway();
|
|
18
|
+
mockProtocol = {
|
|
19
|
+
name: 'mock',
|
|
20
|
+
route: jest.fn().mockReturnValue(true),
|
|
21
|
+
handle: jest.fn().mockResolvedValue({ status: 200, body: 'ok' })
|
|
22
|
+
};
|
|
23
|
+
gateway.registerProtocol(mockProtocol);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should route request to registered protocol', async () => {
|
|
27
|
+
const req: ApiRequest = {
|
|
28
|
+
path: '/test',
|
|
29
|
+
method: 'GET',
|
|
30
|
+
headers: {},
|
|
31
|
+
query: {}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const response = await gateway.handle(req);
|
|
35
|
+
|
|
36
|
+
expect(mockProtocol.route).toHaveBeenCalledWith(req);
|
|
37
|
+
expect(mockProtocol.handle).toHaveBeenCalledWith(req);
|
|
38
|
+
expect(response.status).toBe(200);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should return 404 if no protocol matches', async () => {
|
|
42
|
+
const specializedGateway = new ObjectGateway();
|
|
43
|
+
const response = await specializedGateway.handle({
|
|
44
|
+
path: '/unknown',
|
|
45
|
+
method: 'GET',
|
|
46
|
+
headers: {},
|
|
47
|
+
query: {}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
expect(response.status).toBe(404);
|
|
51
|
+
expect(response.body.error.code).toBe('PROTOCOL_NOT_FOUND');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should apply request transformers', async () => {
|
|
55
|
+
const req: ApiRequest = {
|
|
56
|
+
path: '/original',
|
|
57
|
+
method: 'GET',
|
|
58
|
+
headers: {},
|
|
59
|
+
query: {}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
gateway.addRequestTransform(async (r) => {
|
|
63
|
+
return { ...r, path: '/transformed' };
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
await gateway.handle(req);
|
|
67
|
+
|
|
68
|
+
// Protocol should see the transformed request
|
|
69
|
+
expect(mockProtocol.route).toHaveBeenCalledWith(expect.objectContaining({ path: '/transformed' }));
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should apply response transformers', async () => {
|
|
73
|
+
const req: ApiRequest = {
|
|
74
|
+
path: '/test',
|
|
75
|
+
method: 'GET',
|
|
76
|
+
headers: {},
|
|
77
|
+
query: {}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
gateway.addResponseTransform(async (res) => {
|
|
81
|
+
return { ...res, headers: { ...res.headers, 'X-Custom': 'Added' } };
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const response = await gateway.handle(req);
|
|
85
|
+
|
|
86
|
+
expect(response.headers?.['X-Custom']).toBe('Added');
|
|
87
|
+
});
|
|
88
|
+
});
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectQL
|
|
3
|
+
* Copyright (c) 2026-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { ObjectStackProtocolImplementation } from '../src/protocol';
|
|
10
|
+
import { IObjectQL } from '@objectql/types';
|
|
11
|
+
|
|
12
|
+
describe('ObjectStackProtocolImplementation', () => {
|
|
13
|
+
let mockEngine: Partial<IObjectQL>;
|
|
14
|
+
let protocol: ObjectStackProtocolImplementation;
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
mockEngine = {
|
|
18
|
+
metadata: {
|
|
19
|
+
getTypes: jest.fn().mockReturnValue(['object']),
|
|
20
|
+
list: jest.fn().mockReturnValue([{ name: 'testObject', label: 'Test Object' }]),
|
|
21
|
+
get: jest.fn().mockReturnValue({ name: 'testObject', fields: {} }),
|
|
22
|
+
} as any,
|
|
23
|
+
// Mock kernel-like direct methods
|
|
24
|
+
find: jest.fn(),
|
|
25
|
+
get: jest.fn(),
|
|
26
|
+
create: jest.fn(),
|
|
27
|
+
update: jest.fn(),
|
|
28
|
+
delete: jest.fn(),
|
|
29
|
+
};
|
|
30
|
+
protocol = new ObjectStackProtocolImplementation(mockEngine as IObjectQL);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe('Meta Operations', () => {
|
|
34
|
+
it('getMetaTypes should return types from engine metadata', async () => {
|
|
35
|
+
const result = await protocol.getMetaTypes({});
|
|
36
|
+
expect(result).toEqual({ types: ['object'] });
|
|
37
|
+
expect(mockEngine.metadata?.getTypes).toHaveBeenCalled();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('getMetaItems should return items for a type', async () => {
|
|
41
|
+
const result = await protocol.getMetaItems({ type: 'object' });
|
|
42
|
+
expect(result).toEqual({
|
|
43
|
+
type: 'object',
|
|
44
|
+
items: [{ name: 'testObject', label: 'Test Object' }]
|
|
45
|
+
});
|
|
46
|
+
expect(mockEngine.metadata?.list).toHaveBeenCalledWith('object');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('getMetaItem should return specific item definition', async () => {
|
|
50
|
+
const result = await protocol.getMetaItem({ type: 'object', name: 'testObject' });
|
|
51
|
+
expect(result).toEqual({ name: 'testObject', fields: {} });
|
|
52
|
+
expect(mockEngine.metadata?.get).toHaveBeenCalledWith('object', 'testObject');
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe('Data Operations (Direct Kernel Mode)', () => {
|
|
57
|
+
it('findData should delegate to engine.find', async () => {
|
|
58
|
+
const mockData = [{ id: '1', name: 'Test' }];
|
|
59
|
+
(mockEngine as any).find.mockResolvedValue(mockData);
|
|
60
|
+
|
|
61
|
+
const result = await protocol.findData({ object: 'testObject', query: {} });
|
|
62
|
+
expect(result).toBe(mockData);
|
|
63
|
+
expect((mockEngine as any).find).toHaveBeenCalledWith('testObject', {});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('getData should delegate to engine.get', async () => {
|
|
67
|
+
const mockRecord = { id: '1', name: 'Test' };
|
|
68
|
+
(mockEngine as any).get.mockResolvedValue(mockRecord);
|
|
69
|
+
|
|
70
|
+
const result = await protocol.getData({ object: 'testObject', id: '1' });
|
|
71
|
+
expect(result).toBe(mockRecord);
|
|
72
|
+
expect((mockEngine as any).get).toHaveBeenCalledWith('testObject', '1');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('createData should delegate to engine.create', async () => {
|
|
76
|
+
const newData = { name: 'New' };
|
|
77
|
+
const createdRecord = { id: '2', ...newData };
|
|
78
|
+
(mockEngine as any).create.mockResolvedValue(createdRecord);
|
|
79
|
+
|
|
80
|
+
const result = await protocol.createData({ object: 'testObject', data: newData });
|
|
81
|
+
expect(result).toBe(createdRecord);
|
|
82
|
+
expect((mockEngine as any).create).toHaveBeenCalledWith('testObject', newData);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('updateData should delegate to engine.update', async () => {
|
|
86
|
+
const updateData = { name: 'Updated' };
|
|
87
|
+
const updatedRecord = { id: '1', ...updateData };
|
|
88
|
+
(mockEngine as any).update.mockResolvedValue(updatedRecord);
|
|
89
|
+
|
|
90
|
+
const result = await protocol.updateData({ object: 'testObject', id: '1', data: updateData });
|
|
91
|
+
expect(result).toBe(updatedRecord);
|
|
92
|
+
expect((mockEngine as any).update).toHaveBeenCalledWith('testObject', '1', updateData);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('deleteData should delegate to engine.delete', async () => {
|
|
96
|
+
(mockEngine as any).delete.mockResolvedValue(true);
|
|
97
|
+
|
|
98
|
+
const result = await protocol.deleteData({ object: 'testObject', id: '1' });
|
|
99
|
+
expect(result).toEqual({ object: 'testObject', id: '1', success: true });
|
|
100
|
+
expect((mockEngine as any).delete).toHaveBeenCalledWith('testObject', '1');
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe('Legacy Mode (IObjectQL Context)', () => {
|
|
105
|
+
let mockRepo: any;
|
|
106
|
+
|
|
107
|
+
beforeEach(() => {
|
|
108
|
+
// Remove direct methods to force legacy path
|
|
109
|
+
delete (mockEngine as any).find;
|
|
110
|
+
delete (mockEngine as any).get;
|
|
111
|
+
delete (mockEngine as any).create;
|
|
112
|
+
delete (mockEngine as any).update;
|
|
113
|
+
delete (mockEngine as any).delete;
|
|
114
|
+
|
|
115
|
+
mockRepo = {
|
|
116
|
+
find: jest.fn(),
|
|
117
|
+
findOne: jest.fn(),
|
|
118
|
+
create: jest.fn(),
|
|
119
|
+
update: jest.fn(),
|
|
120
|
+
delete: jest.fn(),
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
(mockEngine as any).createContext = jest.fn().mockReturnValue({
|
|
124
|
+
object: jest.fn().mockReturnValue(mockRepo)
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('findData should use repo.find in legacy mode', async () => {
|
|
129
|
+
const mockData = [{ id: '1' }];
|
|
130
|
+
mockRepo.find.mockResolvedValue(mockData);
|
|
131
|
+
|
|
132
|
+
await protocol.findData({ object: 'testObject' });
|
|
133
|
+
expect(mockRepo.find).toHaveBeenCalled();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('deleteData should use repo.delete in legacy mode', async () => {
|
|
137
|
+
mockRepo.delete.mockResolvedValue(true);
|
|
138
|
+
const result = await protocol.deleteData({ object: 'testObject', id: '1' });
|
|
139
|
+
expect(result.success).toBe(true);
|
|
140
|
+
expect(mockRepo.delete).toHaveBeenCalledWith('1');
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
});
|