@objectstack/objectql 3.3.0 → 4.0.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/.turbo/turbo-build.log +10 -10
- package/CHANGELOG.md +33 -0
- package/dist/index.d.mts +34 -20
- package/dist/index.d.ts +34 -20
- package/dist/index.js +282 -122
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +282 -122
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -6
- package/src/engine.test.ts +13 -13
- package/src/engine.ts +36 -77
- package/src/plugin.integration.test.ts +212 -0
- package/src/plugin.ts +73 -18
- package/src/protocol-data.test.ts +41 -38
- package/src/protocol-discovery.test.ts +25 -25
- package/src/protocol-meta.test.ts +440 -0
- package/src/protocol.ts +258 -68
- package/tsconfig.json +2 -1
package/src/plugin.ts
CHANGED
|
@@ -175,7 +175,7 @@ export class ObjectQLPlugin implements Plugin {
|
|
|
175
175
|
if (hookCtx.input?.id && !hookCtx.previous) {
|
|
176
176
|
try {
|
|
177
177
|
const existing = await this.ql!.findOne(hookCtx.object, {
|
|
178
|
-
|
|
178
|
+
where: { id: hookCtx.input.id }
|
|
179
179
|
});
|
|
180
180
|
if (existing) {
|
|
181
181
|
hookCtx.previous = existing;
|
|
@@ -191,7 +191,7 @@ export class ObjectQLPlugin implements Plugin {
|
|
|
191
191
|
if (hookCtx.input?.id && !hookCtx.previous) {
|
|
192
192
|
try {
|
|
193
193
|
const existing = await this.ql!.findOne(hookCtx.object, {
|
|
194
|
-
|
|
194
|
+
where: { id: hookCtx.input.id }
|
|
195
195
|
});
|
|
196
196
|
if (existing) {
|
|
197
197
|
hookCtx.previous = existing;
|
|
@@ -239,9 +239,13 @@ export class ObjectQLPlugin implements Plugin {
|
|
|
239
239
|
/**
|
|
240
240
|
* Synchronize all registered object schemas to the database.
|
|
241
241
|
*
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
*
|
|
242
|
+
* Groups objects by their responsible driver, then:
|
|
243
|
+
* - If the driver advertises `supports.batchSchemaSync` and implements
|
|
244
|
+
* `syncSchemasBatch()`, submits all schemas in a single call (reducing
|
|
245
|
+
* network round-trips for remote drivers like Turso).
|
|
246
|
+
* - Otherwise falls back to sequential `syncSchema()` per object.
|
|
247
|
+
*
|
|
248
|
+
* This is idempotent — drivers must tolerate repeated calls without
|
|
245
249
|
* duplicating tables or erroring out.
|
|
246
250
|
*
|
|
247
251
|
* Drivers that do not implement `syncSchema` are silently skipped.
|
|
@@ -255,6 +259,9 @@ export class ObjectQLPlugin implements Plugin {
|
|
|
255
259
|
let synced = 0;
|
|
256
260
|
let skipped = 0;
|
|
257
261
|
|
|
262
|
+
// Group objects by driver for potential batch optimization
|
|
263
|
+
const driverGroups = new Map<any, Array<{ obj: any; tableName: string }>>();
|
|
264
|
+
|
|
258
265
|
for (const obj of allObjects) {
|
|
259
266
|
const driver = this.ql.getDriverForObject(obj.name);
|
|
260
267
|
if (!driver) {
|
|
@@ -274,21 +281,69 @@ export class ObjectQLPlugin implements Plugin {
|
|
|
274
281
|
continue;
|
|
275
282
|
}
|
|
276
283
|
|
|
277
|
-
// Use the physical table name (e.g., 'sys_user') for DDL operations
|
|
278
|
-
// instead of the FQN (e.g., 'sys__user'). ObjectSchema.create()
|
|
279
|
-
// auto-derives tableName as {namespace}_{name}.
|
|
280
284
|
const tableName = obj.tableName || obj.name;
|
|
281
285
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
286
|
+
let group = driverGroups.get(driver);
|
|
287
|
+
if (!group) {
|
|
288
|
+
group = [];
|
|
289
|
+
driverGroups.set(driver, group);
|
|
290
|
+
}
|
|
291
|
+
group.push({ obj, tableName });
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Process each driver group
|
|
295
|
+
for (const [driver, entries] of driverGroups) {
|
|
296
|
+
// Batch path: driver supports batch schema sync
|
|
297
|
+
if (
|
|
298
|
+
driver.supports?.batchSchemaSync &&
|
|
299
|
+
typeof driver.syncSchemasBatch === 'function'
|
|
300
|
+
) {
|
|
301
|
+
const batchPayload = entries.map((e) => ({
|
|
302
|
+
object: e.tableName,
|
|
303
|
+
schema: e.obj,
|
|
304
|
+
}));
|
|
305
|
+
try {
|
|
306
|
+
await driver.syncSchemasBatch(batchPayload);
|
|
307
|
+
synced += entries.length;
|
|
308
|
+
ctx.logger.debug('Batch schema sync succeeded', {
|
|
309
|
+
driver: driver.name,
|
|
310
|
+
count: entries.length,
|
|
311
|
+
});
|
|
312
|
+
} catch (e: unknown) {
|
|
313
|
+
ctx.logger.warn('Batch schema sync failed, falling back to sequential', {
|
|
314
|
+
driver: driver.name,
|
|
315
|
+
error: e instanceof Error ? e.message : String(e),
|
|
316
|
+
});
|
|
317
|
+
// Fallback: sequential sync for this driver's objects
|
|
318
|
+
for (const { obj, tableName } of entries) {
|
|
319
|
+
try {
|
|
320
|
+
await driver.syncSchema(tableName, obj);
|
|
321
|
+
synced++;
|
|
322
|
+
} catch (seqErr: unknown) {
|
|
323
|
+
ctx.logger.warn('Failed to sync schema for object', {
|
|
324
|
+
object: obj.name,
|
|
325
|
+
tableName,
|
|
326
|
+
driver: driver.name,
|
|
327
|
+
error: seqErr instanceof Error ? seqErr.message : String(seqErr),
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
} else {
|
|
333
|
+
// Sequential path: no batch support
|
|
334
|
+
for (const { obj, tableName } of entries) {
|
|
335
|
+
try {
|
|
336
|
+
await driver.syncSchema(tableName, obj);
|
|
337
|
+
synced++;
|
|
338
|
+
} catch (e: unknown) {
|
|
339
|
+
ctx.logger.warn('Failed to sync schema for object', {
|
|
340
|
+
object: obj.name,
|
|
341
|
+
tableName,
|
|
342
|
+
driver: driver.name,
|
|
343
|
+
error: e instanceof Error ? e.message : String(e),
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
}
|
|
292
347
|
}
|
|
293
348
|
}
|
|
294
349
|
|
|
@@ -25,87 +25,88 @@ describe('ObjectStackProtocolImplementation - Data Operations', () => {
|
|
|
25
25
|
// ═══════════════════════════════════════════════════════════════
|
|
26
26
|
|
|
27
27
|
describe('findData', () => {
|
|
28
|
-
it('should normalize expand string to
|
|
29
|
-
await protocol.findData({ object: 'order_item', query: { expand: 'order,product' } });
|
|
28
|
+
it('should normalize $expand (OData) string to expand Record', async () => {
|
|
29
|
+
await protocol.findData({ object: 'order_item', query: { $expand: 'order,product' } });
|
|
30
30
|
|
|
31
31
|
expect(mockEngine.find).toHaveBeenCalledWith(
|
|
32
32
|
'order_item',
|
|
33
33
|
expect.objectContaining({
|
|
34
|
-
|
|
34
|
+
expand: { order: { object: 'order' }, product: { object: 'product' } },
|
|
35
35
|
}),
|
|
36
36
|
);
|
|
37
|
-
// expand should be deleted from options
|
|
37
|
+
// $expand should be deleted from options
|
|
38
38
|
const callArgs = mockEngine.find.mock.calls[0][1];
|
|
39
|
-
expect(callArgs.expand).toBeUndefined();
|
|
40
39
|
expect(callArgs.$expand).toBeUndefined();
|
|
41
40
|
});
|
|
42
41
|
|
|
43
|
-
it('should normalize $expand (OData) to
|
|
42
|
+
it('should normalize $expand (OData) with different fields to expand Record', async () => {
|
|
44
43
|
await protocol.findData({ object: 'task', query: { $expand: 'assignee,project' } });
|
|
45
44
|
|
|
46
45
|
expect(mockEngine.find).toHaveBeenCalledWith(
|
|
47
46
|
'task',
|
|
48
47
|
expect.objectContaining({
|
|
49
|
-
|
|
48
|
+
expand: { assignee: { object: 'assignee' }, project: { object: 'project' } },
|
|
50
49
|
}),
|
|
51
50
|
);
|
|
52
51
|
});
|
|
53
52
|
|
|
54
|
-
it('should
|
|
53
|
+
it('should normalize populate array to expand Record', async () => {
|
|
55
54
|
await protocol.findData({ object: 'task', query: { populate: ['assignee'] } });
|
|
56
55
|
|
|
57
56
|
expect(mockEngine.find).toHaveBeenCalledWith(
|
|
58
57
|
'task',
|
|
59
58
|
expect.objectContaining({
|
|
60
|
-
|
|
59
|
+
expand: { assignee: { object: 'assignee' } },
|
|
61
60
|
}),
|
|
62
61
|
);
|
|
63
62
|
});
|
|
64
63
|
|
|
65
|
-
it('should normalize populate string to
|
|
64
|
+
it('should normalize populate string to expand Record', async () => {
|
|
66
65
|
await protocol.findData({ object: 'task', query: { populate: 'assignee,project' } });
|
|
67
66
|
|
|
68
67
|
expect(mockEngine.find).toHaveBeenCalledWith(
|
|
69
68
|
'task',
|
|
70
69
|
expect.objectContaining({
|
|
71
|
-
|
|
70
|
+
expand: { assignee: { object: 'assignee' }, project: { object: 'project' } },
|
|
72
71
|
}),
|
|
73
72
|
);
|
|
74
73
|
});
|
|
75
74
|
|
|
76
|
-
it('should prefer
|
|
75
|
+
it('should prefer populate names over expand string when both provided', async () => {
|
|
77
76
|
await protocol.findData({
|
|
78
77
|
object: 'task',
|
|
79
78
|
query: { populate: ['assignee'], expand: 'project' },
|
|
80
79
|
});
|
|
81
80
|
|
|
82
|
-
// populate
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
);
|
|
81
|
+
// populate names take precedence; the non-object expand string is
|
|
82
|
+
// cleaned up first, then populate-derived names create the Record.
|
|
83
|
+
const callArgs = mockEngine.find.mock.calls[0][1];
|
|
84
|
+
expect(callArgs.populate).toBeUndefined();
|
|
85
|
+
expect(callArgs.$expand).toBeUndefined();
|
|
86
|
+
expect(callArgs.expand).toEqual({ assignee: { object: 'assignee' } });
|
|
89
87
|
});
|
|
90
88
|
|
|
91
|
-
it('should
|
|
92
|
-
await protocol.findData({
|
|
89
|
+
it('should pass expand Record object through as-is', async () => {
|
|
90
|
+
await protocol.findData({
|
|
91
|
+
object: 'task',
|
|
92
|
+
query: { expand: { owner: { object: 'owner' }, team: { object: 'team' } } },
|
|
93
|
+
});
|
|
93
94
|
|
|
94
95
|
expect(mockEngine.find).toHaveBeenCalledWith(
|
|
95
96
|
'task',
|
|
96
97
|
expect.objectContaining({
|
|
97
|
-
|
|
98
|
+
expand: { owner: { object: 'owner' }, team: { object: 'team' } },
|
|
98
99
|
}),
|
|
99
100
|
);
|
|
100
101
|
});
|
|
101
102
|
|
|
102
|
-
it('should normalize select string to array', async () => {
|
|
103
|
+
it('should normalize select string to fields array', async () => {
|
|
103
104
|
await protocol.findData({ object: 'task', query: { select: 'name,status,assignee' } });
|
|
104
105
|
|
|
105
106
|
expect(mockEngine.find).toHaveBeenCalledWith(
|
|
106
107
|
'task',
|
|
107
108
|
expect.objectContaining({
|
|
108
|
-
|
|
109
|
+
fields: ['name', 'status', 'assignee'],
|
|
109
110
|
}),
|
|
110
111
|
);
|
|
111
112
|
});
|
|
@@ -116,8 +117,8 @@ describe('ObjectStackProtocolImplementation - Data Operations', () => {
|
|
|
116
117
|
expect(mockEngine.find).toHaveBeenCalledWith(
|
|
117
118
|
'task',
|
|
118
119
|
expect.objectContaining({
|
|
119
|
-
|
|
120
|
-
|
|
120
|
+
limit: 10,
|
|
121
|
+
offset: 20,
|
|
121
122
|
}),
|
|
122
123
|
);
|
|
123
124
|
});
|
|
@@ -148,7 +149,7 @@ describe('ObjectStackProtocolImplementation - Data Operations', () => {
|
|
|
148
149
|
// ═══════════════════════════════════════════════════════════════
|
|
149
150
|
|
|
150
151
|
describe('getData', () => {
|
|
151
|
-
it('should convert expand string to
|
|
152
|
+
it('should convert expand string to expand Record', async () => {
|
|
152
153
|
mockEngine.findOne.mockResolvedValue({ id: 'oi_1', name: 'Item 1' });
|
|
153
154
|
|
|
154
155
|
await protocol.getData({ object: 'order_item', id: 'oi_1', expand: 'order,product' });
|
|
@@ -156,13 +157,13 @@ describe('ObjectStackProtocolImplementation - Data Operations', () => {
|
|
|
156
157
|
expect(mockEngine.findOne).toHaveBeenCalledWith(
|
|
157
158
|
'order_item',
|
|
158
159
|
expect.objectContaining({
|
|
159
|
-
|
|
160
|
-
|
|
160
|
+
where: { id: 'oi_1' },
|
|
161
|
+
expand: { order: { object: 'order' }, product: { object: 'product' } },
|
|
161
162
|
}),
|
|
162
163
|
);
|
|
163
164
|
});
|
|
164
165
|
|
|
165
|
-
it('should convert expand array to
|
|
166
|
+
it('should convert expand array to expand Record', async () => {
|
|
166
167
|
mockEngine.findOne.mockResolvedValue({ id: 't1' });
|
|
167
168
|
|
|
168
169
|
await protocol.getData({ object: 'task', id: 't1', expand: ['assignee', 'project'] });
|
|
@@ -170,12 +171,13 @@ describe('ObjectStackProtocolImplementation - Data Operations', () => {
|
|
|
170
171
|
expect(mockEngine.findOne).toHaveBeenCalledWith(
|
|
171
172
|
'task',
|
|
172
173
|
expect.objectContaining({
|
|
173
|
-
|
|
174
|
+
where: { id: 't1' },
|
|
175
|
+
expand: { assignee: { object: 'assignee' }, project: { object: 'project' } },
|
|
174
176
|
}),
|
|
175
177
|
);
|
|
176
178
|
});
|
|
177
179
|
|
|
178
|
-
it('should convert select string to array', async () => {
|
|
180
|
+
it('should convert select string to fields array', async () => {
|
|
179
181
|
mockEngine.findOne.mockResolvedValue({ id: 't1', name: 'Test' });
|
|
180
182
|
|
|
181
183
|
await protocol.getData({ object: 'task', id: 't1', select: 'name,status' });
|
|
@@ -183,12 +185,13 @@ describe('ObjectStackProtocolImplementation - Data Operations', () => {
|
|
|
183
185
|
expect(mockEngine.findOne).toHaveBeenCalledWith(
|
|
184
186
|
'task',
|
|
185
187
|
expect.objectContaining({
|
|
186
|
-
|
|
188
|
+
where: { id: 't1' },
|
|
189
|
+
fields: ['name', 'status'],
|
|
187
190
|
}),
|
|
188
191
|
);
|
|
189
192
|
});
|
|
190
193
|
|
|
191
|
-
it('should pass both expand and
|
|
194
|
+
it('should pass both expand and fields together', async () => {
|
|
192
195
|
mockEngine.findOne.mockResolvedValue({ id: 'oi_1' });
|
|
193
196
|
|
|
194
197
|
await protocol.getData({
|
|
@@ -201,9 +204,9 @@ describe('ObjectStackProtocolImplementation - Data Operations', () => {
|
|
|
201
204
|
expect(mockEngine.findOne).toHaveBeenCalledWith(
|
|
202
205
|
'order_item',
|
|
203
206
|
expect.objectContaining({
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
+
where: { id: 'oi_1' },
|
|
208
|
+
expand: { order: { object: 'order' } },
|
|
209
|
+
fields: ['name', 'total'],
|
|
207
210
|
}),
|
|
208
211
|
);
|
|
209
212
|
});
|
|
@@ -215,7 +218,7 @@ describe('ObjectStackProtocolImplementation - Data Operations', () => {
|
|
|
215
218
|
|
|
216
219
|
expect(mockEngine.findOne).toHaveBeenCalledWith(
|
|
217
220
|
'task',
|
|
218
|
-
{
|
|
221
|
+
{ where: { id: 't1' } },
|
|
219
222
|
);
|
|
220
223
|
});
|
|
221
224
|
|
|
@@ -92,7 +92,7 @@ describe('ObjectStackProtocolImplementation - Dynamic Service Discovery', () =>
|
|
|
92
92
|
|
|
93
93
|
// Core services should always be available
|
|
94
94
|
expect(discovery.services.metadata.enabled).toBe(true);
|
|
95
|
-
expect(discovery.services.metadata.status).toBe('
|
|
95
|
+
expect(discovery.services.metadata.status).toBe('available');
|
|
96
96
|
expect(discovery.services.data.enabled).toBe(true);
|
|
97
97
|
expect(discovery.services.data.status).toBe('available');
|
|
98
98
|
expect(discovery.services.analytics.enabled).toBe(true);
|
|
@@ -148,14 +148,14 @@ describe('ObjectStackProtocolImplementation - Dynamic Service Discovery', () =>
|
|
|
148
148
|
expect(discovery.capabilities).toBeDefined();
|
|
149
149
|
// workflow is registered but doesn't map to a well-known capability directly
|
|
150
150
|
expect(discovery.services.workflow.enabled).toBe(true);
|
|
151
|
-
// All well-known capabilities should be
|
|
152
|
-
expect(discovery.capabilities!.feed).
|
|
153
|
-
expect(discovery.capabilities!.comments).
|
|
154
|
-
expect(discovery.capabilities!.automation).
|
|
155
|
-
expect(discovery.capabilities!.cron).
|
|
156
|
-
expect(discovery.capabilities!.search).
|
|
157
|
-
expect(discovery.capabilities!.export).
|
|
158
|
-
expect(discovery.capabilities!.chunkedUpload).
|
|
151
|
+
// All well-known capabilities should be disabled since workflow doesn't map to any
|
|
152
|
+
expect(discovery.capabilities!.feed).toEqual({ enabled: false });
|
|
153
|
+
expect(discovery.capabilities!.comments).toEqual({ enabled: false });
|
|
154
|
+
expect(discovery.capabilities!.automation).toEqual({ enabled: false });
|
|
155
|
+
expect(discovery.capabilities!.cron).toEqual({ enabled: false });
|
|
156
|
+
expect(discovery.capabilities!.search).toEqual({ enabled: false });
|
|
157
|
+
expect(discovery.capabilities!.export).toEqual({ enabled: false });
|
|
158
|
+
expect(discovery.capabilities!.chunkedUpload).toEqual({ enabled: false });
|
|
159
159
|
});
|
|
160
160
|
|
|
161
161
|
it('should set all capabilities to false when no services are registered', async () => {
|
|
@@ -163,13 +163,13 @@ describe('ObjectStackProtocolImplementation - Dynamic Service Discovery', () =>
|
|
|
163
163
|
const discovery = await protocol.getDiscovery();
|
|
164
164
|
|
|
165
165
|
expect(discovery.capabilities).toBeDefined();
|
|
166
|
-
expect(discovery.capabilities!.feed).
|
|
167
|
-
expect(discovery.capabilities!.comments).
|
|
168
|
-
expect(discovery.capabilities!.automation).
|
|
169
|
-
expect(discovery.capabilities!.cron).
|
|
170
|
-
expect(discovery.capabilities!.search).
|
|
171
|
-
expect(discovery.capabilities!.export).
|
|
172
|
-
expect(discovery.capabilities!.chunkedUpload).
|
|
166
|
+
expect(discovery.capabilities!.feed).toEqual({ enabled: false });
|
|
167
|
+
expect(discovery.capabilities!.comments).toEqual({ enabled: false });
|
|
168
|
+
expect(discovery.capabilities!.automation).toEqual({ enabled: false });
|
|
169
|
+
expect(discovery.capabilities!.cron).toEqual({ enabled: false });
|
|
170
|
+
expect(discovery.capabilities!.search).toEqual({ enabled: false });
|
|
171
|
+
expect(discovery.capabilities!.export).toEqual({ enabled: false });
|
|
172
|
+
expect(discovery.capabilities!.chunkedUpload).toEqual({ enabled: false });
|
|
173
173
|
});
|
|
174
174
|
|
|
175
175
|
it('should dynamically set capabilities based on registered services', async () => {
|
|
@@ -182,13 +182,13 @@ describe('ObjectStackProtocolImplementation - Dynamic Service Discovery', () =>
|
|
|
182
182
|
protocol = new ObjectStackProtocolImplementation(engine, () => mockServices);
|
|
183
183
|
const discovery = await protocol.getDiscovery();
|
|
184
184
|
|
|
185
|
-
expect(discovery.capabilities!.feed).
|
|
186
|
-
expect(discovery.capabilities!.comments).
|
|
187
|
-
expect(discovery.capabilities!.automation).
|
|
188
|
-
expect(discovery.capabilities!.cron).
|
|
189
|
-
expect(discovery.capabilities!.search).
|
|
190
|
-
expect(discovery.capabilities!.export).
|
|
191
|
-
expect(discovery.capabilities!.chunkedUpload).
|
|
185
|
+
expect(discovery.capabilities!.feed).toEqual({ enabled: true });
|
|
186
|
+
expect(discovery.capabilities!.comments).toEqual({ enabled: true });
|
|
187
|
+
expect(discovery.capabilities!.automation).toEqual({ enabled: true });
|
|
188
|
+
expect(discovery.capabilities!.cron).toEqual({ enabled: false });
|
|
189
|
+
expect(discovery.capabilities!.search).toEqual({ enabled: true });
|
|
190
|
+
expect(discovery.capabilities!.export).toEqual({ enabled: true });
|
|
191
|
+
expect(discovery.capabilities!.chunkedUpload).toEqual({ enabled: true });
|
|
192
192
|
});
|
|
193
193
|
|
|
194
194
|
it('should enable cron capability when job service is registered', async () => {
|
|
@@ -198,7 +198,7 @@ describe('ObjectStackProtocolImplementation - Dynamic Service Discovery', () =>
|
|
|
198
198
|
protocol = new ObjectStackProtocolImplementation(engine, () => mockServices);
|
|
199
199
|
const discovery = await protocol.getDiscovery();
|
|
200
200
|
|
|
201
|
-
expect(discovery.capabilities!.cron).
|
|
201
|
+
expect(discovery.capabilities!.cron).toEqual({ enabled: true });
|
|
202
202
|
});
|
|
203
203
|
|
|
204
204
|
it('should enable export capability when queue service is registered', async () => {
|
|
@@ -208,6 +208,6 @@ describe('ObjectStackProtocolImplementation - Dynamic Service Discovery', () =>
|
|
|
208
208
|
protocol = new ObjectStackProtocolImplementation(engine, () => mockServices);
|
|
209
209
|
const discovery = await protocol.getDiscovery();
|
|
210
210
|
|
|
211
|
-
expect(discovery.capabilities!.export).
|
|
211
|
+
expect(discovery.capabilities!.export).toEqual({ enabled: true });
|
|
212
212
|
});
|
|
213
213
|
});
|