@objectstack/client 4.0.2 → 4.0.4
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 +15 -0
- package/dist/index.d.mts +84 -3
- package/dist/index.d.ts +84 -3
- package/dist/index.js +162 -14
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +161 -14
- package/dist/index.mjs.map +1 -1
- package/package.json +12 -12
- package/src/client.hono.test.ts +11 -3
- package/src/index.ts +58 -28
- package/src/realtime-api.ts +208 -0
- package/tests/integration/01-discovery.test.ts +5 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@objectstack/client",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.4",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"description": "Official Client SDK for ObjectStack Protocol",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -13,20 +13,20 @@
|
|
|
13
13
|
}
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@objectstack/core": "4.0.
|
|
17
|
-
"@objectstack/spec": "4.0.
|
|
16
|
+
"@objectstack/core": "4.0.4",
|
|
17
|
+
"@objectstack/spec": "4.0.4"
|
|
18
18
|
},
|
|
19
19
|
"devDependencies": {
|
|
20
|
-
"@hono/node-server": "^1.19.
|
|
21
|
-
"msw": "^2.
|
|
20
|
+
"@hono/node-server": "^1.19.14",
|
|
21
|
+
"msw": "^2.13.2",
|
|
22
22
|
"typescript": "^6.0.2",
|
|
23
|
-
"vitest": "^4.1.
|
|
24
|
-
"@objectstack/driver-memory": "4.0.
|
|
25
|
-
"@objectstack/hono": "4.0.
|
|
26
|
-
"@objectstack/objectql": "4.0.
|
|
27
|
-
"@objectstack/plugin-hono-server": "4.0.
|
|
28
|
-
"@objectstack/plugin-msw": "4.0.
|
|
29
|
-
"@objectstack/runtime": "4.0.
|
|
23
|
+
"vitest": "^4.1.4",
|
|
24
|
+
"@objectstack/driver-memory": "4.0.4",
|
|
25
|
+
"@objectstack/hono": "4.0.4",
|
|
26
|
+
"@objectstack/objectql": "4.0.4",
|
|
27
|
+
"@objectstack/plugin-hono-server": "4.0.4",
|
|
28
|
+
"@objectstack/plugin-msw": "4.0.4",
|
|
29
|
+
"@objectstack/runtime": "4.0.4"
|
|
30
30
|
},
|
|
31
31
|
"scripts": {
|
|
32
32
|
"build": "tsup --config ../../tsup.config.ts",
|
package/src/client.hono.test.ts
CHANGED
|
@@ -107,11 +107,19 @@ describe('ObjectStackClient (with Hono Server)', () => {
|
|
|
107
107
|
baseUrl = `http://localhost:${port}`;
|
|
108
108
|
|
|
109
109
|
console.log(`Test server running at ${baseUrl}`);
|
|
110
|
-
});
|
|
110
|
+
}, 30_000);
|
|
111
111
|
|
|
112
112
|
afterAll(async () => {
|
|
113
|
-
if (kernel)
|
|
114
|
-
|
|
113
|
+
if (kernel) {
|
|
114
|
+
// Race shutdown against a hard deadline.
|
|
115
|
+
// kernel.shutdown() can hang when pino's flush callback never fires
|
|
116
|
+
// in CI (worker-thread transport timing issues), so cap the wait.
|
|
117
|
+
await Promise.race([
|
|
118
|
+
kernel.shutdown(),
|
|
119
|
+
new Promise<void>((resolve) => setTimeout(resolve, 10_000)),
|
|
120
|
+
]);
|
|
121
|
+
}
|
|
122
|
+
}, 30_000);
|
|
115
123
|
|
|
116
124
|
it('should connect to hono server and discover endpoints', async () => {
|
|
117
125
|
const client = new ObjectStackClient({ baseUrl });
|
package/src/index.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
2
|
|
|
3
3
|
import { QueryAST, SortNode, AggregationNode, isFilterAST } from '@objectstack/spec/data';
|
|
4
|
-
import {
|
|
5
|
-
BatchUpdateRequest,
|
|
6
|
-
BatchUpdateResponse,
|
|
4
|
+
import {
|
|
5
|
+
BatchUpdateRequest,
|
|
6
|
+
BatchUpdateResponse,
|
|
7
7
|
UpdateManyRequest,
|
|
8
8
|
DeleteManyRequest,
|
|
9
9
|
BatchOptions,
|
|
@@ -88,6 +88,7 @@ import {
|
|
|
88
88
|
ApiRoutes,
|
|
89
89
|
} from '@objectstack/spec/api';
|
|
90
90
|
import { Logger, createLogger } from '@objectstack/core';
|
|
91
|
+
import { RealtimeAPI } from './realtime-api';
|
|
91
92
|
|
|
92
93
|
/**
|
|
93
94
|
* Route types that the client can resolve.
|
|
@@ -228,18 +229,22 @@ export class ObjectStackClient {
|
|
|
228
229
|
private fetchImpl: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
|
229
230
|
private discoveryInfo?: DiscoveryResult;
|
|
230
231
|
private logger: Logger;
|
|
232
|
+
private realtimeAPI: RealtimeAPI;
|
|
231
233
|
|
|
232
234
|
constructor(config: ClientConfig) {
|
|
233
235
|
this.baseUrl = config.baseUrl.replace(/\/$/, ''); // Remove trailing slash
|
|
234
236
|
this.token = config.token;
|
|
235
237
|
this.fetchImpl = config.fetch || globalThis.fetch.bind(globalThis);
|
|
236
|
-
|
|
238
|
+
|
|
237
239
|
// Initialize logger
|
|
238
|
-
this.logger = config.logger || createLogger({
|
|
240
|
+
this.logger = config.logger || createLogger({
|
|
239
241
|
level: config.debug ? 'debug' : 'info',
|
|
240
242
|
format: 'pretty'
|
|
241
243
|
});
|
|
242
|
-
|
|
244
|
+
|
|
245
|
+
// Initialize realtime API
|
|
246
|
+
this.realtimeAPI = new RealtimeAPI(this.baseUrl, this.token);
|
|
247
|
+
|
|
243
248
|
this.logger.debug('ObjectStack client created', { baseUrl: this.baseUrl });
|
|
244
249
|
}
|
|
245
250
|
|
|
@@ -248,12 +253,26 @@ export class ObjectStackClient {
|
|
|
248
253
|
*/
|
|
249
254
|
async connect() {
|
|
250
255
|
this.logger.debug('Connecting to ObjectStack server', { baseUrl: this.baseUrl });
|
|
251
|
-
|
|
256
|
+
|
|
252
257
|
try {
|
|
253
258
|
let data: DiscoveryResult | undefined;
|
|
254
259
|
|
|
255
|
-
// 1. Try
|
|
260
|
+
// 1. Try Protocol-standard Discovery Path /api/v1/discovery (primary)
|
|
256
261
|
try {
|
|
262
|
+
const discoveryUrl = `${this.baseUrl}/api/v1/discovery`;
|
|
263
|
+
this.logger.debug('Probing protocol-standard discovery endpoint', { url: discoveryUrl });
|
|
264
|
+
const res = await this.fetchImpl(discoveryUrl);
|
|
265
|
+
if (res.ok) {
|
|
266
|
+
const body = await res.json();
|
|
267
|
+
data = body.data || body;
|
|
268
|
+
this.logger.debug('Discovered via /api/v1/discovery');
|
|
269
|
+
}
|
|
270
|
+
} catch (e) {
|
|
271
|
+
this.logger.debug('Protocol-standard discovery probe failed', { error: (e as Error).message });
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// 2. Fallback to Standard Discovery (.well-known)
|
|
275
|
+
if (!data) {
|
|
257
276
|
let wellKnownUrl: string;
|
|
258
277
|
try {
|
|
259
278
|
// If baseUrl is absolute, get origin
|
|
@@ -264,24 +283,10 @@ export class ObjectStackClient {
|
|
|
264
283
|
wellKnownUrl = '/.well-known/objectstack';
|
|
265
284
|
}
|
|
266
285
|
|
|
267
|
-
this.logger.debug('
|
|
286
|
+
this.logger.debug('Falling back to .well-known discovery', { url: wellKnownUrl });
|
|
268
287
|
const res = await this.fetchImpl(wellKnownUrl);
|
|
269
|
-
if (res.ok) {
|
|
270
|
-
const body = await res.json();
|
|
271
|
-
data = body.data || body;
|
|
272
|
-
this.logger.debug('Discovered via .well-known');
|
|
273
|
-
}
|
|
274
|
-
} catch (e) {
|
|
275
|
-
this.logger.debug('Standard discovery probe failed', { error: (e as Error).message });
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
// 2. Fallback to Protocol-standard Discovery Path /api/v1/discovery
|
|
279
|
-
if (!data) {
|
|
280
|
-
const fallbackUrl = `${this.baseUrl}/api/v1/discovery`;
|
|
281
|
-
this.logger.debug('Falling back to standard discovery endpoint', { url: fallbackUrl });
|
|
282
|
-
const res = await this.fetchImpl(fallbackUrl);
|
|
283
288
|
if (!res.ok) {
|
|
284
|
-
throw new Error(`Failed to connect to ${
|
|
289
|
+
throw new Error(`Failed to connect to ${wellKnownUrl}: ${res.statusText}`);
|
|
285
290
|
}
|
|
286
291
|
const body = await res.json();
|
|
287
292
|
data = body.data || body;
|
|
@@ -292,13 +297,13 @@ export class ObjectStackClient {
|
|
|
292
297
|
}
|
|
293
298
|
|
|
294
299
|
this.discoveryInfo = data;
|
|
295
|
-
|
|
296
|
-
this.logger.info('Connected to ObjectStack server', {
|
|
300
|
+
|
|
301
|
+
this.logger.info('Connected to ObjectStack server', {
|
|
297
302
|
version: data.version,
|
|
298
303
|
apiName: data.apiName,
|
|
299
|
-
services: data.services
|
|
304
|
+
services: data.services
|
|
300
305
|
});
|
|
301
|
-
|
|
306
|
+
|
|
302
307
|
return data as DiscoveryResult;
|
|
303
308
|
} catch (e) {
|
|
304
309
|
this.logger.error('Failed to connect to ObjectStack server', e as Error, { baseUrl: this.baseUrl });
|
|
@@ -573,6 +578,16 @@ export class ObjectStackClient {
|
|
|
573
578
|
* Authentication Services
|
|
574
579
|
*/
|
|
575
580
|
auth = {
|
|
581
|
+
/**
|
|
582
|
+
* Get authentication configuration
|
|
583
|
+
* Returns available auth providers and features
|
|
584
|
+
*/
|
|
585
|
+
getConfig: async () => {
|
|
586
|
+
const route = this.getRoute('auth');
|
|
587
|
+
const res = await this.fetch(`${this.baseUrl}${route}/config`);
|
|
588
|
+
return this.unwrapResponse(res);
|
|
589
|
+
},
|
|
590
|
+
|
|
576
591
|
/**
|
|
577
592
|
* Login with email and password
|
|
578
593
|
* Uses better-auth endpoint: POST /sign-in/email
|
|
@@ -887,6 +902,14 @@ export class ObjectStackClient {
|
|
|
887
902
|
},
|
|
888
903
|
};
|
|
889
904
|
|
|
905
|
+
/**
|
|
906
|
+
* Event Subscription API
|
|
907
|
+
* Provides real-time event subscriptions for metadata and data changes
|
|
908
|
+
*/
|
|
909
|
+
get events() {
|
|
910
|
+
return this.realtimeAPI;
|
|
911
|
+
}
|
|
912
|
+
|
|
890
913
|
/**
|
|
891
914
|
* Permissions Services
|
|
892
915
|
*/
|
|
@@ -1789,6 +1812,9 @@ export class ObjectStackClient {
|
|
|
1789
1812
|
// Re-export type-safe query builder
|
|
1790
1813
|
export { QueryBuilder, FilterBuilder, createQuery, createFilter } from './query-builder';
|
|
1791
1814
|
|
|
1815
|
+
// Re-export realtime API types
|
|
1816
|
+
export { RealtimeAPI, RealtimeSubscriptionFilter, RealtimeEventHandler } from './realtime-api';
|
|
1817
|
+
|
|
1792
1818
|
// Re-export commonly used types from @objectstack/spec/api for convenience
|
|
1793
1819
|
export type {
|
|
1794
1820
|
BatchUpdateRequest,
|
|
@@ -1856,4 +1882,8 @@ export type {
|
|
|
1856
1882
|
SubscribeResponse,
|
|
1857
1883
|
UnsubscribeResponse,
|
|
1858
1884
|
WellKnownCapabilities,
|
|
1885
|
+
GetAuthConfigResponse,
|
|
1886
|
+
AuthProviderInfo,
|
|
1887
|
+
EmailPasswordConfigPublic,
|
|
1888
|
+
AuthFeaturesConfig,
|
|
1859
1889
|
} from '@objectstack/spec/api';
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Realtime API Module for ObjectStackClient
|
|
5
|
+
*
|
|
6
|
+
* Provides real-time event subscription capabilities using long-polling.
|
|
7
|
+
* For production WebSocket/SSE support, extend with transport adapters.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { RealtimeEventPayload } from '@objectstack/spec/contracts';
|
|
11
|
+
import type { MetadataEvent, DataEvent } from '@objectstack/spec/api';
|
|
12
|
+
|
|
13
|
+
export interface RealtimeSubscriptionFilter {
|
|
14
|
+
/** Metadata/object type filter */
|
|
15
|
+
type?: string;
|
|
16
|
+
/** Package ID filter */
|
|
17
|
+
packageId?: string;
|
|
18
|
+
/** Event types to listen for */
|
|
19
|
+
eventTypes?: string[];
|
|
20
|
+
/** Record ID filter (for data events) */
|
|
21
|
+
recordId?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type RealtimeEventHandler = (event: RealtimeEventPayload) => void;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Realtime API for subscribing to server events
|
|
28
|
+
*
|
|
29
|
+
* Note: Currently uses in-memory adapter. WebSocket/SSE transport planned for future.
|
|
30
|
+
*/
|
|
31
|
+
export class RealtimeAPI {
|
|
32
|
+
// @ts-expect-error - Reserved for future WebSocket/SSE implementation
|
|
33
|
+
private _baseUrl: string;
|
|
34
|
+
// @ts-expect-error - Reserved for future WebSocket/SSE implementation
|
|
35
|
+
private _token?: string;
|
|
36
|
+
private subscriptions = new Map<string, {
|
|
37
|
+
filter: RealtimeSubscriptionFilter;
|
|
38
|
+
handler: RealtimeEventHandler;
|
|
39
|
+
}>();
|
|
40
|
+
private pollInterval?: ReturnType<typeof setInterval>;
|
|
41
|
+
private eventBuffer: RealtimeEventPayload[] = [];
|
|
42
|
+
|
|
43
|
+
constructor(baseUrl: string, token?: string) {
|
|
44
|
+
this._baseUrl = baseUrl;
|
|
45
|
+
this._token = token;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Subscribe to metadata events
|
|
50
|
+
* Returns an unsubscribe function
|
|
51
|
+
*/
|
|
52
|
+
subscribeMetadata(
|
|
53
|
+
type: string,
|
|
54
|
+
callback: (event: MetadataEvent) => void,
|
|
55
|
+
options?: { packageId?: string }
|
|
56
|
+
): () => void {
|
|
57
|
+
const subscriptionId = `metadata-${type}-${Date.now()}`;
|
|
58
|
+
|
|
59
|
+
this.subscriptions.set(subscriptionId, {
|
|
60
|
+
filter: {
|
|
61
|
+
type,
|
|
62
|
+
packageId: options?.packageId,
|
|
63
|
+
eventTypes: [
|
|
64
|
+
`metadata.${type}.created`,
|
|
65
|
+
`metadata.${type}.updated`,
|
|
66
|
+
`metadata.${type}.deleted`
|
|
67
|
+
]
|
|
68
|
+
},
|
|
69
|
+
handler: (event) => {
|
|
70
|
+
// Type guard and filter
|
|
71
|
+
if (event.type.startsWith('metadata.')) {
|
|
72
|
+
callback(event as any as MetadataEvent);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Start polling if not already started
|
|
78
|
+
this.startPolling();
|
|
79
|
+
|
|
80
|
+
// Return unsubscribe function
|
|
81
|
+
return () => {
|
|
82
|
+
this.subscriptions.delete(subscriptionId);
|
|
83
|
+
if (this.subscriptions.size === 0) {
|
|
84
|
+
this.stopPolling();
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Subscribe to data record events
|
|
91
|
+
* Returns an unsubscribe function
|
|
92
|
+
*/
|
|
93
|
+
subscribeData(
|
|
94
|
+
object: string,
|
|
95
|
+
callback: (event: DataEvent) => void,
|
|
96
|
+
options?: { recordId?: string }
|
|
97
|
+
): () => void {
|
|
98
|
+
const subscriptionId = `data-${object}-${Date.now()}`;
|
|
99
|
+
|
|
100
|
+
this.subscriptions.set(subscriptionId, {
|
|
101
|
+
filter: {
|
|
102
|
+
type: object,
|
|
103
|
+
recordId: options?.recordId,
|
|
104
|
+
eventTypes: [
|
|
105
|
+
'data.record.created',
|
|
106
|
+
'data.record.updated',
|
|
107
|
+
'data.record.deleted'
|
|
108
|
+
]
|
|
109
|
+
},
|
|
110
|
+
handler: (event) => {
|
|
111
|
+
// Type guard and filter
|
|
112
|
+
if (event.type.startsWith('data.') && event.object === object) {
|
|
113
|
+
if (!options?.recordId || (event.payload as any)?.recordId === options.recordId) {
|
|
114
|
+
callback(event as any as DataEvent);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Start polling if not already started
|
|
121
|
+
this.startPolling();
|
|
122
|
+
|
|
123
|
+
// Return unsubscribe function
|
|
124
|
+
return () => {
|
|
125
|
+
this.subscriptions.delete(subscriptionId);
|
|
126
|
+
if (this.subscriptions.size === 0) {
|
|
127
|
+
this.stopPolling();
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Emit an event to all matching subscriptions (client-side only)
|
|
134
|
+
* This is used for in-process event delivery
|
|
135
|
+
*/
|
|
136
|
+
private emitEvent(event: RealtimeEventPayload): void {
|
|
137
|
+
for (const sub of this.subscriptions.values()) {
|
|
138
|
+
// Check if event matches subscription filters
|
|
139
|
+
const matchesType = !sub.filter.type ||
|
|
140
|
+
event.type.includes(sub.filter.type) ||
|
|
141
|
+
event.object === sub.filter.type;
|
|
142
|
+
|
|
143
|
+
const matchesEventType = !sub.filter.eventTypes?.length ||
|
|
144
|
+
sub.filter.eventTypes.includes(event.type);
|
|
145
|
+
|
|
146
|
+
const matchesPackage = !sub.filter.packageId ||
|
|
147
|
+
(event.payload as any)?.packageId === sub.filter.packageId;
|
|
148
|
+
|
|
149
|
+
if (matchesType && matchesEventType && matchesPackage) {
|
|
150
|
+
try {
|
|
151
|
+
sub.handler(event);
|
|
152
|
+
} catch (error) {
|
|
153
|
+
console.error('Error in realtime event handler:', error);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Start polling for events (fallback mechanism)
|
|
161
|
+
* In production, this would be replaced with WebSocket/SSE
|
|
162
|
+
*/
|
|
163
|
+
private startPolling(): void {
|
|
164
|
+
if (this.pollInterval) return;
|
|
165
|
+
|
|
166
|
+
// For now, we rely on the in-memory adapter within the same process
|
|
167
|
+
// Events are delivered synchronously via the IRealtimeService
|
|
168
|
+
// This polling is a placeholder for future WebSocket/SSE implementation
|
|
169
|
+
|
|
170
|
+
// Poll every 2 seconds for buffered events
|
|
171
|
+
this.pollInterval = setInterval(() => {
|
|
172
|
+
// Process any buffered events
|
|
173
|
+
while (this.eventBuffer.length > 0) {
|
|
174
|
+
const event = this.eventBuffer.shift();
|
|
175
|
+
if (event) {
|
|
176
|
+
this.emitEvent(event);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}, 2000);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Stop polling for events
|
|
184
|
+
*/
|
|
185
|
+
private stopPolling(): void {
|
|
186
|
+
if (this.pollInterval) {
|
|
187
|
+
clearInterval(this.pollInterval);
|
|
188
|
+
this.pollInterval = undefined;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Internal method to buffer events from server
|
|
194
|
+
* This would be called by WebSocket/SSE handlers in production
|
|
195
|
+
*/
|
|
196
|
+
_bufferEvent(event: RealtimeEventPayload): void {
|
|
197
|
+
this.eventBuffer.push(event);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Disconnect and clean up all subscriptions
|
|
202
|
+
*/
|
|
203
|
+
disconnect(): void {
|
|
204
|
+
this.stopPolling();
|
|
205
|
+
this.subscriptions.clear();
|
|
206
|
+
this.eventBuffer = [];
|
|
207
|
+
}
|
|
208
|
+
}
|
|
@@ -13,15 +13,15 @@ import { ObjectStackClient } from '../../src/index';
|
|
|
13
13
|
const TEST_SERVER_URL = process.env.TEST_SERVER_URL || 'http://localhost:3000';
|
|
14
14
|
|
|
15
15
|
describe('Discovery & Connection', () => {
|
|
16
|
-
describe('TC-DISC-001:
|
|
17
|
-
test('should discover API from
|
|
18
|
-
const client = new ObjectStackClient({
|
|
16
|
+
describe('TC-DISC-001: Protocol-standard Discovery via /api/v1/discovery', () => {
|
|
17
|
+
test('should discover API from /api/v1/discovery', async () => {
|
|
18
|
+
const client = new ObjectStackClient({
|
|
19
19
|
baseUrl: TEST_SERVER_URL,
|
|
20
20
|
debug: true
|
|
21
21
|
});
|
|
22
|
-
|
|
22
|
+
|
|
23
23
|
const discovery = await client.connect();
|
|
24
|
-
|
|
24
|
+
|
|
25
25
|
expect(discovery.version).toBeDefined();
|
|
26
26
|
expect(discovery.apiName).toBeDefined();
|
|
27
27
|
expect(discovery.routes).toBeDefined();
|