@objectstack/objectql 4.0.4 → 4.0.5
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/dist/index.d.mts +468 -1113
- package/dist/index.d.ts +468 -1113
- package/dist/index.js +1271 -268
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1266 -268
- package/dist/index.mjs.map +1 -1
- package/package.json +32 -6
- package/.turbo/turbo-build.log +0 -22
- package/CHANGELOG.md +0 -720
- package/src/datasource-mapping.test.ts +0 -181
- package/src/engine.test.ts +0 -613
- package/src/engine.ts +0 -1668
- package/src/index.ts +0 -41
- package/src/kernel-factory.ts +0 -48
- package/src/metadata-facade.ts +0 -96
- package/src/plugin.integration.test.ts +0 -995
- package/src/plugin.ts +0 -534
- package/src/protocol-data.test.ts +0 -245
- package/src/protocol-discovery.test.ts +0 -213
- package/src/protocol-feed.test.ts +0 -303
- package/src/protocol-meta.test.ts +0 -440
- package/src/protocol.ts +0 -1242
- package/src/registry.test.ts +0 -494
- package/src/registry.ts +0 -716
- package/src/util.test.ts +0 -226
- package/src/util.ts +0 -219
- package/tsconfig.json +0 -10
package/src/engine.test.ts
DELETED
|
@@ -1,613 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
-
import { ObjectQL } from './engine';
|
|
3
|
-
import { SchemaRegistry } from './registry';
|
|
4
|
-
import type { IDataDriver } from '@objectstack/spec/contracts';
|
|
5
|
-
|
|
6
|
-
// Mock the SchemaRegistry to avoid side effects between tests
|
|
7
|
-
vi.mock('./registry', () => {
|
|
8
|
-
const mockObjects = new Map();
|
|
9
|
-
const mockContributors = new Map();
|
|
10
|
-
return {
|
|
11
|
-
SchemaRegistry: {
|
|
12
|
-
getObject: vi.fn((name) => mockObjects.get(name)),
|
|
13
|
-
resolveObject: vi.fn((name) => mockObjects.get(name)),
|
|
14
|
-
registerObject: vi.fn((obj, packageId, namespace, ownership, priority) => {
|
|
15
|
-
const fqn = namespace ? `${namespace}__${obj.name}` : obj.name;
|
|
16
|
-
mockObjects.set(fqn, { ...obj, name: fqn });
|
|
17
|
-
// Also track contributors for getObjectOwner
|
|
18
|
-
if (!mockContributors.has(fqn)) {
|
|
19
|
-
mockContributors.set(fqn, []);
|
|
20
|
-
}
|
|
21
|
-
const contributors = mockContributors.get(fqn);
|
|
22
|
-
contributors.push({ packageId, namespace, ownership, priority, definition: obj });
|
|
23
|
-
return fqn;
|
|
24
|
-
}),
|
|
25
|
-
getObjectOwner: vi.fn((fqn) => {
|
|
26
|
-
const contributors = mockContributors.get(fqn);
|
|
27
|
-
return contributors?.find(c => c.ownership === 'own');
|
|
28
|
-
}),
|
|
29
|
-
registerNamespace: vi.fn(),
|
|
30
|
-
registerKind: vi.fn(),
|
|
31
|
-
registerItem: vi.fn(),
|
|
32
|
-
registerApp: vi.fn(),
|
|
33
|
-
installPackage: vi.fn((manifest) => ({
|
|
34
|
-
manifest,
|
|
35
|
-
status: 'installed',
|
|
36
|
-
enabled: true,
|
|
37
|
-
installedAt: new Date().toISOString(),
|
|
38
|
-
})),
|
|
39
|
-
reset: vi.fn(() => {
|
|
40
|
-
mockObjects.clear();
|
|
41
|
-
mockContributors.clear();
|
|
42
|
-
}),
|
|
43
|
-
metadata: {
|
|
44
|
-
get: vi.fn(() => mockObjects) // Expose for verification if needed
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
};
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
describe('ObjectQL Engine', () => {
|
|
51
|
-
let engine: ObjectQL;
|
|
52
|
-
let mockDriver: IDataDriver;
|
|
53
|
-
let mockDriver2: IDataDriver;
|
|
54
|
-
|
|
55
|
-
beforeEach(() => {
|
|
56
|
-
// Clear Registry Mocks
|
|
57
|
-
vi.clearAllMocks();
|
|
58
|
-
|
|
59
|
-
// Setup Drivers
|
|
60
|
-
mockDriver = {
|
|
61
|
-
name: 'default-driver',
|
|
62
|
-
connect: vi.fn().mockResolvedValue(undefined),
|
|
63
|
-
disconnect: vi.fn().mockResolvedValue(undefined),
|
|
64
|
-
find: vi.fn().mockResolvedValue([{ id: '1', name: 'Test Record' }]),
|
|
65
|
-
findOne: vi.fn(),
|
|
66
|
-
create: vi.fn().mockResolvedValue({ id: '1', success: true }),
|
|
67
|
-
update: vi.fn(),
|
|
68
|
-
delete: vi.fn(),
|
|
69
|
-
count: vi.fn(),
|
|
70
|
-
capabilities: {} as any // Simplified
|
|
71
|
-
} as unknown as IDataDriver;
|
|
72
|
-
|
|
73
|
-
mockDriver2 = {
|
|
74
|
-
name: 'mongo',
|
|
75
|
-
connect: vi.fn().mockResolvedValue(undefined),
|
|
76
|
-
disconnect: vi.fn().mockResolvedValue(undefined),
|
|
77
|
-
find: vi.fn().mockResolvedValue([{ id: '2', name: 'Mongo Record' }]),
|
|
78
|
-
findOne: vi.fn(),
|
|
79
|
-
create: vi.fn().mockResolvedValue({ id: '2', success: true }),
|
|
80
|
-
update: vi.fn(),
|
|
81
|
-
delete: vi.fn(),
|
|
82
|
-
count: vi.fn(),
|
|
83
|
-
capabilities: {} as any
|
|
84
|
-
} as unknown as IDataDriver;
|
|
85
|
-
|
|
86
|
-
engine = new ObjectQL();
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
describe('Initialization', () => {
|
|
90
|
-
it('should initialize with default logger', () => {
|
|
91
|
-
expect(engine).toBeDefined();
|
|
92
|
-
expect(engine.getStatus().status).toBe('running');
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it('should register and connect drivers on init', async () => {
|
|
96
|
-
engine.registerDriver(mockDriver, true);
|
|
97
|
-
await engine.init();
|
|
98
|
-
expect(mockDriver.connect).toHaveBeenCalled();
|
|
99
|
-
});
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
describe('Metadata Registration', () => {
|
|
103
|
-
it('should register objects from app manifest with namespace', () => {
|
|
104
|
-
const manifest = {
|
|
105
|
-
id: 'com.example.app',
|
|
106
|
-
namespace: 'example',
|
|
107
|
-
objects: [
|
|
108
|
-
{ name: 'task', fields: {} }
|
|
109
|
-
]
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
engine.registerApp(manifest);
|
|
113
|
-
expect(SchemaRegistry.registerObject).toHaveBeenCalledWith(
|
|
114
|
-
expect.objectContaining({ name: 'task' }),
|
|
115
|
-
'com.example.app',
|
|
116
|
-
'example',
|
|
117
|
-
'own'
|
|
118
|
-
);
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it('should register objects without namespace (legacy)', () => {
|
|
122
|
-
const manifest = {
|
|
123
|
-
id: 'com.legacy.app',
|
|
124
|
-
objects: [
|
|
125
|
-
{ name: 'item', fields: {} }
|
|
126
|
-
]
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
engine.registerApp(manifest);
|
|
130
|
-
expect(SchemaRegistry.registerObject).toHaveBeenCalledWith(
|
|
131
|
-
expect.objectContaining({ name: 'item' }),
|
|
132
|
-
'com.legacy.app',
|
|
133
|
-
undefined,
|
|
134
|
-
'own'
|
|
135
|
-
);
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
it('should register object extensions', () => {
|
|
139
|
-
const manifest = {
|
|
140
|
-
id: 'com.extender.app',
|
|
141
|
-
namespace: 'ext',
|
|
142
|
-
objectExtensions: [
|
|
143
|
-
{ extend: 'base__contact', fields: { custom_field: { type: 'text' } }, priority: 250 }
|
|
144
|
-
]
|
|
145
|
-
};
|
|
146
|
-
|
|
147
|
-
engine.registerApp(manifest);
|
|
148
|
-
expect(SchemaRegistry.registerObject).toHaveBeenCalledWith(
|
|
149
|
-
expect.objectContaining({ name: 'base__contact' }),
|
|
150
|
-
'com.extender.app',
|
|
151
|
-
undefined,
|
|
152
|
-
'extend',
|
|
153
|
-
250
|
|
154
|
-
);
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
it('should register kinds from app manifest', () => {
|
|
158
|
-
const manifest = {
|
|
159
|
-
id: 'com.example.app',
|
|
160
|
-
contributes: {
|
|
161
|
-
kinds: [{ id: 'test.kind', description: 'Test Kind' }]
|
|
162
|
-
}
|
|
163
|
-
};
|
|
164
|
-
|
|
165
|
-
engine.registerApp(manifest);
|
|
166
|
-
expect(SchemaRegistry.registerKind).toHaveBeenCalledWith(expect.objectContaining({ id: 'test.kind' }));
|
|
167
|
-
});
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
describe('Driver Routing', () => {
|
|
171
|
-
beforeEach(async () => {
|
|
172
|
-
// Setup:
|
|
173
|
-
// - Default Driver: mockDriver
|
|
174
|
-
// - Specific Driver: mockDriver2 (named 'mongo')
|
|
175
|
-
engine.registerDriver(mockDriver, true);
|
|
176
|
-
engine.registerDriver(mockDriver2);
|
|
177
|
-
await engine.init();
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
it('should route to default driver when no datasource is specified', async () => {
|
|
181
|
-
// Mock Schema: Object uses default datasource
|
|
182
|
-
vi.mocked(SchemaRegistry.getObject).mockReturnValue({ name: 'task', datasource: 'default', fields: {} });
|
|
183
|
-
|
|
184
|
-
await engine.find('task', { filters: [] });
|
|
185
|
-
|
|
186
|
-
expect(mockDriver.find).toHaveBeenCalled();
|
|
187
|
-
expect(mockDriver2.find).not.toHaveBeenCalled();
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
it('should route to specific driver when datasource is specified', async () => {
|
|
191
|
-
// Mock Schema: Object uses 'mongo' datasource
|
|
192
|
-
vi.mocked(SchemaRegistry.getObject).mockReturnValue({ name: 'log', datasource: 'mongo', fields: {} });
|
|
193
|
-
|
|
194
|
-
await engine.find('log', { filters: [] });
|
|
195
|
-
|
|
196
|
-
expect(mockDriver.find).not.toHaveBeenCalled();
|
|
197
|
-
expect(mockDriver2.find).toHaveBeenCalled();
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
it('should throw error if datasource is not found', async () => {
|
|
201
|
-
// Mock Schema: Object uses unknown datasource
|
|
202
|
-
vi.mocked(SchemaRegistry.getObject).mockReturnValue({ name: 'old_data', datasource: 'legacy_sql', fields: {} });
|
|
203
|
-
|
|
204
|
-
await expect(engine.find('old_data', {})).rejects.toThrow("Datasource 'legacy_sql' configured for object 'old_data' is not registered");
|
|
205
|
-
});
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
describe('CRUD Operations', () => {
|
|
209
|
-
beforeEach(async () => {
|
|
210
|
-
engine.registerDriver(mockDriver, true);
|
|
211
|
-
await engine.init();
|
|
212
|
-
vi.mocked(SchemaRegistry.getObject).mockReturnValue({ name: 'task', fields: {} });
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
it('should execute insert operation', async () => {
|
|
216
|
-
const result = await engine.insert('task', { title: 'New Task' });
|
|
217
|
-
expect(mockDriver.create).toHaveBeenCalledWith('task', { title: 'New Task' }, undefined);
|
|
218
|
-
expect(result).toEqual({ id: '1', success: true });
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
it('should execute find operation', async () => {
|
|
222
|
-
const result = await engine.find('task', {});
|
|
223
|
-
expect(mockDriver.find).toHaveBeenCalled();
|
|
224
|
-
expect(result).toHaveLength(1);
|
|
225
|
-
});
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
describe('Expand Related Records', () => {
|
|
229
|
-
beforeEach(async () => {
|
|
230
|
-
engine.registerDriver(mockDriver, true);
|
|
231
|
-
await engine.init();
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
it('should expand lookup fields by replacing IDs with full objects', async () => {
|
|
235
|
-
// Setup: task has a lookup field "assignee" → user object
|
|
236
|
-
vi.mocked(SchemaRegistry.getObject).mockImplementation((name) => {
|
|
237
|
-
if (name === 'task') return {
|
|
238
|
-
name: 'task',
|
|
239
|
-
fields: {
|
|
240
|
-
assignee: { type: 'lookup', reference: 'user' },
|
|
241
|
-
title: { type: 'text' },
|
|
242
|
-
},
|
|
243
|
-
} as any;
|
|
244
|
-
if (name === 'user') return {
|
|
245
|
-
name: 'user',
|
|
246
|
-
fields: {
|
|
247
|
-
name: { type: 'text' },
|
|
248
|
-
},
|
|
249
|
-
} as any;
|
|
250
|
-
return undefined;
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
// Primary find returns tasks with assignee IDs
|
|
254
|
-
vi.mocked(mockDriver.find)
|
|
255
|
-
.mockResolvedValueOnce([
|
|
256
|
-
{ id: 't1', title: 'Task 1', assignee: 'u1' },
|
|
257
|
-
{ id: 't2', title: 'Task 2', assignee: 'u2' },
|
|
258
|
-
])
|
|
259
|
-
// Second call (expand): returns user records
|
|
260
|
-
.mockResolvedValueOnce([
|
|
261
|
-
{ id: 'u1', name: 'Alice' },
|
|
262
|
-
{ id: 'u2', name: 'Bob' },
|
|
263
|
-
]);
|
|
264
|
-
|
|
265
|
-
const result = await engine.find('task', { expand: { assignee: { object: 'assignee' } } });
|
|
266
|
-
|
|
267
|
-
expect(result).toHaveLength(2);
|
|
268
|
-
expect(result[0].assignee).toEqual({ id: 'u1', name: 'Alice' });
|
|
269
|
-
expect(result[1].assignee).toEqual({ id: 'u2', name: 'Bob' });
|
|
270
|
-
|
|
271
|
-
// Verify the expand query used $in
|
|
272
|
-
expect(mockDriver.find).toHaveBeenCalledTimes(2);
|
|
273
|
-
expect(mockDriver.find).toHaveBeenLastCalledWith(
|
|
274
|
-
'user',
|
|
275
|
-
expect.objectContaining({
|
|
276
|
-
object: 'user',
|
|
277
|
-
where: { id: { $in: ['u1', 'u2'] } },
|
|
278
|
-
}),
|
|
279
|
-
);
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
it('should expand master_detail fields', async () => {
|
|
283
|
-
vi.mocked(SchemaRegistry.getObject).mockImplementation((name) => {
|
|
284
|
-
if (name === 'order_item') return {
|
|
285
|
-
name: 'order_item',
|
|
286
|
-
fields: {
|
|
287
|
-
order: { type: 'master_detail', reference: 'order' },
|
|
288
|
-
},
|
|
289
|
-
} as any;
|
|
290
|
-
if (name === 'order') return {
|
|
291
|
-
name: 'order',
|
|
292
|
-
fields: { total: { type: 'number' } },
|
|
293
|
-
} as any;
|
|
294
|
-
return undefined;
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
vi.mocked(mockDriver.find)
|
|
298
|
-
.mockResolvedValueOnce([
|
|
299
|
-
{ id: 'oi1', order: 'o1' },
|
|
300
|
-
])
|
|
301
|
-
.mockResolvedValueOnce([
|
|
302
|
-
{ id: 'o1', total: 100 },
|
|
303
|
-
]);
|
|
304
|
-
|
|
305
|
-
const result = await engine.find('order_item', { expand: { order: { object: 'order' } } });
|
|
306
|
-
expect(result[0].order).toEqual({ id: 'o1', total: 100 });
|
|
307
|
-
});
|
|
308
|
-
|
|
309
|
-
it('should skip expand for fields without reference definition', async () => {
|
|
310
|
-
vi.mocked(SchemaRegistry.getObject).mockReturnValue({
|
|
311
|
-
name: 'task',
|
|
312
|
-
fields: {
|
|
313
|
-
title: { type: 'text' }, // Not a lookup
|
|
314
|
-
},
|
|
315
|
-
} as any);
|
|
316
|
-
|
|
317
|
-
vi.mocked(mockDriver.find).mockResolvedValueOnce([
|
|
318
|
-
{ id: 't1', title: 'Task 1' },
|
|
319
|
-
]);
|
|
320
|
-
|
|
321
|
-
const result = await engine.find('task', { expand: { title: { object: 'title' } } });
|
|
322
|
-
expect(result[0].title).toBe('Task 1'); // Unchanged
|
|
323
|
-
expect(mockDriver.find).toHaveBeenCalledTimes(1); // No expand query
|
|
324
|
-
});
|
|
325
|
-
|
|
326
|
-
it('should skip expand if schema is not registered', async () => {
|
|
327
|
-
vi.mocked(SchemaRegistry.getObject).mockReturnValue(undefined);
|
|
328
|
-
|
|
329
|
-
vi.mocked(mockDriver.find).mockResolvedValueOnce([
|
|
330
|
-
{ id: 't1', assignee: 'u1' },
|
|
331
|
-
]);
|
|
332
|
-
|
|
333
|
-
const result = await engine.find('task', { expand: { assignee: { object: 'assignee' } } });
|
|
334
|
-
expect(result[0].assignee).toBe('u1'); // Unchanged — raw ID
|
|
335
|
-
expect(mockDriver.find).toHaveBeenCalledTimes(1);
|
|
336
|
-
});
|
|
337
|
-
|
|
338
|
-
it('should handle null values gracefully during expand', async () => {
|
|
339
|
-
vi.mocked(SchemaRegistry.getObject).mockImplementation((name) => {
|
|
340
|
-
if (name === 'task') return {
|
|
341
|
-
name: 'task',
|
|
342
|
-
fields: {
|
|
343
|
-
assignee: { type: 'lookup', reference: 'user' },
|
|
344
|
-
},
|
|
345
|
-
} as any;
|
|
346
|
-
if (name === 'user') return {
|
|
347
|
-
name: 'user',
|
|
348
|
-
fields: {},
|
|
349
|
-
} as any;
|
|
350
|
-
return undefined;
|
|
351
|
-
});
|
|
352
|
-
|
|
353
|
-
vi.mocked(mockDriver.find)
|
|
354
|
-
.mockResolvedValueOnce([
|
|
355
|
-
{ id: 't1', assignee: null },
|
|
356
|
-
{ id: 't2', assignee: 'u1' },
|
|
357
|
-
])
|
|
358
|
-
.mockResolvedValueOnce([
|
|
359
|
-
{ id: 'u1', name: 'Alice' },
|
|
360
|
-
]);
|
|
361
|
-
|
|
362
|
-
const result = await engine.find('task', { expand: { assignee: { object: 'assignee' } } });
|
|
363
|
-
expect(result[0].assignee).toBeNull();
|
|
364
|
-
expect(result[1].assignee).toEqual({ id: 'u1', name: 'Alice' });
|
|
365
|
-
});
|
|
366
|
-
|
|
367
|
-
it('should de-duplicate foreign key IDs in batch query', async () => {
|
|
368
|
-
vi.mocked(SchemaRegistry.getObject).mockImplementation((name) => {
|
|
369
|
-
if (name === 'task') return {
|
|
370
|
-
name: 'task',
|
|
371
|
-
fields: {
|
|
372
|
-
assignee: { type: 'lookup', reference: 'user' },
|
|
373
|
-
},
|
|
374
|
-
} as any;
|
|
375
|
-
if (name === 'user') return {
|
|
376
|
-
name: 'user',
|
|
377
|
-
fields: {},
|
|
378
|
-
} as any;
|
|
379
|
-
return undefined;
|
|
380
|
-
});
|
|
381
|
-
|
|
382
|
-
vi.mocked(mockDriver.find)
|
|
383
|
-
.mockResolvedValueOnce([
|
|
384
|
-
{ id: 't1', assignee: 'u1' },
|
|
385
|
-
{ id: 't2', assignee: 'u1' }, // Same user
|
|
386
|
-
{ id: 't3', assignee: 'u2' },
|
|
387
|
-
])
|
|
388
|
-
.mockResolvedValueOnce([
|
|
389
|
-
{ id: 'u1', name: 'Alice' },
|
|
390
|
-
{ id: 'u2', name: 'Bob' },
|
|
391
|
-
]);
|
|
392
|
-
|
|
393
|
-
const result = await engine.find('task', { expand: { assignee: { object: 'assignee' } } });
|
|
394
|
-
|
|
395
|
-
// Verify only 2 unique IDs queried
|
|
396
|
-
expect(mockDriver.find).toHaveBeenLastCalledWith(
|
|
397
|
-
'user',
|
|
398
|
-
expect.objectContaining({
|
|
399
|
-
where: { id: { $in: ['u1', 'u2'] } },
|
|
400
|
-
}),
|
|
401
|
-
);
|
|
402
|
-
expect(result[0].assignee).toEqual({ id: 'u1', name: 'Alice' });
|
|
403
|
-
expect(result[1].assignee).toEqual({ id: 'u1', name: 'Alice' });
|
|
404
|
-
});
|
|
405
|
-
|
|
406
|
-
it('should keep raw ID when referenced record not found', async () => {
|
|
407
|
-
vi.mocked(SchemaRegistry.getObject).mockImplementation((name) => {
|
|
408
|
-
if (name === 'task') return {
|
|
409
|
-
name: 'task',
|
|
410
|
-
fields: {
|
|
411
|
-
assignee: { type: 'lookup', reference: 'user' },
|
|
412
|
-
},
|
|
413
|
-
} as any;
|
|
414
|
-
if (name === 'user') return {
|
|
415
|
-
name: 'user',
|
|
416
|
-
fields: {},
|
|
417
|
-
} as any;
|
|
418
|
-
return undefined;
|
|
419
|
-
});
|
|
420
|
-
|
|
421
|
-
vi.mocked(mockDriver.find)
|
|
422
|
-
.mockResolvedValueOnce([
|
|
423
|
-
{ id: 't1', assignee: 'u_deleted' },
|
|
424
|
-
])
|
|
425
|
-
.mockResolvedValueOnce([]); // No records found
|
|
426
|
-
|
|
427
|
-
const result = await engine.find('task', { expand: { assignee: { object: 'assignee' } } });
|
|
428
|
-
expect(result[0].assignee).toBe('u_deleted'); // Fallback to raw ID
|
|
429
|
-
});
|
|
430
|
-
|
|
431
|
-
it('should expand multiple fields in a single query', async () => {
|
|
432
|
-
vi.mocked(SchemaRegistry.getObject).mockImplementation((name) => {
|
|
433
|
-
if (name === 'task') return {
|
|
434
|
-
name: 'task',
|
|
435
|
-
fields: {
|
|
436
|
-
assignee: { type: 'lookup', reference: 'user' },
|
|
437
|
-
project: { type: 'lookup', reference: 'project' },
|
|
438
|
-
},
|
|
439
|
-
} as any;
|
|
440
|
-
if (name === 'user') return {
|
|
441
|
-
name: 'user',
|
|
442
|
-
fields: {},
|
|
443
|
-
} as any;
|
|
444
|
-
if (name === 'project') return {
|
|
445
|
-
name: 'project',
|
|
446
|
-
fields: {},
|
|
447
|
-
} as any;
|
|
448
|
-
return undefined;
|
|
449
|
-
});
|
|
450
|
-
|
|
451
|
-
vi.mocked(mockDriver.find)
|
|
452
|
-
.mockResolvedValueOnce([
|
|
453
|
-
{ id: 't1', assignee: 'u1', project: 'p1' },
|
|
454
|
-
])
|
|
455
|
-
.mockResolvedValueOnce([{ id: 'u1', name: 'Alice' }])
|
|
456
|
-
.mockResolvedValueOnce([{ id: 'p1', name: 'Project X' }]);
|
|
457
|
-
|
|
458
|
-
const result = await engine.find('task', { expand: { assignee: { object: 'assignee' }, project: { object: 'project' } } });
|
|
459
|
-
|
|
460
|
-
expect(result[0].assignee).toEqual({ id: 'u1', name: 'Alice' });
|
|
461
|
-
expect(result[0].project).toEqual({ id: 'p1', name: 'Project X' });
|
|
462
|
-
expect(mockDriver.find).toHaveBeenCalledTimes(3);
|
|
463
|
-
});
|
|
464
|
-
|
|
465
|
-
it('should work with findOne and expand', async () => {
|
|
466
|
-
vi.mocked(SchemaRegistry.getObject).mockImplementation((name) => {
|
|
467
|
-
if (name === 'task') return {
|
|
468
|
-
name: 'task',
|
|
469
|
-
fields: {
|
|
470
|
-
assignee: { type: 'lookup', reference: 'user' },
|
|
471
|
-
},
|
|
472
|
-
} as any;
|
|
473
|
-
if (name === 'user') return {
|
|
474
|
-
name: 'user',
|
|
475
|
-
fields: {},
|
|
476
|
-
} as any;
|
|
477
|
-
return undefined;
|
|
478
|
-
});
|
|
479
|
-
|
|
480
|
-
vi.mocked(mockDriver.findOne as any).mockResolvedValueOnce(
|
|
481
|
-
{ id: 't1', title: 'Task 1', assignee: 'u1' },
|
|
482
|
-
);
|
|
483
|
-
vi.mocked(mockDriver.find).mockResolvedValueOnce([
|
|
484
|
-
{ id: 'u1', name: 'Alice' },
|
|
485
|
-
]);
|
|
486
|
-
|
|
487
|
-
const result = await engine.findOne('task', { expand: { assignee: { object: 'assignee' } } });
|
|
488
|
-
|
|
489
|
-
expect(result.assignee).toEqual({ id: 'u1', name: 'Alice' });
|
|
490
|
-
});
|
|
491
|
-
|
|
492
|
-
it('should handle already-expanded objects (skip re-expansion)', async () => {
|
|
493
|
-
vi.mocked(SchemaRegistry.getObject).mockImplementation((name) => {
|
|
494
|
-
if (name === 'task') return {
|
|
495
|
-
name: 'task',
|
|
496
|
-
fields: {
|
|
497
|
-
assignee: { type: 'lookup', reference: 'user' },
|
|
498
|
-
},
|
|
499
|
-
} as any;
|
|
500
|
-
if (name === 'user') return {
|
|
501
|
-
name: 'user',
|
|
502
|
-
fields: {},
|
|
503
|
-
} as any;
|
|
504
|
-
return undefined;
|
|
505
|
-
});
|
|
506
|
-
|
|
507
|
-
// Driver returns an already-expanded object
|
|
508
|
-
vi.mocked(mockDriver.find).mockResolvedValueOnce([
|
|
509
|
-
{ id: 't1', assignee: { id: 'u1', name: 'Alice' } },
|
|
510
|
-
]);
|
|
511
|
-
|
|
512
|
-
const result = await engine.find('task', { expand: { assignee: { object: 'assignee' } } });
|
|
513
|
-
|
|
514
|
-
// No expand query should have been made — the value was already an object
|
|
515
|
-
expect(mockDriver.find).toHaveBeenCalledTimes(1);
|
|
516
|
-
expect(result[0].assignee).toEqual({ id: 'u1', name: 'Alice' });
|
|
517
|
-
});
|
|
518
|
-
|
|
519
|
-
it('should gracefully handle expand errors and keep raw IDs', async () => {
|
|
520
|
-
vi.mocked(SchemaRegistry.getObject).mockImplementation((name) => {
|
|
521
|
-
if (name === 'task') return {
|
|
522
|
-
name: 'task',
|
|
523
|
-
fields: {
|
|
524
|
-
assignee: { type: 'lookup', reference: 'user' },
|
|
525
|
-
},
|
|
526
|
-
} as any;
|
|
527
|
-
if (name === 'user') return {
|
|
528
|
-
name: 'user',
|
|
529
|
-
fields: {},
|
|
530
|
-
} as any;
|
|
531
|
-
return undefined;
|
|
532
|
-
});
|
|
533
|
-
|
|
534
|
-
vi.mocked(mockDriver.find)
|
|
535
|
-
.mockResolvedValueOnce([
|
|
536
|
-
{ id: 't1', assignee: 'u1' },
|
|
537
|
-
])
|
|
538
|
-
.mockRejectedValueOnce(new Error('Driver connection failed'));
|
|
539
|
-
|
|
540
|
-
const result = await engine.find('task', { expand: { assignee: { object: 'assignee' } } });
|
|
541
|
-
expect(result[0].assignee).toBe('u1'); // Kept raw ID
|
|
542
|
-
});
|
|
543
|
-
|
|
544
|
-
it('should handle multi-value lookup fields (arrays)', async () => {
|
|
545
|
-
vi.mocked(SchemaRegistry.getObject).mockImplementation((name) => {
|
|
546
|
-
if (name === 'task') return {
|
|
547
|
-
name: 'task',
|
|
548
|
-
fields: {
|
|
549
|
-
watchers: { type: 'lookup', reference: 'user', multiple: true },
|
|
550
|
-
},
|
|
551
|
-
} as any;
|
|
552
|
-
if (name === 'user') return {
|
|
553
|
-
name: 'user',
|
|
554
|
-
fields: {},
|
|
555
|
-
} as any;
|
|
556
|
-
return undefined;
|
|
557
|
-
});
|
|
558
|
-
|
|
559
|
-
vi.mocked(mockDriver.find)
|
|
560
|
-
.mockResolvedValueOnce([
|
|
561
|
-
{ id: 't1', watchers: ['u1', 'u2'] },
|
|
562
|
-
])
|
|
563
|
-
.mockResolvedValueOnce([
|
|
564
|
-
{ id: 'u1', name: 'Alice' },
|
|
565
|
-
{ id: 'u2', name: 'Bob' },
|
|
566
|
-
]);
|
|
567
|
-
|
|
568
|
-
const result = await engine.find('task', { expand: { watchers: { object: 'watchers' } } });
|
|
569
|
-
expect(result[0].watchers).toEqual([
|
|
570
|
-
{ id: 'u1', name: 'Alice' },
|
|
571
|
-
{ id: 'u2', name: 'Bob' },
|
|
572
|
-
]);
|
|
573
|
-
});
|
|
574
|
-
|
|
575
|
-
it('should expand only fields specified in the expand map (populate creates flat expand)', async () => {
|
|
576
|
-
// populate: ['project'] creates expand: { project: { object: 'project' } } (1 level only)
|
|
577
|
-
// Nested fields like project.org should NOT be expanded unless explicitly nested in the AST
|
|
578
|
-
vi.mocked(SchemaRegistry.getObject).mockImplementation((name) => {
|
|
579
|
-
const schemas: Record<string, any> = {
|
|
580
|
-
task: { name: 'task', fields: { project: { type: 'lookup', reference: 'project' } } },
|
|
581
|
-
project: { name: 'project', fields: { org: { type: 'lookup', reference: 'org' } } },
|
|
582
|
-
};
|
|
583
|
-
return schemas[name] as any;
|
|
584
|
-
});
|
|
585
|
-
|
|
586
|
-
vi.mocked(mockDriver.find)
|
|
587
|
-
.mockResolvedValueOnce([{ id: 't1', project: 'p1' }]) // find task
|
|
588
|
-
.mockResolvedValueOnce([{ id: 'p1', org: 'o1' }]); // expand project (depth 0)
|
|
589
|
-
// org should NOT be expanded further — flat populate doesn't create nested expand
|
|
590
|
-
|
|
591
|
-
const result = await engine.find('task', { expand: { project: { object: 'project' } } });
|
|
592
|
-
|
|
593
|
-
// Project expanded, but org inside project remains as raw ID
|
|
594
|
-
expect(result[0].project).toEqual({ id: 'p1', org: 'o1' });
|
|
595
|
-
expect(mockDriver.find).toHaveBeenCalledTimes(2); // Only primary + 1 expand query
|
|
596
|
-
});
|
|
597
|
-
|
|
598
|
-
it('should return records unchanged when expand map is empty', async () => {
|
|
599
|
-
vi.mocked(SchemaRegistry.getObject).mockReturnValue({
|
|
600
|
-
name: 'task',
|
|
601
|
-
fields: {},
|
|
602
|
-
} as any);
|
|
603
|
-
|
|
604
|
-
vi.mocked(mockDriver.find).mockResolvedValueOnce([
|
|
605
|
-
{ id: 't1', title: 'Task 1' },
|
|
606
|
-
]);
|
|
607
|
-
|
|
608
|
-
const result = await engine.find('task', {});
|
|
609
|
-
expect(result).toEqual([{ id: 't1', title: 'Task 1' }]);
|
|
610
|
-
expect(mockDriver.find).toHaveBeenCalledTimes(1);
|
|
611
|
-
});
|
|
612
|
-
});
|
|
613
|
-
});
|