@workos/oagen-emitters 0.0.1 → 0.2.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.
- package/.github/workflows/release-please.yml +9 -1
- package/.husky/commit-msg +0 -0
- package/.husky/pre-commit +1 -0
- package/.husky/pre-push +1 -0
- package/.oxfmtrc.json +8 -1
- package/.prettierignore +1 -0
- package/.release-please-manifest.json +3 -0
- package/.vscode/settings.json +3 -0
- package/CHANGELOG.md +61 -0
- package/README.md +2 -2
- package/dist/index.d.mts +7 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +4070 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +14 -18
- package/release-please-config.json +11 -0
- package/smoke/sdk-dotnet.ts +17 -3
- package/smoke/sdk-elixir.ts +17 -3
- package/smoke/sdk-go.ts +21 -4
- package/smoke/sdk-kotlin.ts +23 -4
- package/smoke/sdk-node.ts +15 -3
- package/smoke/sdk-ruby.ts +17 -3
- package/smoke/sdk-rust.ts +16 -3
- package/src/node/client.ts +521 -206
- package/src/node/common.ts +74 -4
- package/src/node/config.ts +1 -0
- package/src/node/enums.ts +53 -9
- package/src/node/errors.ts +82 -3
- package/src/node/fixtures.ts +87 -16
- package/src/node/index.ts +66 -10
- package/src/node/manifest.ts +4 -2
- package/src/node/models.ts +251 -124
- package/src/node/naming.ts +107 -3
- package/src/node/resources.ts +1162 -108
- package/src/node/serializers.ts +512 -52
- package/src/node/tests.ts +650 -110
- package/src/node/type-map.ts +89 -11
- package/src/node/utils.ts +426 -113
- package/test/node/client.test.ts +1083 -20
- package/test/node/enums.test.ts +73 -4
- package/test/node/errors.test.ts +4 -21
- package/test/node/models.test.ts +499 -5
- package/test/node/naming.test.ts +14 -7
- package/test/node/resources.test.ts +1568 -9
- package/test/node/serializers.test.ts +241 -5
- package/tsconfig.json +2 -3
- package/{tsup.config.ts → tsdown.config.ts} +1 -1
- package/dist/index.d.ts +0 -5
- package/dist/index.js +0 -2158
package/test/node/client.test.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
2
|
import { generateClient } from '../../src/node/client.js';
|
|
3
|
-
import
|
|
3
|
+
import { isServiceCoveredByExisting } from '../../src/node/utils.js';
|
|
4
|
+
import type { EmitterContext, ApiSpec, Service, Model, Enum } from '@workos/oagen';
|
|
5
|
+
import type { ApiSurface } from '@workos/oagen/compat';
|
|
4
6
|
|
|
5
7
|
const service: Service = {
|
|
6
8
|
name: 'Organizations',
|
|
@@ -9,7 +11,13 @@ const service: Service = {
|
|
|
9
11
|
name: 'getOrganization',
|
|
10
12
|
httpMethod: 'get',
|
|
11
13
|
path: '/organizations/{id}',
|
|
12
|
-
pathParams: [
|
|
14
|
+
pathParams: [
|
|
15
|
+
{
|
|
16
|
+
name: 'id',
|
|
17
|
+
type: { kind: 'primitive', type: 'string' },
|
|
18
|
+
required: true,
|
|
19
|
+
},
|
|
20
|
+
],
|
|
13
21
|
queryParams: [],
|
|
14
22
|
headerParams: [],
|
|
15
23
|
response: { kind: 'model', name: 'Organization' },
|
|
@@ -23,7 +31,11 @@ const model: Model = {
|
|
|
23
31
|
name: 'Organization',
|
|
24
32
|
fields: [
|
|
25
33
|
{ name: 'id', type: { kind: 'primitive', type: 'string' }, required: true },
|
|
26
|
-
{
|
|
34
|
+
{
|
|
35
|
+
name: 'name',
|
|
36
|
+
type: { kind: 'primitive', type: 'string' },
|
|
37
|
+
required: true,
|
|
38
|
+
},
|
|
27
39
|
],
|
|
28
40
|
};
|
|
29
41
|
|
|
@@ -40,7 +52,6 @@ const ctx: EmitterContext = {
|
|
|
40
52
|
namespace: 'workos',
|
|
41
53
|
namespacePascal: 'WorkOS',
|
|
42
54
|
spec,
|
|
43
|
-
irVersion: 6,
|
|
44
55
|
};
|
|
45
56
|
|
|
46
57
|
describe('generateClient', () => {
|
|
@@ -50,11 +61,16 @@ describe('generateClient', () => {
|
|
|
50
61
|
expect(workosFile).toBeDefined();
|
|
51
62
|
|
|
52
63
|
const content = workosFile!.content;
|
|
53
|
-
expect(content).toContain('export class WorkOS {');
|
|
64
|
+
expect(content).toContain('export class WorkOS extends WorkOSBase {');
|
|
54
65
|
expect(content).toContain('readonly organizations = new Organizations(this);');
|
|
55
|
-
expect(content).toContain(
|
|
56
|
-
|
|
57
|
-
|
|
66
|
+
expect(content).toContain("import { WorkOSBase } from './common/workos-base';");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('allows workos.ts to participate in integration (no integrateTarget: false)', () => {
|
|
70
|
+
const files = generateClient(spec, ctx);
|
|
71
|
+
const workosFile = files.find((f) => f.path === 'src/workos.ts');
|
|
72
|
+
expect(workosFile).toBeDefined();
|
|
73
|
+
expect(workosFile!.integrateTarget).not.toBe(false);
|
|
58
74
|
});
|
|
59
75
|
|
|
60
76
|
it('generates barrel exports', () => {
|
|
@@ -66,10 +82,22 @@ describe('generateClient', () => {
|
|
|
66
82
|
expect(content).toContain("export * from './common/exceptions';");
|
|
67
83
|
expect(content).toContain("export { AutoPaginatable } from './common/utils/pagination';");
|
|
68
84
|
expect(content).toContain("export { WorkOS } from './workos';");
|
|
69
|
-
|
|
85
|
+
// Service types are now re-exported via the service barrel
|
|
86
|
+
expect(content).toContain("export * from './organizations/interfaces';");
|
|
87
|
+
expect(content).not.toContain('export type { Organization, OrganizationResponse }');
|
|
70
88
|
expect(content).toContain("export { Organizations } from './organizations/organizations';");
|
|
71
89
|
});
|
|
72
90
|
|
|
91
|
+
it('generates per-service barrel files', () => {
|
|
92
|
+
const files = generateClient(spec, ctx);
|
|
93
|
+
const serviceBarrel = files.find((f) => f.path === 'src/organizations/interfaces/index.ts');
|
|
94
|
+
expect(serviceBarrel).toBeDefined();
|
|
95
|
+
|
|
96
|
+
const content = serviceBarrel!.content;
|
|
97
|
+
expect(content).toContain("export * from './organization.interface';");
|
|
98
|
+
expect(serviceBarrel!.skipIfExists).toBe(true);
|
|
99
|
+
});
|
|
100
|
+
|
|
73
101
|
it('generates package.json and tsconfig.json', () => {
|
|
74
102
|
const files = generateClient(spec, ctx);
|
|
75
103
|
const pkg = files.find((f) => f.path === 'package.json');
|
|
@@ -84,7 +112,7 @@ describe('generateClient', () => {
|
|
|
84
112
|
|
|
85
113
|
it('uses overlay-resolved names for imports and accessors', () => {
|
|
86
114
|
const mfaService: Service = {
|
|
87
|
-
name: '
|
|
115
|
+
name: 'Billing',
|
|
88
116
|
operations: [
|
|
89
117
|
{
|
|
90
118
|
name: 'enrollFactor',
|
|
@@ -102,7 +130,13 @@ describe('generateClient', () => {
|
|
|
102
130
|
|
|
103
131
|
const mfaModel: Model = {
|
|
104
132
|
name: 'AuthenticationFactor',
|
|
105
|
-
fields: [
|
|
133
|
+
fields: [
|
|
134
|
+
{
|
|
135
|
+
name: 'id',
|
|
136
|
+
type: { kind: 'primitive', type: 'string' },
|
|
137
|
+
required: true,
|
|
138
|
+
},
|
|
139
|
+
],
|
|
106
140
|
};
|
|
107
141
|
|
|
108
142
|
const overlaySpec: ApiSpec = {
|
|
@@ -118,12 +152,16 @@ describe('generateClient', () => {
|
|
|
118
152
|
namespace: 'workos',
|
|
119
153
|
namespacePascal: 'WorkOS',
|
|
120
154
|
spec: overlaySpec,
|
|
121
|
-
irVersion: 6,
|
|
122
155
|
overlayLookup: {
|
|
123
156
|
methodByOperation: new Map([
|
|
124
157
|
[
|
|
125
158
|
'POST /auth/factors/enroll',
|
|
126
|
-
{
|
|
159
|
+
{
|
|
160
|
+
className: 'Mfa',
|
|
161
|
+
methodName: 'enrollFactor',
|
|
162
|
+
params: [],
|
|
163
|
+
returnType: 'void',
|
|
164
|
+
},
|
|
127
165
|
],
|
|
128
166
|
]),
|
|
129
167
|
httpKeyByMethod: new Map(),
|
|
@@ -147,19 +185,1044 @@ describe('generateClient', () => {
|
|
|
147
185
|
|
|
148
186
|
const barrel = files.find((f) => f.path === 'src/index.ts');
|
|
149
187
|
expect(barrel).toBeDefined();
|
|
150
|
-
// Barrel export uses resolved name
|
|
188
|
+
// Barrel export uses resolved name for resource class
|
|
151
189
|
expect(barrel!.content).toContain("from './mfa/mfa'");
|
|
190
|
+
// Service barrel uses resolved directory name
|
|
191
|
+
expect(barrel!.content).toContain("export * from './mfa/interfaces'");
|
|
192
|
+
|
|
193
|
+
// Per-service barrel is generated with resolved directory
|
|
194
|
+
const serviceBarrel = files.find((f) => f.path === 'src/mfa/interfaces/index.ts');
|
|
195
|
+
expect(serviceBarrel).toBeDefined();
|
|
196
|
+
expect(serviceBarrel!.content).toContain("export * from './authentication-factor.interface';");
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('does not generate error handling in WorkOS client (lives in WorkOSBase)', () => {
|
|
200
|
+
const files = generateClient(spec, ctx);
|
|
201
|
+
const workosFile = files.find((f) => f.path === 'src/workos.ts')!;
|
|
202
|
+
const content = workosFile.content;
|
|
203
|
+
|
|
204
|
+
expect(content).not.toContain('handleHttpError');
|
|
205
|
+
expect(content).not.toContain('UnauthorizedException');
|
|
206
|
+
expect(content).not.toContain('NotFoundException');
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('skips explicit model export when name is already in apiSurface.exports', () => {
|
|
210
|
+
// Simulates the Event shadowing bug: the existing SDK already exports "Event"
|
|
211
|
+
// via a wildcard re-export (e.g., a hand-written 60+ member discriminated union).
|
|
212
|
+
// The barrel must not emit an explicit `export type { Event }` that would shadow it.
|
|
213
|
+
const eventService: Service = {
|
|
214
|
+
name: 'Events',
|
|
215
|
+
operations: [
|
|
216
|
+
{
|
|
217
|
+
name: 'listEvents',
|
|
218
|
+
httpMethod: 'get',
|
|
219
|
+
path: '/events',
|
|
220
|
+
pathParams: [],
|
|
221
|
+
queryParams: [],
|
|
222
|
+
headerParams: [],
|
|
223
|
+
response: { kind: 'model', name: 'Event' },
|
|
224
|
+
errors: [],
|
|
225
|
+
injectIdempotencyKey: false,
|
|
226
|
+
},
|
|
227
|
+
],
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const eventModel: Model = {
|
|
231
|
+
name: 'Event',
|
|
232
|
+
fields: [
|
|
233
|
+
{
|
|
234
|
+
name: 'id',
|
|
235
|
+
type: { kind: 'primitive', type: 'string' },
|
|
236
|
+
required: true,
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
name: 'event',
|
|
240
|
+
type: { kind: 'primitive', type: 'string' },
|
|
241
|
+
required: true,
|
|
242
|
+
},
|
|
243
|
+
],
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
const otherModel: Model = {
|
|
247
|
+
name: 'EventCursor',
|
|
248
|
+
fields: [
|
|
249
|
+
{
|
|
250
|
+
name: 'cursor',
|
|
251
|
+
type: { kind: 'primitive', type: 'string' },
|
|
252
|
+
required: true,
|
|
253
|
+
},
|
|
254
|
+
],
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
const eventSpec: ApiSpec = {
|
|
258
|
+
name: 'Test',
|
|
259
|
+
version: '1.0.0',
|
|
260
|
+
baseUrl: 'https://api.example.com',
|
|
261
|
+
services: [eventService],
|
|
262
|
+
models: [eventModel, otherModel],
|
|
263
|
+
enums: [],
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
const surface: ApiSurface = {
|
|
267
|
+
language: 'node',
|
|
268
|
+
extractedFrom: '/tmp/test-sdk',
|
|
269
|
+
extractedAt: '2025-01-01T00:00:00Z',
|
|
270
|
+
classes: {},
|
|
271
|
+
interfaces: {
|
|
272
|
+
Event: {
|
|
273
|
+
name: 'Event',
|
|
274
|
+
sourceFile: 'src/common/interfaces/event.interface.ts',
|
|
275
|
+
fields: {},
|
|
276
|
+
extends: [],
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
typeAliases: {},
|
|
280
|
+
enums: {},
|
|
281
|
+
// The existing SDK's barrel re-exports "Event" via a wildcard chain
|
|
282
|
+
exports: {
|
|
283
|
+
'src/common/interfaces/event.interface.ts': ['Event'],
|
|
284
|
+
'src/index.ts': ['Event', 'WorkOS', 'Events'],
|
|
285
|
+
},
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
const eventCtx: EmitterContext = {
|
|
289
|
+
namespace: 'workos',
|
|
290
|
+
namespacePascal: 'WorkOS',
|
|
291
|
+
spec: eventSpec,
|
|
292
|
+
apiSurface: surface,
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
const files = generateClient(eventSpec, eventCtx);
|
|
296
|
+
const barrel = files.find((f) => f.path === 'src/index.ts')!;
|
|
297
|
+
const content = barrel.content;
|
|
298
|
+
|
|
299
|
+
// Event must NOT appear as an explicit named export — it would shadow the wildcard
|
|
300
|
+
expect(content).not.toContain('export type { Event,');
|
|
301
|
+
expect(content).not.toContain('export type { Event }');
|
|
302
|
+
|
|
303
|
+
// EventCursor is NOT in apiSurface.exports, so it should still be exported
|
|
304
|
+
// (via common barrel wildcard since it's unassigned to any service)
|
|
305
|
+
expect(content).toContain("export * from './common/interfaces'");
|
|
306
|
+
|
|
307
|
+
// The resource class export should still be present
|
|
308
|
+
expect(content).toContain("export { Events } from './events/events'");
|
|
152
309
|
});
|
|
153
310
|
|
|
154
|
-
it('
|
|
311
|
+
it('skips explicit enum export when name is already in apiSurface.exports', () => {
|
|
312
|
+
const enumSpec: ApiSpec = {
|
|
313
|
+
name: 'Test',
|
|
314
|
+
version: '1.0.0',
|
|
315
|
+
baseUrl: 'https://api.example.com',
|
|
316
|
+
services: [service],
|
|
317
|
+
models: [model],
|
|
318
|
+
enums: [
|
|
319
|
+
{
|
|
320
|
+
name: 'EventType',
|
|
321
|
+
values: [
|
|
322
|
+
{ name: 'CONNECTION_ACTIVATED', value: 'connection.activated' },
|
|
323
|
+
{ name: 'CONNECTION_DELETED', value: 'connection.deleted' },
|
|
324
|
+
],
|
|
325
|
+
},
|
|
326
|
+
],
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
const surface: ApiSurface = {
|
|
330
|
+
language: 'node',
|
|
331
|
+
extractedFrom: '/tmp/test-sdk',
|
|
332
|
+
extractedAt: '2025-01-01T00:00:00Z',
|
|
333
|
+
classes: {},
|
|
334
|
+
interfaces: {},
|
|
335
|
+
typeAliases: {},
|
|
336
|
+
enums: {},
|
|
337
|
+
exports: {
|
|
338
|
+
'src/common/interfaces/event-type.interface.ts': ['EventType'],
|
|
339
|
+
},
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
const enumCtx: EmitterContext = {
|
|
343
|
+
namespace: 'workos',
|
|
344
|
+
namespacePascal: 'WorkOS',
|
|
345
|
+
spec: enumSpec,
|
|
346
|
+
apiSurface: surface,
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
const files = generateClient(enumSpec, enumCtx);
|
|
350
|
+
const barrel = files.find((f) => f.path === 'src/index.ts')!;
|
|
351
|
+
|
|
352
|
+
// EventType should NOT appear as an explicit export — already covered by wildcard
|
|
353
|
+
expect(barrel.content).not.toContain('export type { EventType }');
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it('emits model exports normally when no apiSurface is present', () => {
|
|
357
|
+
// Without apiSurface, all models should be exported via service barrel
|
|
155
358
|
const files = generateClient(spec, ctx);
|
|
359
|
+
const barrel = files.find((f) => f.path === 'src/index.ts')!;
|
|
360
|
+
expect(barrel.content).toContain("export * from './organizations/interfaces'");
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it('renders spec.description as JSDoc on WorkOS class', () => {
|
|
364
|
+
const specWithDesc: ApiSpec = {
|
|
365
|
+
...spec,
|
|
366
|
+
description: 'The WorkOS API provides a unified interface for enterprise features.',
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
const descCtx: EmitterContext = {
|
|
370
|
+
namespace: 'workos',
|
|
371
|
+
namespacePascal: 'WorkOS',
|
|
372
|
+
spec: specWithDesc,
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
const files = generateClient(specWithDesc, descCtx);
|
|
156
376
|
const workosFile = files.find((f) => f.path === 'src/workos.ts')!;
|
|
157
377
|
const content = workosFile.content;
|
|
158
378
|
|
|
159
|
-
expect(content).toContain('
|
|
160
|
-
expect(content).toContain('
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
379
|
+
expect(content).toContain('/** The WorkOS API provides a unified interface for enterprise features. */');
|
|
380
|
+
expect(content).toContain('export class WorkOS extends WorkOSBase {');
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
it('uses value export for baseline TS enums and type export for type aliases', () => {
|
|
384
|
+
const enumDef: Enum = {
|
|
385
|
+
name: 'ConnectionType',
|
|
386
|
+
values: [
|
|
387
|
+
{ name: 'ADFSSAML', value: 'ADFSSAML' },
|
|
388
|
+
{ name: 'GoogleOAuth', value: 'GoogleOAuth' },
|
|
389
|
+
],
|
|
390
|
+
};
|
|
391
|
+
const aliasEnumDef: Enum = {
|
|
392
|
+
name: 'DirectoryState',
|
|
393
|
+
values: [
|
|
394
|
+
{ name: 'active', value: 'active' },
|
|
395
|
+
{ name: 'inactive', value: 'inactive' },
|
|
396
|
+
],
|
|
397
|
+
};
|
|
398
|
+
const enumService: Service = {
|
|
399
|
+
name: 'Payments',
|
|
400
|
+
operations: [
|
|
401
|
+
{
|
|
402
|
+
name: 'listPayments',
|
|
403
|
+
httpMethod: 'get',
|
|
404
|
+
path: '/payments',
|
|
405
|
+
pathParams: [],
|
|
406
|
+
queryParams: [
|
|
407
|
+
{
|
|
408
|
+
name: 'type',
|
|
409
|
+
type: {
|
|
410
|
+
kind: 'enum',
|
|
411
|
+
name: 'ConnectionType',
|
|
412
|
+
values: ['ADFSSAML', 'GoogleOAuth'],
|
|
413
|
+
},
|
|
414
|
+
required: false,
|
|
415
|
+
},
|
|
416
|
+
],
|
|
417
|
+
headerParams: [],
|
|
418
|
+
response: { kind: 'model', name: 'Organization' },
|
|
419
|
+
errors: [],
|
|
420
|
+
injectIdempotencyKey: false,
|
|
421
|
+
},
|
|
422
|
+
],
|
|
423
|
+
};
|
|
424
|
+
const dirService: Service = {
|
|
425
|
+
name: 'Invoices',
|
|
426
|
+
operations: [
|
|
427
|
+
{
|
|
428
|
+
name: 'listInvoices',
|
|
429
|
+
httpMethod: 'get',
|
|
430
|
+
path: '/invoices',
|
|
431
|
+
pathParams: [],
|
|
432
|
+
queryParams: [
|
|
433
|
+
{
|
|
434
|
+
name: 'state',
|
|
435
|
+
type: {
|
|
436
|
+
kind: 'enum',
|
|
437
|
+
name: 'DirectoryState',
|
|
438
|
+
values: ['active', 'inactive'],
|
|
439
|
+
},
|
|
440
|
+
required: false,
|
|
441
|
+
},
|
|
442
|
+
],
|
|
443
|
+
headerParams: [],
|
|
444
|
+
response: { kind: 'model', name: 'Organization' },
|
|
445
|
+
errors: [],
|
|
446
|
+
injectIdempotencyKey: false,
|
|
447
|
+
},
|
|
448
|
+
],
|
|
449
|
+
};
|
|
450
|
+
const enumSpec: ApiSpec = {
|
|
451
|
+
name: 'Test',
|
|
452
|
+
version: '1.0.0',
|
|
453
|
+
baseUrl: 'https://api.example.com',
|
|
454
|
+
services: [service, enumService, dirService],
|
|
455
|
+
models: [model],
|
|
456
|
+
enums: [enumDef, aliasEnumDef],
|
|
457
|
+
};
|
|
458
|
+
const enumCtx: EmitterContext = {
|
|
459
|
+
namespace: 'workos',
|
|
460
|
+
namespacePascal: 'WorkOS',
|
|
461
|
+
spec: enumSpec,
|
|
462
|
+
apiSurface: {
|
|
463
|
+
language: 'node',
|
|
464
|
+
extractedFrom: 'test',
|
|
465
|
+
extractedAt: '2024-01-01',
|
|
466
|
+
interfaces: {},
|
|
467
|
+
classes: {},
|
|
468
|
+
enums: {
|
|
469
|
+
ConnectionType: {
|
|
470
|
+
name: 'ConnectionType',
|
|
471
|
+
members: { ADFSSAML: 'ADFSSAML', GoogleOAuth: 'GoogleOAuth' },
|
|
472
|
+
},
|
|
473
|
+
},
|
|
474
|
+
typeAliases: {},
|
|
475
|
+
exports: {},
|
|
476
|
+
},
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
const files = generateClient(enumSpec, enumCtx);
|
|
480
|
+
const barrel = files.find((f) => f.path === 'src/index.ts');
|
|
481
|
+
expect(barrel).toBeDefined();
|
|
482
|
+
|
|
483
|
+
const content = barrel!.content;
|
|
484
|
+
// Both enums are now re-exported via per-service barrel wildcards
|
|
485
|
+
expect(content).toContain("export * from './payments/interfaces'");
|
|
486
|
+
expect(content).toContain("export * from './invoices/interfaces'");
|
|
487
|
+
// Individual enum exports should NOT appear (covered by wildcard)
|
|
488
|
+
expect(content).not.toContain('export { ConnectionType }');
|
|
489
|
+
expect(content).not.toContain('export type { InvoiceState }');
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
it('skips services whose endpoints are fully covered by existing hand-written services', () => {
|
|
493
|
+
const connectionsService: Service = {
|
|
494
|
+
name: 'Payments',
|
|
495
|
+
operations: [
|
|
496
|
+
{
|
|
497
|
+
name: 'listPayments',
|
|
498
|
+
httpMethod: 'get',
|
|
499
|
+
path: '/payments',
|
|
500
|
+
pathParams: [],
|
|
501
|
+
queryParams: [],
|
|
502
|
+
headerParams: [],
|
|
503
|
+
response: { kind: 'model', name: 'ConnectionList' },
|
|
504
|
+
errors: [],
|
|
505
|
+
injectIdempotencyKey: false,
|
|
506
|
+
},
|
|
507
|
+
{
|
|
508
|
+
name: 'getConnection',
|
|
509
|
+
httpMethod: 'get',
|
|
510
|
+
path: '/payments/{id}',
|
|
511
|
+
pathParams: [
|
|
512
|
+
{
|
|
513
|
+
name: 'id',
|
|
514
|
+
type: { kind: 'primitive', type: 'string' },
|
|
515
|
+
required: true,
|
|
516
|
+
},
|
|
517
|
+
],
|
|
518
|
+
queryParams: [],
|
|
519
|
+
headerParams: [],
|
|
520
|
+
response: { kind: 'model', name: 'Connection' },
|
|
521
|
+
errors: [],
|
|
522
|
+
injectIdempotencyKey: false,
|
|
523
|
+
},
|
|
524
|
+
],
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
const connectionModel: Model = {
|
|
528
|
+
name: 'Connection',
|
|
529
|
+
fields: [
|
|
530
|
+
{
|
|
531
|
+
name: 'id',
|
|
532
|
+
type: { kind: 'primitive', type: 'string' },
|
|
533
|
+
required: true,
|
|
534
|
+
},
|
|
535
|
+
{
|
|
536
|
+
name: 'name',
|
|
537
|
+
type: { kind: 'primitive', type: 'string' },
|
|
538
|
+
required: true,
|
|
539
|
+
},
|
|
540
|
+
],
|
|
541
|
+
};
|
|
542
|
+
|
|
543
|
+
const radarService: Service = {
|
|
544
|
+
name: 'Radar',
|
|
545
|
+
operations: [
|
|
546
|
+
{
|
|
547
|
+
name: 'assess',
|
|
548
|
+
httpMethod: 'post',
|
|
549
|
+
path: '/radar/assess',
|
|
550
|
+
pathParams: [],
|
|
551
|
+
queryParams: [],
|
|
552
|
+
headerParams: [],
|
|
553
|
+
response: { kind: 'model', name: 'RadarResult' },
|
|
554
|
+
errors: [],
|
|
555
|
+
injectIdempotencyKey: false,
|
|
556
|
+
},
|
|
557
|
+
],
|
|
558
|
+
};
|
|
559
|
+
|
|
560
|
+
const radarModel: Model = {
|
|
561
|
+
name: 'RadarResult',
|
|
562
|
+
fields: [
|
|
563
|
+
{
|
|
564
|
+
name: 'score',
|
|
565
|
+
type: { kind: 'primitive', type: 'number' },
|
|
566
|
+
required: true,
|
|
567
|
+
},
|
|
568
|
+
],
|
|
569
|
+
};
|
|
570
|
+
|
|
571
|
+
const coveredSpec: ApiSpec = {
|
|
572
|
+
name: 'Test',
|
|
573
|
+
version: '1.0.0',
|
|
574
|
+
baseUrl: 'https://api.example.com',
|
|
575
|
+
services: [connectionsService, radarService],
|
|
576
|
+
models: [connectionModel, radarModel],
|
|
577
|
+
enums: [],
|
|
578
|
+
};
|
|
579
|
+
|
|
580
|
+
const coveredCtx: EmitterContext = {
|
|
581
|
+
namespace: 'workos',
|
|
582
|
+
namespacePascal: 'WorkOS',
|
|
583
|
+
spec: coveredSpec,
|
|
584
|
+
apiSurface: {
|
|
585
|
+
language: 'node',
|
|
586
|
+
extractedFrom: 'test',
|
|
587
|
+
extractedAt: '2024-01-01',
|
|
588
|
+
interfaces: {},
|
|
589
|
+
classes: {
|
|
590
|
+
Sso: {
|
|
591
|
+
name: 'Sso',
|
|
592
|
+
methods: {
|
|
593
|
+
listConnections: [
|
|
594
|
+
{
|
|
595
|
+
name: 'listConnections',
|
|
596
|
+
params: [],
|
|
597
|
+
returnType: 'Promise<AutoPaginatable<Connection>>',
|
|
598
|
+
async: true,
|
|
599
|
+
},
|
|
600
|
+
],
|
|
601
|
+
getConnection: [
|
|
602
|
+
{
|
|
603
|
+
name: 'getConnection',
|
|
604
|
+
params: [{ name: 'id', type: 'string', optional: false }],
|
|
605
|
+
returnType: 'Promise<Connection>',
|
|
606
|
+
async: true,
|
|
607
|
+
},
|
|
608
|
+
],
|
|
609
|
+
},
|
|
610
|
+
properties: {},
|
|
611
|
+
constructorParams: [],
|
|
612
|
+
},
|
|
613
|
+
},
|
|
614
|
+
enums: {},
|
|
615
|
+
typeAliases: {},
|
|
616
|
+
exports: {},
|
|
617
|
+
},
|
|
618
|
+
overlayLookup: {
|
|
619
|
+
methodByOperation: new Map([
|
|
620
|
+
[
|
|
621
|
+
'GET /connections',
|
|
622
|
+
{
|
|
623
|
+
className: 'Sso',
|
|
624
|
+
methodName: 'listConnections',
|
|
625
|
+
params: [],
|
|
626
|
+
returnType: 'Promise<AutoPaginatable<Connection>>',
|
|
627
|
+
},
|
|
628
|
+
],
|
|
629
|
+
[
|
|
630
|
+
'GET /connections/{id}',
|
|
631
|
+
{
|
|
632
|
+
className: 'Sso',
|
|
633
|
+
methodName: 'getConnection',
|
|
634
|
+
params: [{ name: 'id', type: 'string', optional: false }],
|
|
635
|
+
returnType: 'Promise<Connection>',
|
|
636
|
+
},
|
|
637
|
+
],
|
|
638
|
+
]),
|
|
639
|
+
httpKeyByMethod: new Map(),
|
|
640
|
+
interfaceByName: new Map(),
|
|
641
|
+
typeAliasByName: new Map(),
|
|
642
|
+
requiredExports: new Map(),
|
|
643
|
+
modelNameByIR: new Map(),
|
|
644
|
+
fileBySymbol: new Map(),
|
|
645
|
+
},
|
|
646
|
+
};
|
|
647
|
+
|
|
648
|
+
const files = generateClient(coveredSpec, coveredCtx);
|
|
649
|
+
const workosFile = files.find((f) => f.path === 'src/workos.ts')!;
|
|
650
|
+
const content = workosFile.content;
|
|
651
|
+
|
|
652
|
+
// Connections service should NOT appear (fully covered by Sso in baseline)
|
|
653
|
+
expect(content).not.toContain('Connections');
|
|
654
|
+
expect(content).not.toContain("from './sso/sso'");
|
|
655
|
+
|
|
656
|
+
// Radar service should still appear (not covered)
|
|
657
|
+
expect(content).toContain('readonly radar = new Radar(this);');
|
|
658
|
+
expect(content).toContain("import { Radar } from './radar/radar';");
|
|
659
|
+
|
|
660
|
+
// Barrel should also skip the Connections resource class export
|
|
661
|
+
const barrel = files.find((f) => f.path === 'src/index.ts')!;
|
|
662
|
+
const barrelContent = barrel.content;
|
|
663
|
+
expect(barrelContent).not.toContain('export { Sso }');
|
|
664
|
+
expect(barrelContent).not.toContain('export { Connections }');
|
|
665
|
+
|
|
666
|
+
// Covered services don't generate barrel exports — their types are
|
|
667
|
+
// already exported by the hand-written service's own barrel.
|
|
668
|
+
expect(barrelContent).not.toContain("export * from './sso/interfaces'");
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
it('does not skip services when only some operations are covered', () => {
|
|
672
|
+
const partialService: Service = {
|
|
673
|
+
name: 'Invoices',
|
|
674
|
+
operations: [
|
|
675
|
+
{
|
|
676
|
+
name: 'listInvoices',
|
|
677
|
+
httpMethod: 'get',
|
|
678
|
+
path: '/invoices',
|
|
679
|
+
pathParams: [],
|
|
680
|
+
queryParams: [],
|
|
681
|
+
headerParams: [],
|
|
682
|
+
response: { kind: 'model', name: 'DirectoryList' },
|
|
683
|
+
errors: [],
|
|
684
|
+
injectIdempotencyKey: false,
|
|
685
|
+
},
|
|
686
|
+
{
|
|
687
|
+
name: 'createInvoice',
|
|
688
|
+
httpMethod: 'post',
|
|
689
|
+
path: '/invoices',
|
|
690
|
+
pathParams: [],
|
|
691
|
+
queryParams: [],
|
|
692
|
+
headerParams: [],
|
|
693
|
+
response: { kind: 'model', name: 'Invoice' },
|
|
694
|
+
errors: [],
|
|
695
|
+
injectIdempotencyKey: false,
|
|
696
|
+
},
|
|
697
|
+
],
|
|
698
|
+
};
|
|
699
|
+
|
|
700
|
+
const dirModel: Model = {
|
|
701
|
+
name: 'Invoice',
|
|
702
|
+
fields: [
|
|
703
|
+
{
|
|
704
|
+
name: 'id',
|
|
705
|
+
type: { kind: 'primitive', type: 'string' },
|
|
706
|
+
required: true,
|
|
707
|
+
},
|
|
708
|
+
],
|
|
709
|
+
};
|
|
710
|
+
|
|
711
|
+
const partialSpec: ApiSpec = {
|
|
712
|
+
name: 'Test',
|
|
713
|
+
version: '1.0.0',
|
|
714
|
+
baseUrl: 'https://api.example.com',
|
|
715
|
+
services: [partialService],
|
|
716
|
+
models: [dirModel],
|
|
717
|
+
enums: [],
|
|
718
|
+
};
|
|
719
|
+
|
|
720
|
+
const partialCtx: EmitterContext = {
|
|
721
|
+
namespace: 'workos',
|
|
722
|
+
namespacePascal: 'WorkOS',
|
|
723
|
+
spec: partialSpec,
|
|
724
|
+
apiSurface: {
|
|
725
|
+
language: 'node',
|
|
726
|
+
extractedFrom: 'test',
|
|
727
|
+
extractedAt: '2024-01-01',
|
|
728
|
+
interfaces: {},
|
|
729
|
+
classes: {
|
|
730
|
+
Billing: {
|
|
731
|
+
name: 'Billing',
|
|
732
|
+
methods: {
|
|
733
|
+
listInvoices: [
|
|
734
|
+
{
|
|
735
|
+
name: 'listInvoices',
|
|
736
|
+
params: [],
|
|
737
|
+
returnType: 'Promise<AutoPaginatable<Invoice>>',
|
|
738
|
+
async: true,
|
|
739
|
+
},
|
|
740
|
+
],
|
|
741
|
+
},
|
|
742
|
+
properties: {},
|
|
743
|
+
constructorParams: [],
|
|
744
|
+
},
|
|
745
|
+
},
|
|
746
|
+
enums: {},
|
|
747
|
+
typeAliases: {},
|
|
748
|
+
exports: {},
|
|
749
|
+
},
|
|
750
|
+
overlayLookup: {
|
|
751
|
+
methodByOperation: new Map([
|
|
752
|
+
[
|
|
753
|
+
'GET /invoices',
|
|
754
|
+
{
|
|
755
|
+
className: 'Billing',
|
|
756
|
+
methodName: 'listInvoices',
|
|
757
|
+
params: [],
|
|
758
|
+
returnType: 'Promise<AutoPaginatable<Invoice>>',
|
|
759
|
+
},
|
|
760
|
+
],
|
|
761
|
+
]),
|
|
762
|
+
httpKeyByMethod: new Map(),
|
|
763
|
+
interfaceByName: new Map(),
|
|
764
|
+
typeAliasByName: new Map(),
|
|
765
|
+
requiredExports: new Map(),
|
|
766
|
+
modelNameByIR: new Map(),
|
|
767
|
+
fileBySymbol: new Map(),
|
|
768
|
+
},
|
|
769
|
+
};
|
|
770
|
+
|
|
771
|
+
const files = generateClient(partialSpec, partialCtx);
|
|
772
|
+
const workosFile = files.find((f) => f.path === 'src/workos.ts')!;
|
|
773
|
+
const content = workosFile.content;
|
|
774
|
+
|
|
775
|
+
// Service should still be generated because it has an uncovered operation
|
|
776
|
+
expect(content).toContain('Billing');
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
it('does not skip services when no overlay is provided', () => {
|
|
780
|
+
const files = generateClient(spec, ctx);
|
|
781
|
+
const workosFile = files.find((f) => f.path === 'src/workos.ts')!;
|
|
782
|
+
expect(workosFile.content).toContain('readonly organizations = new Organizations(this);');
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
it('does not skip services when overlay exists but no apiSurface baseline', () => {
|
|
786
|
+
const mfaService: Service = {
|
|
787
|
+
name: 'Analytics',
|
|
788
|
+
operations: [
|
|
789
|
+
{
|
|
790
|
+
name: 'enrollFactor',
|
|
791
|
+
httpMethod: 'post',
|
|
792
|
+
path: '/auth/factors/enroll',
|
|
793
|
+
pathParams: [],
|
|
794
|
+
queryParams: [],
|
|
795
|
+
headerParams: [],
|
|
796
|
+
response: { kind: 'model', name: 'AuthenticationFactor' },
|
|
797
|
+
errors: [],
|
|
798
|
+
injectIdempotencyKey: true,
|
|
799
|
+
},
|
|
800
|
+
],
|
|
801
|
+
};
|
|
802
|
+
|
|
803
|
+
const mfaModel: Model = {
|
|
804
|
+
name: 'AuthenticationFactor',
|
|
805
|
+
fields: [
|
|
806
|
+
{
|
|
807
|
+
name: 'id',
|
|
808
|
+
type: { kind: 'primitive', type: 'string' },
|
|
809
|
+
required: true,
|
|
810
|
+
},
|
|
811
|
+
],
|
|
812
|
+
};
|
|
813
|
+
|
|
814
|
+
const mfaSpec: ApiSpec = {
|
|
815
|
+
name: 'Test',
|
|
816
|
+
version: '1.0.0',
|
|
817
|
+
baseUrl: 'https://api.example.com',
|
|
818
|
+
services: [mfaService],
|
|
819
|
+
models: [mfaModel],
|
|
820
|
+
enums: [],
|
|
821
|
+
};
|
|
822
|
+
|
|
823
|
+
const namingOnlyCtx: EmitterContext = {
|
|
824
|
+
namespace: 'workos',
|
|
825
|
+
namespacePascal: 'WorkOS',
|
|
826
|
+
spec: mfaSpec,
|
|
827
|
+
overlayLookup: {
|
|
828
|
+
methodByOperation: new Map([
|
|
829
|
+
[
|
|
830
|
+
'POST /auth/factors/enroll',
|
|
831
|
+
{
|
|
832
|
+
className: 'Analytics',
|
|
833
|
+
methodName: 'enrollFactor',
|
|
834
|
+
params: [],
|
|
835
|
+
returnType: 'void',
|
|
836
|
+
},
|
|
837
|
+
],
|
|
838
|
+
]),
|
|
839
|
+
httpKeyByMethod: new Map(),
|
|
840
|
+
interfaceByName: new Map(),
|
|
841
|
+
typeAliasByName: new Map(),
|
|
842
|
+
requiredExports: new Map(),
|
|
843
|
+
modelNameByIR: new Map(),
|
|
844
|
+
fileBySymbol: new Map(),
|
|
845
|
+
},
|
|
846
|
+
};
|
|
847
|
+
|
|
848
|
+
const files = generateClient(mfaSpec, namingOnlyCtx);
|
|
849
|
+
const workosFile = files.find((f) => f.path === 'src/workos.ts')!;
|
|
850
|
+
expect(workosFile.content).toContain('readonly analytics = new Analytics(this);');
|
|
851
|
+
});
|
|
852
|
+
});
|
|
853
|
+
|
|
854
|
+
describe('isServiceCoveredByExisting', () => {
|
|
855
|
+
const emptySpec: ApiSpec = {
|
|
856
|
+
name: 'Test',
|
|
857
|
+
version: '1.0.0',
|
|
858
|
+
baseUrl: '',
|
|
859
|
+
services: [],
|
|
860
|
+
models: [],
|
|
861
|
+
enums: [],
|
|
862
|
+
};
|
|
863
|
+
|
|
864
|
+
it('returns false when no overlay is provided', () => {
|
|
865
|
+
const svc: Service = {
|
|
866
|
+
name: 'Payments',
|
|
867
|
+
operations: [
|
|
868
|
+
{
|
|
869
|
+
name: 'listPayments',
|
|
870
|
+
httpMethod: 'get',
|
|
871
|
+
path: '/payments',
|
|
872
|
+
pathParams: [],
|
|
873
|
+
queryParams: [],
|
|
874
|
+
headerParams: [],
|
|
875
|
+
response: { kind: 'model', name: 'ConnectionList' },
|
|
876
|
+
errors: [],
|
|
877
|
+
injectIdempotencyKey: false,
|
|
878
|
+
},
|
|
879
|
+
],
|
|
880
|
+
};
|
|
881
|
+
const noOverlayCtx: EmitterContext = {
|
|
882
|
+
namespace: 'workos',
|
|
883
|
+
namespacePascal: 'WorkOS',
|
|
884
|
+
spec: emptySpec,
|
|
885
|
+
};
|
|
886
|
+
expect(isServiceCoveredByExisting(svc, noOverlayCtx)).toBe(false);
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
it('returns false when overlay is empty', () => {
|
|
890
|
+
const svc: Service = {
|
|
891
|
+
name: 'Payments',
|
|
892
|
+
operations: [
|
|
893
|
+
{
|
|
894
|
+
name: 'listPayments',
|
|
895
|
+
httpMethod: 'get',
|
|
896
|
+
path: '/payments',
|
|
897
|
+
pathParams: [],
|
|
898
|
+
queryParams: [],
|
|
899
|
+
headerParams: [],
|
|
900
|
+
response: { kind: 'model', name: 'ConnectionList' },
|
|
901
|
+
errors: [],
|
|
902
|
+
injectIdempotencyKey: false,
|
|
903
|
+
},
|
|
904
|
+
],
|
|
905
|
+
};
|
|
906
|
+
const emptyOverlayCtx: EmitterContext = {
|
|
907
|
+
namespace: 'workos',
|
|
908
|
+
namespacePascal: 'WorkOS',
|
|
909
|
+
spec: emptySpec,
|
|
910
|
+
overlayLookup: {
|
|
911
|
+
methodByOperation: new Map(),
|
|
912
|
+
httpKeyByMethod: new Map(),
|
|
913
|
+
interfaceByName: new Map(),
|
|
914
|
+
typeAliasByName: new Map(),
|
|
915
|
+
requiredExports: new Map(),
|
|
916
|
+
modelNameByIR: new Map(),
|
|
917
|
+
fileBySymbol: new Map(),
|
|
918
|
+
},
|
|
919
|
+
};
|
|
920
|
+
expect(isServiceCoveredByExisting(svc, emptyOverlayCtx)).toBe(false);
|
|
921
|
+
});
|
|
922
|
+
|
|
923
|
+
it('returns true when all operations are covered by overlay and class exists in baseline', () => {
|
|
924
|
+
const svc: Service = {
|
|
925
|
+
name: 'Connections',
|
|
926
|
+
operations: [
|
|
927
|
+
{
|
|
928
|
+
name: 'listConnections',
|
|
929
|
+
httpMethod: 'get',
|
|
930
|
+
path: '/connections',
|
|
931
|
+
pathParams: [],
|
|
932
|
+
queryParams: [],
|
|
933
|
+
headerParams: [],
|
|
934
|
+
response: { kind: 'model', name: 'ConnectionList' },
|
|
935
|
+
errors: [],
|
|
936
|
+
injectIdempotencyKey: false,
|
|
937
|
+
},
|
|
938
|
+
{
|
|
939
|
+
name: 'getConnection',
|
|
940
|
+
httpMethod: 'get',
|
|
941
|
+
path: '/connections/{id}',
|
|
942
|
+
pathParams: [
|
|
943
|
+
{
|
|
944
|
+
name: 'id',
|
|
945
|
+
type: { kind: 'primitive', type: 'string' },
|
|
946
|
+
required: true,
|
|
947
|
+
},
|
|
948
|
+
],
|
|
949
|
+
queryParams: [],
|
|
950
|
+
headerParams: [],
|
|
951
|
+
response: { kind: 'model', name: 'Connection' },
|
|
952
|
+
errors: [],
|
|
953
|
+
injectIdempotencyKey: false,
|
|
954
|
+
},
|
|
955
|
+
],
|
|
956
|
+
};
|
|
957
|
+
const fullCoverageCtx: EmitterContext = {
|
|
958
|
+
namespace: 'workos',
|
|
959
|
+
namespacePascal: 'WorkOS',
|
|
960
|
+
spec: emptySpec,
|
|
961
|
+
apiSurface: {
|
|
962
|
+
language: 'node',
|
|
963
|
+
extractedFrom: 'test',
|
|
964
|
+
extractedAt: '2024-01-01',
|
|
965
|
+
interfaces: {},
|
|
966
|
+
classes: {
|
|
967
|
+
Sso: {
|
|
968
|
+
name: 'Sso',
|
|
969
|
+
methods: {},
|
|
970
|
+
properties: {},
|
|
971
|
+
constructorParams: [],
|
|
972
|
+
},
|
|
973
|
+
},
|
|
974
|
+
enums: {},
|
|
975
|
+
typeAliases: {},
|
|
976
|
+
exports: {},
|
|
977
|
+
},
|
|
978
|
+
overlayLookup: {
|
|
979
|
+
methodByOperation: new Map([
|
|
980
|
+
[
|
|
981
|
+
'GET /connections',
|
|
982
|
+
{
|
|
983
|
+
className: 'Sso',
|
|
984
|
+
methodName: 'listConnections',
|
|
985
|
+
params: [],
|
|
986
|
+
returnType: 'Promise<AutoPaginatable<Connection>>',
|
|
987
|
+
},
|
|
988
|
+
],
|
|
989
|
+
[
|
|
990
|
+
'GET /connections/{id}',
|
|
991
|
+
{
|
|
992
|
+
className: 'Sso',
|
|
993
|
+
methodName: 'getConnection',
|
|
994
|
+
params: [{ name: 'id', type: 'string', optional: false }],
|
|
995
|
+
returnType: 'Promise<Connection>',
|
|
996
|
+
},
|
|
997
|
+
],
|
|
998
|
+
]),
|
|
999
|
+
httpKeyByMethod: new Map(),
|
|
1000
|
+
interfaceByName: new Map(),
|
|
1001
|
+
typeAliasByName: new Map(),
|
|
1002
|
+
requiredExports: new Map(),
|
|
1003
|
+
modelNameByIR: new Map(),
|
|
1004
|
+
fileBySymbol: new Map(),
|
|
1005
|
+
},
|
|
1006
|
+
};
|
|
1007
|
+
expect(isServiceCoveredByExisting(svc, fullCoverageCtx)).toBe(true);
|
|
1008
|
+
});
|
|
1009
|
+
|
|
1010
|
+
it('returns false when only some operations are covered', () => {
|
|
1011
|
+
const svc: Service = {
|
|
1012
|
+
name: 'Invoices',
|
|
1013
|
+
operations: [
|
|
1014
|
+
{
|
|
1015
|
+
name: 'listInvoices',
|
|
1016
|
+
httpMethod: 'get',
|
|
1017
|
+
path: '/invoices',
|
|
1018
|
+
pathParams: [],
|
|
1019
|
+
queryParams: [],
|
|
1020
|
+
headerParams: [],
|
|
1021
|
+
response: { kind: 'model', name: 'DirectoryList' },
|
|
1022
|
+
errors: [],
|
|
1023
|
+
injectIdempotencyKey: false,
|
|
1024
|
+
},
|
|
1025
|
+
{
|
|
1026
|
+
name: 'createInvoice',
|
|
1027
|
+
httpMethod: 'post',
|
|
1028
|
+
path: '/invoices',
|
|
1029
|
+
pathParams: [],
|
|
1030
|
+
queryParams: [],
|
|
1031
|
+
headerParams: [],
|
|
1032
|
+
response: { kind: 'model', name: 'Invoice' },
|
|
1033
|
+
errors: [],
|
|
1034
|
+
injectIdempotencyKey: false,
|
|
1035
|
+
},
|
|
1036
|
+
],
|
|
1037
|
+
};
|
|
1038
|
+
const partialCtx: EmitterContext = {
|
|
1039
|
+
namespace: 'workos',
|
|
1040
|
+
namespacePascal: 'WorkOS',
|
|
1041
|
+
spec: emptySpec,
|
|
1042
|
+
apiSurface: {
|
|
1043
|
+
language: 'node',
|
|
1044
|
+
extractedFrom: 'test',
|
|
1045
|
+
extractedAt: '2024-01-01',
|
|
1046
|
+
interfaces: {},
|
|
1047
|
+
classes: {
|
|
1048
|
+
Billing: {
|
|
1049
|
+
name: 'Billing',
|
|
1050
|
+
methods: {},
|
|
1051
|
+
properties: {},
|
|
1052
|
+
constructorParams: [],
|
|
1053
|
+
},
|
|
1054
|
+
},
|
|
1055
|
+
enums: {},
|
|
1056
|
+
typeAliases: {},
|
|
1057
|
+
exports: {},
|
|
1058
|
+
},
|
|
1059
|
+
overlayLookup: {
|
|
1060
|
+
methodByOperation: new Map([
|
|
1061
|
+
[
|
|
1062
|
+
'GET /invoices',
|
|
1063
|
+
{
|
|
1064
|
+
className: 'Billing',
|
|
1065
|
+
methodName: 'listInvoices',
|
|
1066
|
+
params: [],
|
|
1067
|
+
returnType: 'Promise<AutoPaginatable<Directory>>',
|
|
1068
|
+
},
|
|
1069
|
+
],
|
|
1070
|
+
]),
|
|
1071
|
+
httpKeyByMethod: new Map(),
|
|
1072
|
+
interfaceByName: new Map(),
|
|
1073
|
+
typeAliasByName: new Map(),
|
|
1074
|
+
requiredExports: new Map(),
|
|
1075
|
+
modelNameByIR: new Map(),
|
|
1076
|
+
fileBySymbol: new Map(),
|
|
1077
|
+
},
|
|
1078
|
+
};
|
|
1079
|
+
expect(isServiceCoveredByExisting(svc, partialCtx)).toBe(false);
|
|
1080
|
+
});
|
|
1081
|
+
|
|
1082
|
+
it('returns false for services with zero operations', () => {
|
|
1083
|
+
const emptySvc: Service = {
|
|
1084
|
+
name: 'Empty',
|
|
1085
|
+
operations: [],
|
|
1086
|
+
};
|
|
1087
|
+
const overlayCtx: EmitterContext = {
|
|
1088
|
+
namespace: 'workos',
|
|
1089
|
+
namespacePascal: 'WorkOS',
|
|
1090
|
+
spec: emptySpec,
|
|
1091
|
+
apiSurface: {
|
|
1092
|
+
language: 'node',
|
|
1093
|
+
extractedFrom: 'test',
|
|
1094
|
+
extractedAt: '2024-01-01',
|
|
1095
|
+
interfaces: {},
|
|
1096
|
+
classes: {
|
|
1097
|
+
Other: {
|
|
1098
|
+
name: 'Other',
|
|
1099
|
+
methods: {},
|
|
1100
|
+
properties: {},
|
|
1101
|
+
constructorParams: [],
|
|
1102
|
+
},
|
|
1103
|
+
},
|
|
1104
|
+
enums: {},
|
|
1105
|
+
typeAliases: {},
|
|
1106
|
+
exports: {},
|
|
1107
|
+
},
|
|
1108
|
+
overlayLookup: {
|
|
1109
|
+
methodByOperation: new Map([
|
|
1110
|
+
[
|
|
1111
|
+
'GET /something',
|
|
1112
|
+
{
|
|
1113
|
+
className: 'Other',
|
|
1114
|
+
methodName: 'doSomething',
|
|
1115
|
+
params: [],
|
|
1116
|
+
returnType: 'void',
|
|
1117
|
+
},
|
|
1118
|
+
],
|
|
1119
|
+
]),
|
|
1120
|
+
httpKeyByMethod: new Map(),
|
|
1121
|
+
interfaceByName: new Map(),
|
|
1122
|
+
typeAliasByName: new Map(),
|
|
1123
|
+
requiredExports: new Map(),
|
|
1124
|
+
modelNameByIR: new Map(),
|
|
1125
|
+
fileBySymbol: new Map(),
|
|
1126
|
+
},
|
|
1127
|
+
};
|
|
1128
|
+
expect(isServiceCoveredByExisting(emptySvc, overlayCtx)).toBe(false);
|
|
1129
|
+
});
|
|
1130
|
+
|
|
1131
|
+
it('returns false when overlay covers operations but target class is not in baseline', () => {
|
|
1132
|
+
const svc: Service = {
|
|
1133
|
+
name: 'Payments',
|
|
1134
|
+
operations: [
|
|
1135
|
+
{
|
|
1136
|
+
name: 'listPayments',
|
|
1137
|
+
httpMethod: 'get',
|
|
1138
|
+
path: '/payments',
|
|
1139
|
+
pathParams: [],
|
|
1140
|
+
queryParams: [],
|
|
1141
|
+
headerParams: [],
|
|
1142
|
+
response: { kind: 'model', name: 'ConnectionList' },
|
|
1143
|
+
errors: [],
|
|
1144
|
+
injectIdempotencyKey: false,
|
|
1145
|
+
},
|
|
1146
|
+
],
|
|
1147
|
+
};
|
|
1148
|
+
const missingClassCtx: EmitterContext = {
|
|
1149
|
+
namespace: 'workos',
|
|
1150
|
+
namespacePascal: 'WorkOS',
|
|
1151
|
+
spec: emptySpec,
|
|
1152
|
+
apiSurface: {
|
|
1153
|
+
language: 'node',
|
|
1154
|
+
extractedFrom: 'test',
|
|
1155
|
+
extractedAt: '2024-01-01',
|
|
1156
|
+
interfaces: {},
|
|
1157
|
+
classes: {},
|
|
1158
|
+
enums: {},
|
|
1159
|
+
typeAliases: {},
|
|
1160
|
+
exports: {},
|
|
1161
|
+
},
|
|
1162
|
+
overlayLookup: {
|
|
1163
|
+
methodByOperation: new Map([
|
|
1164
|
+
[
|
|
1165
|
+
'GET /payments',
|
|
1166
|
+
{
|
|
1167
|
+
className: 'Sso',
|
|
1168
|
+
methodName: 'listPayments',
|
|
1169
|
+
params: [],
|
|
1170
|
+
returnType: 'Promise<AutoPaginatable<Connection>>',
|
|
1171
|
+
},
|
|
1172
|
+
],
|
|
1173
|
+
]),
|
|
1174
|
+
httpKeyByMethod: new Map(),
|
|
1175
|
+
interfaceByName: new Map(),
|
|
1176
|
+
typeAliasByName: new Map(),
|
|
1177
|
+
requiredExports: new Map(),
|
|
1178
|
+
modelNameByIR: new Map(),
|
|
1179
|
+
fileBySymbol: new Map(),
|
|
1180
|
+
},
|
|
1181
|
+
};
|
|
1182
|
+
expect(isServiceCoveredByExisting(svc, missingClassCtx)).toBe(false);
|
|
1183
|
+
});
|
|
1184
|
+
|
|
1185
|
+
it('returns false when no apiSurface is provided', () => {
|
|
1186
|
+
const svc: Service = {
|
|
1187
|
+
name: 'Payments',
|
|
1188
|
+
operations: [
|
|
1189
|
+
{
|
|
1190
|
+
name: 'listPayments',
|
|
1191
|
+
httpMethod: 'get',
|
|
1192
|
+
path: '/payments',
|
|
1193
|
+
pathParams: [],
|
|
1194
|
+
queryParams: [],
|
|
1195
|
+
headerParams: [],
|
|
1196
|
+
response: { kind: 'model', name: 'ConnectionList' },
|
|
1197
|
+
errors: [],
|
|
1198
|
+
injectIdempotencyKey: false,
|
|
1199
|
+
},
|
|
1200
|
+
],
|
|
1201
|
+
};
|
|
1202
|
+
const noSurfaceCtx: EmitterContext = {
|
|
1203
|
+
namespace: 'workos',
|
|
1204
|
+
namespacePascal: 'WorkOS',
|
|
1205
|
+
spec: emptySpec,
|
|
1206
|
+
overlayLookup: {
|
|
1207
|
+
methodByOperation: new Map([
|
|
1208
|
+
[
|
|
1209
|
+
'GET /payments',
|
|
1210
|
+
{
|
|
1211
|
+
className: 'Sso',
|
|
1212
|
+
methodName: 'listPayments',
|
|
1213
|
+
params: [],
|
|
1214
|
+
returnType: 'Promise<AutoPaginatable<Connection>>',
|
|
1215
|
+
},
|
|
1216
|
+
],
|
|
1217
|
+
]),
|
|
1218
|
+
httpKeyByMethod: new Map(),
|
|
1219
|
+
interfaceByName: new Map(),
|
|
1220
|
+
typeAliasByName: new Map(),
|
|
1221
|
+
requiredExports: new Map(),
|
|
1222
|
+
modelNameByIR: new Map(),
|
|
1223
|
+
fileBySymbol: new Map(),
|
|
1224
|
+
},
|
|
1225
|
+
};
|
|
1226
|
+
expect(isServiceCoveredByExisting(svc, noSurfaceCtx)).toBe(false);
|
|
164
1227
|
});
|
|
165
1228
|
});
|