@workos/oagen-emitters 0.0.1 → 0.2.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/.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/.prettierignore +1 -0
- package/.release-please-manifest.json +3 -0
- package/.vscode/settings.json +3 -0
- package/CHANGELOG.md +54 -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 +3522 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +14 -18
- package/release-please-config.json +11 -0
- package/src/node/client.ts +437 -204
- package/src/node/common.ts +74 -4
- package/src/node/config.ts +1 -0
- package/src/node/enums.ts +50 -6
- package/src/node/errors.ts +78 -3
- package/src/node/fixtures.ts +84 -15
- package/src/node/index.ts +2 -2
- package/src/node/manifest.ts +4 -2
- package/src/node/models.ts +195 -79
- package/src/node/naming.ts +16 -1
- package/src/node/resources.ts +721 -106
- package/src/node/serializers.ts +510 -52
- package/src/node/tests.ts +621 -105
- package/src/node/type-map.ts +89 -11
- package/src/node/utils.ts +377 -114
- package/test/node/client.test.ts +979 -15
- package/test/node/enums.test.ts +0 -1
- package/test/node/errors.test.ts +4 -21
- package/test/node/models.test.ts +409 -2
- package/test/node/naming.test.ts +0 -3
- package/test/node/resources.test.ts +964 -7
- package/test/node/serializers.test.ts +212 -3
- 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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { generateResources } from '../../src/node/resources.js';
|
|
2
|
+
import { generateResources, resolveResourceClassName, hasCompatibleConstructor } from '../../src/node/resources.js';
|
|
3
3
|
import type { EmitterContext, ApiSpec, Service } from '@workos/oagen';
|
|
4
4
|
|
|
5
5
|
const emptySpec: ApiSpec = {
|
|
@@ -15,7 +15,6 @@ const ctx: EmitterContext = {
|
|
|
15
15
|
namespace: 'workos',
|
|
16
16
|
namespacePascal: 'WorkOS',
|
|
17
17
|
spec: emptySpec,
|
|
18
|
-
irVersion: 6,
|
|
19
18
|
};
|
|
20
19
|
|
|
21
20
|
describe('generateResources', () => {
|
|
@@ -81,7 +80,8 @@ describe('generateResources', () => {
|
|
|
81
80
|
response: { kind: 'model', name: 'Organization' },
|
|
82
81
|
errors: [],
|
|
83
82
|
pagination: {
|
|
84
|
-
|
|
83
|
+
strategy: 'cursor',
|
|
84
|
+
param: 'after',
|
|
85
85
|
dataPath: 'data',
|
|
86
86
|
itemType: { kind: 'model', name: 'Organization' },
|
|
87
87
|
},
|
|
@@ -94,9 +94,9 @@ describe('generateResources', () => {
|
|
|
94
94
|
const files = generateResources(services, ctx);
|
|
95
95
|
const content = files[0].content;
|
|
96
96
|
|
|
97
|
-
// Should have AutoPaginatable
|
|
98
|
-
expect(content).toContain(
|
|
99
|
-
expect(content).toContain(
|
|
97
|
+
// Should have AutoPaginatable type import and createPaginatedList import
|
|
98
|
+
expect(content).toContain("import type { AutoPaginatable } from '../common/utils/pagination'");
|
|
99
|
+
expect(content).toContain("import { createPaginatedList } from '../common/utils/fetch-and-deserialize'");
|
|
100
100
|
|
|
101
101
|
// Should generate options interface
|
|
102
102
|
expect(content).toContain('export interface ListOrganizationsOptions extends PaginationOptions {');
|
|
@@ -106,6 +106,54 @@ describe('generateResources', () => {
|
|
|
106
106
|
expect(content).toContain('Promise<AutoPaginatable<Organization, ListOrganizationsOptions>>');
|
|
107
107
|
});
|
|
108
108
|
|
|
109
|
+
it('uses item type not list wrapper type for paginated methods', () => {
|
|
110
|
+
// The response model is the list wrapper (ConnectionList), but the pagination
|
|
111
|
+
// itemType is the actual item (Connection). The generated code should use the
|
|
112
|
+
// item type for fetchAndDeserialize, not the list wrapper.
|
|
113
|
+
const services: Service[] = [
|
|
114
|
+
{
|
|
115
|
+
name: 'SSO',
|
|
116
|
+
operations: [
|
|
117
|
+
{
|
|
118
|
+
name: 'listConnections',
|
|
119
|
+
httpMethod: 'get',
|
|
120
|
+
path: '/connections',
|
|
121
|
+
pathParams: [],
|
|
122
|
+
queryParams: [],
|
|
123
|
+
headerParams: [],
|
|
124
|
+
response: { kind: 'model', name: 'ConnectionList' },
|
|
125
|
+
errors: [],
|
|
126
|
+
pagination: {
|
|
127
|
+
strategy: 'cursor',
|
|
128
|
+
param: 'after',
|
|
129
|
+
dataPath: 'data',
|
|
130
|
+
itemType: { kind: 'model', name: 'Connection' },
|
|
131
|
+
},
|
|
132
|
+
injectIdempotencyKey: false,
|
|
133
|
+
},
|
|
134
|
+
],
|
|
135
|
+
},
|
|
136
|
+
];
|
|
137
|
+
|
|
138
|
+
const testCtx: EmitterContext = {
|
|
139
|
+
namespace: 'workos',
|
|
140
|
+
namespacePascal: 'WorkOS',
|
|
141
|
+
spec: { ...emptySpec, services, models: [] },
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const files = generateResources(services, testCtx);
|
|
145
|
+
const content = files[0].content;
|
|
146
|
+
|
|
147
|
+
// Should use item type (Connection) not list wrapper (ConnectionList)
|
|
148
|
+
expect(content).toContain('createPaginatedList<ConnectionResponse, Connection,');
|
|
149
|
+
expect(content).toContain('deserializeConnection');
|
|
150
|
+
expect(content).toContain('Promise<AutoPaginatable<Connection,');
|
|
151
|
+
|
|
152
|
+
// Should NOT reference the list wrapper type
|
|
153
|
+
expect(content).not.toContain('ConnectionList');
|
|
154
|
+
expect(content).not.toContain('deserializeConnectionList');
|
|
155
|
+
});
|
|
156
|
+
|
|
109
157
|
it('generates DELETE method returning void', () => {
|
|
110
158
|
const services: Service[] = [
|
|
111
159
|
{
|
|
@@ -191,7 +239,6 @@ describe('generateResources', () => {
|
|
|
191
239
|
namespace: 'workos',
|
|
192
240
|
namespacePascal: 'WorkOS',
|
|
193
241
|
spec: { ...emptySpec, services: [mfaService], models: [] },
|
|
194
|
-
irVersion: 6,
|
|
195
242
|
overlayLookup: {
|
|
196
243
|
methodByOperation: new Map([
|
|
197
244
|
[
|
|
@@ -254,7 +301,917 @@ describe('generateResources', () => {
|
|
|
254
301
|
expect(content).toContain(' *');
|
|
255
302
|
expect(content).toContain(' * You may optionally inform Radar that an attempt was successful.');
|
|
256
303
|
expect(content).toContain(' * @param id - The unique identifier of the attempt.');
|
|
304
|
+
expect(content).toContain(' * @returns {RadarAttempt}');
|
|
257
305
|
expect(content).toContain(' * @deprecated');
|
|
258
306
|
expect(content).toContain(' */');
|
|
259
307
|
});
|
|
308
|
+
|
|
309
|
+
it('renders @returns for response model', () => {
|
|
310
|
+
const services: Service[] = [
|
|
311
|
+
{
|
|
312
|
+
name: 'Organizations',
|
|
313
|
+
operations: [
|
|
314
|
+
{
|
|
315
|
+
name: 'getOrganization',
|
|
316
|
+
httpMethod: 'get',
|
|
317
|
+
path: '/organizations/{id}',
|
|
318
|
+
pathParams: [{ name: 'id', type: { kind: 'primitive', type: 'string' }, required: true }],
|
|
319
|
+
queryParams: [],
|
|
320
|
+
headerParams: [],
|
|
321
|
+
response: { kind: 'model', name: 'Organization' },
|
|
322
|
+
errors: [],
|
|
323
|
+
injectIdempotencyKey: false,
|
|
324
|
+
},
|
|
325
|
+
],
|
|
326
|
+
},
|
|
327
|
+
];
|
|
328
|
+
|
|
329
|
+
const files = generateResources(services, ctx);
|
|
330
|
+
const content = files[0].content;
|
|
331
|
+
expect(content).toContain('@returns {Organization}');
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it('renders query param docs for non-paginated operations', () => {
|
|
335
|
+
const services: Service[] = [
|
|
336
|
+
{
|
|
337
|
+
name: 'Organizations',
|
|
338
|
+
operations: [
|
|
339
|
+
{
|
|
340
|
+
name: 'getOrganization',
|
|
341
|
+
httpMethod: 'get',
|
|
342
|
+
path: '/organizations/{id}',
|
|
343
|
+
pathParams: [{ name: 'id', type: { kind: 'primitive', type: 'string' }, required: true }],
|
|
344
|
+
queryParams: [
|
|
345
|
+
{
|
|
346
|
+
name: 'include_fields',
|
|
347
|
+
type: { kind: 'primitive', type: 'string' },
|
|
348
|
+
required: false,
|
|
349
|
+
description: 'Comma-separated list of fields to include.',
|
|
350
|
+
},
|
|
351
|
+
],
|
|
352
|
+
headerParams: [],
|
|
353
|
+
response: { kind: 'model', name: 'Organization' },
|
|
354
|
+
errors: [],
|
|
355
|
+
injectIdempotencyKey: false,
|
|
356
|
+
},
|
|
357
|
+
],
|
|
358
|
+
},
|
|
359
|
+
];
|
|
360
|
+
|
|
361
|
+
const files = generateResources(services, ctx);
|
|
362
|
+
const content = files[0].content;
|
|
363
|
+
expect(content).toContain('@param options.includeFields - Comma-separated list of fields to include.');
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
it('renders header and cookie param docs', () => {
|
|
367
|
+
const services: Service[] = [
|
|
368
|
+
{
|
|
369
|
+
name: 'Sessions',
|
|
370
|
+
operations: [
|
|
371
|
+
{
|
|
372
|
+
name: 'getSession',
|
|
373
|
+
httpMethod: 'get',
|
|
374
|
+
path: '/sessions/{id}',
|
|
375
|
+
pathParams: [{ name: 'id', type: { kind: 'primitive', type: 'string' }, required: true }],
|
|
376
|
+
queryParams: [],
|
|
377
|
+
headerParams: [
|
|
378
|
+
{
|
|
379
|
+
name: 'X-Request-Id',
|
|
380
|
+
type: { kind: 'primitive', type: 'string' },
|
|
381
|
+
required: false,
|
|
382
|
+
description: 'Unique request identifier.',
|
|
383
|
+
},
|
|
384
|
+
],
|
|
385
|
+
cookieParams: [
|
|
386
|
+
{
|
|
387
|
+
name: 'session_token',
|
|
388
|
+
type: { kind: 'primitive', type: 'string' },
|
|
389
|
+
required: true,
|
|
390
|
+
description: 'The session cookie.',
|
|
391
|
+
},
|
|
392
|
+
],
|
|
393
|
+
response: { kind: 'model', name: 'Session' },
|
|
394
|
+
errors: [],
|
|
395
|
+
injectIdempotencyKey: false,
|
|
396
|
+
},
|
|
397
|
+
],
|
|
398
|
+
},
|
|
399
|
+
];
|
|
400
|
+
|
|
401
|
+
const files = generateResources(services, ctx);
|
|
402
|
+
const content = files[0].content;
|
|
403
|
+
// Header and cookie params are intentionally NOT documented in JSDoc —
|
|
404
|
+
// they are not exposed in the method signature (handled internally by the SDK).
|
|
405
|
+
expect(content).not.toContain('@param xRequestId');
|
|
406
|
+
expect(content).not.toContain('@param sessionToken');
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
it('renders single @returns without status-code duplicates', () => {
|
|
410
|
+
const services: Service[] = [
|
|
411
|
+
{
|
|
412
|
+
name: 'Organizations',
|
|
413
|
+
operations: [
|
|
414
|
+
{
|
|
415
|
+
name: 'createOrganization',
|
|
416
|
+
httpMethod: 'post',
|
|
417
|
+
path: '/organizations',
|
|
418
|
+
pathParams: [],
|
|
419
|
+
queryParams: [],
|
|
420
|
+
headerParams: [],
|
|
421
|
+
requestBody: { kind: 'model', name: 'CreateOrganizationInput' },
|
|
422
|
+
response: { kind: 'model', name: 'Organization' },
|
|
423
|
+
successResponses: [
|
|
424
|
+
{ statusCode: 200, type: { kind: 'model', name: 'Organization' } },
|
|
425
|
+
{ statusCode: 201, type: { kind: 'model', name: 'Organization' } },
|
|
426
|
+
],
|
|
427
|
+
errors: [],
|
|
428
|
+
injectIdempotencyKey: false,
|
|
429
|
+
},
|
|
430
|
+
],
|
|
431
|
+
},
|
|
432
|
+
];
|
|
433
|
+
|
|
434
|
+
const files = generateResources(services, ctx);
|
|
435
|
+
const content = files[0].content;
|
|
436
|
+
// Only emit a single @returns for the primary response model (no status-code variants)
|
|
437
|
+
expect(content).toContain('@returns {Organization}');
|
|
438
|
+
expect(content).not.toContain('@returns {Organization} 200');
|
|
439
|
+
expect(content).not.toContain('@returns {Organization} 201');
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
it('generates DELETE-with-body method using deleteWithBody', () => {
|
|
443
|
+
const services: Service[] = [
|
|
444
|
+
{
|
|
445
|
+
name: 'Radar',
|
|
446
|
+
operations: [
|
|
447
|
+
{
|
|
448
|
+
name: 'deleteRadarListEntry',
|
|
449
|
+
httpMethod: 'delete',
|
|
450
|
+
path: '/radar/lists/{listId}/entries',
|
|
451
|
+
pathParams: [
|
|
452
|
+
{
|
|
453
|
+
name: 'listId',
|
|
454
|
+
type: { kind: 'primitive', type: 'string' },
|
|
455
|
+
required: true,
|
|
456
|
+
},
|
|
457
|
+
],
|
|
458
|
+
queryParams: [],
|
|
459
|
+
headerParams: [],
|
|
460
|
+
requestBody: { kind: 'model', name: 'DeleteRadarListEntryInput' },
|
|
461
|
+
response: { kind: 'primitive', type: 'unknown' },
|
|
462
|
+
errors: [],
|
|
463
|
+
injectIdempotencyKey: false,
|
|
464
|
+
},
|
|
465
|
+
],
|
|
466
|
+
},
|
|
467
|
+
];
|
|
468
|
+
|
|
469
|
+
const files = generateResources(services, ctx);
|
|
470
|
+
const content = files[0].content;
|
|
471
|
+
expect(content).toContain(
|
|
472
|
+
'async deleteRadarListEntry(listId: string, payload: DeleteRadarListEntryInput): Promise<void>',
|
|
473
|
+
);
|
|
474
|
+
expect(content).toContain('await this.workos.deleteWithBody(');
|
|
475
|
+
expect(content).toContain('serializeDeleteRadarListEntryInput(payload)');
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
it('renders deprecated path params', () => {
|
|
479
|
+
const services: Service[] = [
|
|
480
|
+
{
|
|
481
|
+
name: 'Organizations',
|
|
482
|
+
operations: [
|
|
483
|
+
{
|
|
484
|
+
name: 'getOrganization',
|
|
485
|
+
httpMethod: 'get',
|
|
486
|
+
path: '/organizations/{slug}',
|
|
487
|
+
pathParams: [
|
|
488
|
+
{
|
|
489
|
+
name: 'slug',
|
|
490
|
+
type: { kind: 'primitive', type: 'string' },
|
|
491
|
+
required: true,
|
|
492
|
+
description: 'The organization slug.',
|
|
493
|
+
deprecated: true,
|
|
494
|
+
},
|
|
495
|
+
],
|
|
496
|
+
queryParams: [],
|
|
497
|
+
headerParams: [],
|
|
498
|
+
response: { kind: 'model', name: 'Organization' },
|
|
499
|
+
errors: [],
|
|
500
|
+
injectIdempotencyKey: false,
|
|
501
|
+
},
|
|
502
|
+
],
|
|
503
|
+
},
|
|
504
|
+
];
|
|
505
|
+
|
|
506
|
+
const files = generateResources(services, ctx);
|
|
507
|
+
const content = files[0].content;
|
|
508
|
+
expect(content).toContain('@param slug - (deprecated) The organization slug.');
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
it('generates typed options interface for non-paginated GET with query params', () => {
|
|
512
|
+
const services: Service[] = [
|
|
513
|
+
{
|
|
514
|
+
name: 'Organizations',
|
|
515
|
+
operations: [
|
|
516
|
+
{
|
|
517
|
+
name: 'getOrganization',
|
|
518
|
+
httpMethod: 'get',
|
|
519
|
+
path: '/organizations/{id}',
|
|
520
|
+
pathParams: [{ name: 'id', type: { kind: 'primitive', type: 'string' }, required: true }],
|
|
521
|
+
queryParams: [
|
|
522
|
+
{
|
|
523
|
+
name: 'include_fields',
|
|
524
|
+
type: { kind: 'primitive', type: 'string' },
|
|
525
|
+
required: false,
|
|
526
|
+
description: 'Comma-separated list of fields to include.',
|
|
527
|
+
},
|
|
528
|
+
],
|
|
529
|
+
headerParams: [],
|
|
530
|
+
response: { kind: 'model', name: 'Organization' },
|
|
531
|
+
errors: [],
|
|
532
|
+
injectIdempotencyKey: false,
|
|
533
|
+
},
|
|
534
|
+
],
|
|
535
|
+
},
|
|
536
|
+
];
|
|
537
|
+
|
|
538
|
+
const files = generateResources(services, ctx);
|
|
539
|
+
const content = files[0].content;
|
|
540
|
+
|
|
541
|
+
// Should generate a typed options interface
|
|
542
|
+
expect(content).toContain('export interface GetOrganizationOptions {');
|
|
543
|
+
expect(content).toContain('includeFields?: string;');
|
|
544
|
+
|
|
545
|
+
// Should use the typed options in the method signature
|
|
546
|
+
expect(content).toContain(
|
|
547
|
+
'async getOrganization(id: string, options?: GetOrganizationOptions): Promise<Organization>',
|
|
548
|
+
);
|
|
549
|
+
|
|
550
|
+
// Should NOT use Record<string, unknown>
|
|
551
|
+
expect(content).not.toContain('Record<string, unknown>');
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
it('generates typed options interface for void GET with query params', () => {
|
|
555
|
+
const services: Service[] = [
|
|
556
|
+
{
|
|
557
|
+
name: 'Auth',
|
|
558
|
+
operations: [
|
|
559
|
+
{
|
|
560
|
+
name: 'authorize',
|
|
561
|
+
httpMethod: 'get',
|
|
562
|
+
path: '/user_management/authorize',
|
|
563
|
+
pathParams: [],
|
|
564
|
+
queryParams: [
|
|
565
|
+
{
|
|
566
|
+
name: 'client_id',
|
|
567
|
+
type: { kind: 'primitive', type: 'string' },
|
|
568
|
+
required: true,
|
|
569
|
+
},
|
|
570
|
+
{
|
|
571
|
+
name: 'redirect_uri',
|
|
572
|
+
type: { kind: 'primitive', type: 'string' },
|
|
573
|
+
required: true,
|
|
574
|
+
},
|
|
575
|
+
{
|
|
576
|
+
name: 'response_type',
|
|
577
|
+
type: { kind: 'primitive', type: 'string' },
|
|
578
|
+
required: true,
|
|
579
|
+
},
|
|
580
|
+
],
|
|
581
|
+
headerParams: [],
|
|
582
|
+
response: { kind: 'primitive', type: 'unknown' },
|
|
583
|
+
errors: [],
|
|
584
|
+
injectIdempotencyKey: false,
|
|
585
|
+
},
|
|
586
|
+
],
|
|
587
|
+
},
|
|
588
|
+
];
|
|
589
|
+
|
|
590
|
+
const files = generateResources(services, ctx);
|
|
591
|
+
const content = files[0].content;
|
|
592
|
+
|
|
593
|
+
// Should generate a typed options interface
|
|
594
|
+
expect(content).toContain('export interface AuthorizeOptions {');
|
|
595
|
+
expect(content).toContain('clientId: string;');
|
|
596
|
+
expect(content).toContain('redirectUri: string;');
|
|
597
|
+
expect(content).toContain('responseType: string;');
|
|
598
|
+
|
|
599
|
+
// Should use the typed options in the method signature
|
|
600
|
+
expect(content).toContain('async authorize(options?: AuthorizeOptions): Promise<void>');
|
|
601
|
+
|
|
602
|
+
// Should pass options as query params
|
|
603
|
+
expect(content).toContain('query: options');
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
it('generates union type for non-discriminated request body (pass-through)', () => {
|
|
607
|
+
const services: Service[] = [
|
|
608
|
+
{
|
|
609
|
+
name: 'Auth',
|
|
610
|
+
operations: [
|
|
611
|
+
{
|
|
612
|
+
name: 'authenticate',
|
|
613
|
+
httpMethod: 'post',
|
|
614
|
+
path: '/user_management/authenticate',
|
|
615
|
+
pathParams: [],
|
|
616
|
+
queryParams: [],
|
|
617
|
+
headerParams: [],
|
|
618
|
+
requestBody: {
|
|
619
|
+
kind: 'union',
|
|
620
|
+
variants: [
|
|
621
|
+
{ kind: 'model', name: 'AuthByPassword' },
|
|
622
|
+
{ kind: 'model', name: 'AuthByCode' },
|
|
623
|
+
{ kind: 'model', name: 'AuthByMagicAuth' },
|
|
624
|
+
],
|
|
625
|
+
},
|
|
626
|
+
response: { kind: 'model', name: 'AuthenticateResponse' },
|
|
627
|
+
errors: [],
|
|
628
|
+
injectIdempotencyKey: false,
|
|
629
|
+
},
|
|
630
|
+
],
|
|
631
|
+
},
|
|
632
|
+
];
|
|
633
|
+
|
|
634
|
+
const files = generateResources(services, ctx);
|
|
635
|
+
const content = files[0].content;
|
|
636
|
+
|
|
637
|
+
// Should use the union type for the payload parameter
|
|
638
|
+
expect(content).toContain('payload: AuthByPassword | AuthByCode | AuthByMagicAuth');
|
|
639
|
+
|
|
640
|
+
// Should NOT use Record<string, unknown>
|
|
641
|
+
expect(content).not.toContain('Record<string, unknown>');
|
|
642
|
+
|
|
643
|
+
// Should pass payload directly (no serializer for unions)
|
|
644
|
+
expect(content).toContain("'/user_management/authenticate',");
|
|
645
|
+
expect(content).toContain('payload,');
|
|
646
|
+
|
|
647
|
+
// Should import all union variant types
|
|
648
|
+
expect(content).toContain('AuthByPassword');
|
|
649
|
+
expect(content).toContain('AuthByCode');
|
|
650
|
+
expect(content).toContain('AuthByMagicAuth');
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
it('generates discriminated union serializer dispatch for request body', () => {
|
|
654
|
+
const services: Service[] = [
|
|
655
|
+
{
|
|
656
|
+
name: 'Auth',
|
|
657
|
+
operations: [
|
|
658
|
+
{
|
|
659
|
+
name: 'authenticate',
|
|
660
|
+
httpMethod: 'post',
|
|
661
|
+
path: '/user_management/authenticate',
|
|
662
|
+
pathParams: [],
|
|
663
|
+
queryParams: [],
|
|
664
|
+
headerParams: [],
|
|
665
|
+
requestBody: {
|
|
666
|
+
kind: 'union',
|
|
667
|
+
variants: [
|
|
668
|
+
{ kind: 'model', name: 'AuthByPassword' },
|
|
669
|
+
{ kind: 'model', name: 'AuthByCode' },
|
|
670
|
+
{ kind: 'model', name: 'AuthByMagicAuth' },
|
|
671
|
+
],
|
|
672
|
+
discriminator: {
|
|
673
|
+
property: 'grant_type',
|
|
674
|
+
mapping: {
|
|
675
|
+
password: 'AuthByPassword',
|
|
676
|
+
authorization_code: 'AuthByCode',
|
|
677
|
+
'urn:workos:oauth:grant-type:magic-auth:code': 'AuthByMagicAuth',
|
|
678
|
+
},
|
|
679
|
+
},
|
|
680
|
+
},
|
|
681
|
+
response: { kind: 'model', name: 'AuthenticateResponse' },
|
|
682
|
+
errors: [],
|
|
683
|
+
injectIdempotencyKey: false,
|
|
684
|
+
},
|
|
685
|
+
],
|
|
686
|
+
},
|
|
687
|
+
];
|
|
688
|
+
|
|
689
|
+
const files = generateResources(services, ctx);
|
|
690
|
+
const content = files[0].content;
|
|
691
|
+
|
|
692
|
+
// Should use the union type for the payload parameter
|
|
693
|
+
expect(content).toContain('payload: AuthByPassword | AuthByCode | AuthByMagicAuth');
|
|
694
|
+
|
|
695
|
+
// Should dispatch to the correct serializer based on the discriminator
|
|
696
|
+
expect(content).toContain('switch ((payload as any).grantType)');
|
|
697
|
+
expect(content).toContain("case 'password': return serializeAuthByPassword(payload as any)");
|
|
698
|
+
expect(content).toContain("case 'authorization_code': return serializeAuthByCode(payload as any)");
|
|
699
|
+
expect(content).toContain(
|
|
700
|
+
"case 'urn:workos:oauth:grant-type:magic-auth:code': return serializeAuthByMagicAuth(payload as any)",
|
|
701
|
+
);
|
|
702
|
+
|
|
703
|
+
// Should import serializers for all union variants
|
|
704
|
+
expect(content).toContain('serializeAuthByPassword');
|
|
705
|
+
expect(content).toContain('serializeAuthByCode');
|
|
706
|
+
expect(content).toContain('serializeAuthByMagicAuth');
|
|
707
|
+
|
|
708
|
+
// Should NOT pass payload directly without serialization
|
|
709
|
+
expect(content).not.toMatch(/,\n\s+payload,\n/);
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
it('generates discriminated union serializer dispatch for void method', () => {
|
|
713
|
+
const services: Service[] = [
|
|
714
|
+
{
|
|
715
|
+
name: 'Auth',
|
|
716
|
+
operations: [
|
|
717
|
+
{
|
|
718
|
+
name: 'sendToken',
|
|
719
|
+
httpMethod: 'post',
|
|
720
|
+
path: '/auth/token',
|
|
721
|
+
pathParams: [],
|
|
722
|
+
queryParams: [],
|
|
723
|
+
headerParams: [],
|
|
724
|
+
requestBody: {
|
|
725
|
+
kind: 'union',
|
|
726
|
+
variants: [
|
|
727
|
+
{ kind: 'model', name: 'TokenByCode' },
|
|
728
|
+
{ kind: 'model', name: 'TokenByRefresh' },
|
|
729
|
+
],
|
|
730
|
+
discriminator: {
|
|
731
|
+
property: 'grant_type',
|
|
732
|
+
mapping: {
|
|
733
|
+
authorization_code: 'TokenByCode',
|
|
734
|
+
refresh_token: 'TokenByRefresh',
|
|
735
|
+
},
|
|
736
|
+
},
|
|
737
|
+
},
|
|
738
|
+
response: { kind: 'primitive', type: 'unknown' },
|
|
739
|
+
errors: [],
|
|
740
|
+
injectIdempotencyKey: false,
|
|
741
|
+
},
|
|
742
|
+
],
|
|
743
|
+
},
|
|
744
|
+
];
|
|
745
|
+
|
|
746
|
+
const files = generateResources(services, ctx);
|
|
747
|
+
const content = files[0].content;
|
|
748
|
+
|
|
749
|
+
// Should dispatch to the correct serializer
|
|
750
|
+
expect(content).toContain('switch ((payload as any).grantType)');
|
|
751
|
+
expect(content).toContain("case 'authorization_code': return serializeTokenByCode(payload as any)");
|
|
752
|
+
expect(content).toContain("case 'refresh_token': return serializeTokenByRefresh(payload as any)");
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
it('uses createPaginatedList helper in paginated methods', () => {
|
|
756
|
+
const services: Service[] = [
|
|
757
|
+
{
|
|
758
|
+
name: 'Connections',
|
|
759
|
+
operations: [
|
|
760
|
+
{
|
|
761
|
+
name: 'listConnections',
|
|
762
|
+
httpMethod: 'get',
|
|
763
|
+
path: '/connections',
|
|
764
|
+
pathParams: [],
|
|
765
|
+
queryParams: [],
|
|
766
|
+
headerParams: [],
|
|
767
|
+
response: { kind: 'model', name: 'Connection' },
|
|
768
|
+
errors: [],
|
|
769
|
+
pagination: {
|
|
770
|
+
strategy: 'cursor',
|
|
771
|
+
param: 'after',
|
|
772
|
+
dataPath: 'data',
|
|
773
|
+
itemType: { kind: 'model', name: 'Connection' },
|
|
774
|
+
},
|
|
775
|
+
injectIdempotencyKey: false,
|
|
776
|
+
},
|
|
777
|
+
],
|
|
778
|
+
},
|
|
779
|
+
];
|
|
780
|
+
|
|
781
|
+
const files = generateResources(services, ctx);
|
|
782
|
+
const content = files[0].content;
|
|
783
|
+
|
|
784
|
+
// Should use createPaginatedList helper for concise paginated methods
|
|
785
|
+
expect(content).toContain('createPaginatedList<ConnectionResponse, Connection,');
|
|
786
|
+
expect(content).toContain('this.workos,');
|
|
787
|
+
expect(content).toContain('deserializeConnection');
|
|
788
|
+
});
|
|
789
|
+
|
|
790
|
+
it('prefixes ListOptions with service name when method is "list"', () => {
|
|
791
|
+
const services: Service[] = [
|
|
792
|
+
{
|
|
793
|
+
name: 'Connections',
|
|
794
|
+
operations: [
|
|
795
|
+
{
|
|
796
|
+
name: 'list',
|
|
797
|
+
httpMethod: 'get',
|
|
798
|
+
path: '/connections',
|
|
799
|
+
pathParams: [],
|
|
800
|
+
queryParams: [
|
|
801
|
+
{
|
|
802
|
+
name: 'connection_type',
|
|
803
|
+
type: { kind: 'primitive', type: 'string' },
|
|
804
|
+
required: false,
|
|
805
|
+
},
|
|
806
|
+
],
|
|
807
|
+
headerParams: [],
|
|
808
|
+
response: { kind: 'model', name: 'Connection' },
|
|
809
|
+
errors: [],
|
|
810
|
+
pagination: {
|
|
811
|
+
strategy: 'cursor',
|
|
812
|
+
param: 'after',
|
|
813
|
+
dataPath: 'data',
|
|
814
|
+
itemType: { kind: 'model', name: 'Connection' },
|
|
815
|
+
},
|
|
816
|
+
injectIdempotencyKey: false,
|
|
817
|
+
},
|
|
818
|
+
],
|
|
819
|
+
},
|
|
820
|
+
];
|
|
821
|
+
|
|
822
|
+
// Use overlay to resolve method name to "list"
|
|
823
|
+
const overlayCtx: EmitterContext = {
|
|
824
|
+
namespace: 'workos',
|
|
825
|
+
namespacePascal: 'WorkOS',
|
|
826
|
+
spec: { ...emptySpec, services, models: [] },
|
|
827
|
+
overlayLookup: {
|
|
828
|
+
methodByOperation: new Map([
|
|
829
|
+
['GET /connections', { className: 'Connections', methodName: 'list', params: [], returnType: 'void' }],
|
|
830
|
+
]),
|
|
831
|
+
httpKeyByMethod: new Map(),
|
|
832
|
+
interfaceByName: new Map(),
|
|
833
|
+
typeAliasByName: new Map(),
|
|
834
|
+
requiredExports: new Map(),
|
|
835
|
+
modelNameByIR: new Map(),
|
|
836
|
+
fileBySymbol: new Map(),
|
|
837
|
+
},
|
|
838
|
+
};
|
|
839
|
+
|
|
840
|
+
const files = generateResources(services, overlayCtx);
|
|
841
|
+
const content = files[0].content;
|
|
842
|
+
|
|
843
|
+
// Should use service-prefixed options name instead of generic "ListOptions"
|
|
844
|
+
expect(content).toContain('export interface ConnectionsListOptions extends PaginationOptions {');
|
|
845
|
+
expect(content).toContain('Promise<AutoPaginatable<Connection, ConnectionsListOptions>>');
|
|
846
|
+
// Should NOT use the generic "ListOptions"
|
|
847
|
+
expect(content).not.toContain('export interface ListOptions ');
|
|
848
|
+
});
|
|
849
|
+
|
|
850
|
+
it('does not prefix ListOptions when method is not "list"', () => {
|
|
851
|
+
const services: Service[] = [
|
|
852
|
+
{
|
|
853
|
+
name: 'Organizations',
|
|
854
|
+
operations: [
|
|
855
|
+
{
|
|
856
|
+
name: 'listOrganizations',
|
|
857
|
+
httpMethod: 'get',
|
|
858
|
+
path: '/organizations',
|
|
859
|
+
pathParams: [],
|
|
860
|
+
queryParams: [
|
|
861
|
+
{
|
|
862
|
+
name: 'domains',
|
|
863
|
+
type: { kind: 'array', items: { kind: 'primitive', type: 'string' } },
|
|
864
|
+
required: false,
|
|
865
|
+
},
|
|
866
|
+
],
|
|
867
|
+
headerParams: [],
|
|
868
|
+
response: { kind: 'model', name: 'Organization' },
|
|
869
|
+
errors: [],
|
|
870
|
+
pagination: {
|
|
871
|
+
strategy: 'cursor',
|
|
872
|
+
param: 'after',
|
|
873
|
+
dataPath: 'data',
|
|
874
|
+
itemType: { kind: 'model', name: 'Organization' },
|
|
875
|
+
},
|
|
876
|
+
injectIdempotencyKey: false,
|
|
877
|
+
},
|
|
878
|
+
],
|
|
879
|
+
},
|
|
880
|
+
];
|
|
881
|
+
|
|
882
|
+
const files = generateResources(services, ctx);
|
|
883
|
+
const content = files[0].content;
|
|
884
|
+
|
|
885
|
+
// Method is "listOrganizations", not "list", so options name should be normal
|
|
886
|
+
expect(content).toContain('export interface ListOrganizationsOptions extends PaginationOptions {');
|
|
887
|
+
});
|
|
888
|
+
});
|
|
889
|
+
|
|
890
|
+
describe('resolveResourceClassName', () => {
|
|
891
|
+
const webhooksService: Service = {
|
|
892
|
+
name: 'WebhookEvents',
|
|
893
|
+
operations: [
|
|
894
|
+
{
|
|
895
|
+
name: 'listWebhookEvents',
|
|
896
|
+
httpMethod: 'get',
|
|
897
|
+
path: '/webhook_events',
|
|
898
|
+
pathParams: [],
|
|
899
|
+
queryParams: [],
|
|
900
|
+
headerParams: [],
|
|
901
|
+
response: { kind: 'model', name: 'WebhookEvent' },
|
|
902
|
+
errors: [],
|
|
903
|
+
injectIdempotencyKey: false,
|
|
904
|
+
},
|
|
905
|
+
],
|
|
906
|
+
};
|
|
907
|
+
|
|
908
|
+
it('generates separate class when baseline has incompatible constructor', () => {
|
|
909
|
+
const overlayCtx: EmitterContext = {
|
|
910
|
+
namespace: 'workos',
|
|
911
|
+
namespacePascal: 'WorkOS',
|
|
912
|
+
spec: { ...emptySpec, services: [webhooksService] },
|
|
913
|
+
overlayLookup: {
|
|
914
|
+
methodByOperation: new Map([
|
|
915
|
+
[
|
|
916
|
+
'GET /webhook_events',
|
|
917
|
+
{ className: 'Webhooks', methodName: 'listWebhookEvents', params: [], returnType: 'void' },
|
|
918
|
+
],
|
|
919
|
+
]),
|
|
920
|
+
httpKeyByMethod: new Map(),
|
|
921
|
+
interfaceByName: new Map(),
|
|
922
|
+
typeAliasByName: new Map(),
|
|
923
|
+
requiredExports: new Map(),
|
|
924
|
+
modelNameByIR: new Map(),
|
|
925
|
+
fileBySymbol: new Map(),
|
|
926
|
+
},
|
|
927
|
+
apiSurface: {
|
|
928
|
+
language: 'node',
|
|
929
|
+
extractedFrom: 'test',
|
|
930
|
+
extractedAt: '2024-01-01',
|
|
931
|
+
classes: {
|
|
932
|
+
Webhooks: {
|
|
933
|
+
name: 'Webhooks',
|
|
934
|
+
methods: {},
|
|
935
|
+
properties: {},
|
|
936
|
+
constructorParams: [{ name: 'cryptoProvider', type: 'CryptoProvider', optional: false }],
|
|
937
|
+
},
|
|
938
|
+
},
|
|
939
|
+
interfaces: {},
|
|
940
|
+
typeAliases: {},
|
|
941
|
+
enums: {},
|
|
942
|
+
exports: {},
|
|
943
|
+
},
|
|
944
|
+
};
|
|
945
|
+
|
|
946
|
+
const result = resolveResourceClassName(webhooksService, overlayCtx);
|
|
947
|
+
// Falls back to IR name since overlay name has incompatible constructor
|
|
948
|
+
expect(result).toBe('WebhookEvents');
|
|
949
|
+
});
|
|
950
|
+
|
|
951
|
+
it('uses overlay name when baseline has compatible constructor', () => {
|
|
952
|
+
const overlayCtx: EmitterContext = {
|
|
953
|
+
namespace: 'workos',
|
|
954
|
+
namespacePascal: 'WorkOS',
|
|
955
|
+
spec: { ...emptySpec, services: [webhooksService] },
|
|
956
|
+
overlayLookup: {
|
|
957
|
+
methodByOperation: new Map([
|
|
958
|
+
[
|
|
959
|
+
'GET /webhook_events',
|
|
960
|
+
{ className: 'Webhooks', methodName: 'listWebhookEvents', params: [], returnType: 'void' },
|
|
961
|
+
],
|
|
962
|
+
]),
|
|
963
|
+
httpKeyByMethod: new Map(),
|
|
964
|
+
interfaceByName: new Map(),
|
|
965
|
+
typeAliasByName: new Map(),
|
|
966
|
+
requiredExports: new Map(),
|
|
967
|
+
modelNameByIR: new Map(),
|
|
968
|
+
fileBySymbol: new Map(),
|
|
969
|
+
},
|
|
970
|
+
apiSurface: {
|
|
971
|
+
language: 'node',
|
|
972
|
+
extractedFrom: 'test',
|
|
973
|
+
extractedAt: '2024-01-01',
|
|
974
|
+
classes: {
|
|
975
|
+
Webhooks: {
|
|
976
|
+
name: 'Webhooks',
|
|
977
|
+
methods: {},
|
|
978
|
+
properties: {},
|
|
979
|
+
constructorParams: [{ name: 'workos', type: 'WorkOS', optional: false }],
|
|
980
|
+
},
|
|
981
|
+
},
|
|
982
|
+
interfaces: {},
|
|
983
|
+
typeAliases: {},
|
|
984
|
+
enums: {},
|
|
985
|
+
exports: {},
|
|
986
|
+
},
|
|
987
|
+
};
|
|
988
|
+
|
|
989
|
+
const result = resolveResourceClassName(webhooksService, overlayCtx);
|
|
990
|
+
expect(result).toBe('Webhooks');
|
|
991
|
+
});
|
|
992
|
+
|
|
993
|
+
it('appends Endpoints suffix when IR name collides with overlay name', () => {
|
|
994
|
+
const collisionService: Service = {
|
|
995
|
+
name: 'Webhooks',
|
|
996
|
+
operations: [
|
|
997
|
+
{
|
|
998
|
+
name: 'listWebhooks',
|
|
999
|
+
httpMethod: 'get',
|
|
1000
|
+
path: '/webhooks',
|
|
1001
|
+
pathParams: [],
|
|
1002
|
+
queryParams: [],
|
|
1003
|
+
headerParams: [],
|
|
1004
|
+
response: { kind: 'model', name: 'Webhook' },
|
|
1005
|
+
errors: [],
|
|
1006
|
+
injectIdempotencyKey: false,
|
|
1007
|
+
},
|
|
1008
|
+
],
|
|
1009
|
+
};
|
|
1010
|
+
|
|
1011
|
+
const overlayCtx: EmitterContext = {
|
|
1012
|
+
namespace: 'workos',
|
|
1013
|
+
namespacePascal: 'WorkOS',
|
|
1014
|
+
spec: { ...emptySpec, services: [collisionService] },
|
|
1015
|
+
overlayLookup: {
|
|
1016
|
+
methodByOperation: new Map([
|
|
1017
|
+
['GET /webhooks', { className: 'Webhooks', methodName: 'listWebhooks', params: [], returnType: 'void' }],
|
|
1018
|
+
]),
|
|
1019
|
+
httpKeyByMethod: new Map(),
|
|
1020
|
+
interfaceByName: new Map(),
|
|
1021
|
+
typeAliasByName: new Map(),
|
|
1022
|
+
requiredExports: new Map(),
|
|
1023
|
+
modelNameByIR: new Map(),
|
|
1024
|
+
fileBySymbol: new Map(),
|
|
1025
|
+
},
|
|
1026
|
+
apiSurface: {
|
|
1027
|
+
language: 'node',
|
|
1028
|
+
extractedFrom: 'test',
|
|
1029
|
+
extractedAt: '2024-01-01',
|
|
1030
|
+
classes: {
|
|
1031
|
+
Webhooks: {
|
|
1032
|
+
name: 'Webhooks',
|
|
1033
|
+
methods: {},
|
|
1034
|
+
properties: {},
|
|
1035
|
+
constructorParams: [{ name: 'cryptoProvider', type: 'CryptoProvider', optional: false }],
|
|
1036
|
+
},
|
|
1037
|
+
},
|
|
1038
|
+
interfaces: {},
|
|
1039
|
+
typeAliases: {},
|
|
1040
|
+
enums: {},
|
|
1041
|
+
exports: {},
|
|
1042
|
+
},
|
|
1043
|
+
};
|
|
1044
|
+
|
|
1045
|
+
const result = resolveResourceClassName(collisionService, overlayCtx);
|
|
1046
|
+
// IR name "Webhooks" collides with overlay name "Webhooks", so append Endpoints
|
|
1047
|
+
expect(result).toBe('WebhooksEndpoints');
|
|
1048
|
+
});
|
|
1049
|
+
});
|
|
1050
|
+
|
|
1051
|
+
describe('hasCompatibleConstructor', () => {
|
|
1052
|
+
it('returns true when no baseline exists', () => {
|
|
1053
|
+
expect(hasCompatibleConstructor('NewService', ctx)).toBe(true);
|
|
1054
|
+
});
|
|
1055
|
+
|
|
1056
|
+
it('returns true when baseline has workos: WorkOS param', () => {
|
|
1057
|
+
const ctxWithSurface: EmitterContext = {
|
|
1058
|
+
...ctx,
|
|
1059
|
+
apiSurface: {
|
|
1060
|
+
language: 'node',
|
|
1061
|
+
extractedFrom: 'test',
|
|
1062
|
+
extractedAt: '2024-01-01',
|
|
1063
|
+
classes: {
|
|
1064
|
+
Organizations: {
|
|
1065
|
+
name: 'Organizations',
|
|
1066
|
+
methods: {},
|
|
1067
|
+
properties: {},
|
|
1068
|
+
constructorParams: [{ name: 'workos', type: 'WorkOS', optional: false }],
|
|
1069
|
+
},
|
|
1070
|
+
},
|
|
1071
|
+
interfaces: {},
|
|
1072
|
+
typeAliases: {},
|
|
1073
|
+
enums: {},
|
|
1074
|
+
exports: {},
|
|
1075
|
+
},
|
|
1076
|
+
};
|
|
1077
|
+
|
|
1078
|
+
expect(hasCompatibleConstructor('Organizations', ctxWithSurface)).toBe(true);
|
|
1079
|
+
});
|
|
1080
|
+
|
|
1081
|
+
it('returns false when baseline has incompatible constructor', () => {
|
|
1082
|
+
const ctxWithSurface: EmitterContext = {
|
|
1083
|
+
...ctx,
|
|
1084
|
+
apiSurface: {
|
|
1085
|
+
language: 'node',
|
|
1086
|
+
extractedFrom: 'test',
|
|
1087
|
+
extractedAt: '2024-01-01',
|
|
1088
|
+
classes: {
|
|
1089
|
+
Webhooks: {
|
|
1090
|
+
name: 'Webhooks',
|
|
1091
|
+
methods: {},
|
|
1092
|
+
properties: {},
|
|
1093
|
+
constructorParams: [{ name: 'cryptoProvider', type: 'CryptoProvider', optional: false }],
|
|
1094
|
+
},
|
|
1095
|
+
},
|
|
1096
|
+
interfaces: {},
|
|
1097
|
+
typeAliases: {},
|
|
1098
|
+
enums: {},
|
|
1099
|
+
exports: {},
|
|
1100
|
+
},
|
|
1101
|
+
};
|
|
1102
|
+
|
|
1103
|
+
expect(hasCompatibleConstructor('Webhooks', ctxWithSurface)).toBe(false);
|
|
1104
|
+
});
|
|
1105
|
+
|
|
1106
|
+
it('returns true when baseline has no constructor params', () => {
|
|
1107
|
+
const ctxWithSurface: EmitterContext = {
|
|
1108
|
+
...ctx,
|
|
1109
|
+
apiSurface: {
|
|
1110
|
+
language: 'node',
|
|
1111
|
+
extractedFrom: 'test',
|
|
1112
|
+
extractedAt: '2024-01-01',
|
|
1113
|
+
classes: {
|
|
1114
|
+
EmptyService: {
|
|
1115
|
+
name: 'EmptyService',
|
|
1116
|
+
methods: {},
|
|
1117
|
+
properties: {},
|
|
1118
|
+
constructorParams: [],
|
|
1119
|
+
},
|
|
1120
|
+
},
|
|
1121
|
+
interfaces: {},
|
|
1122
|
+
typeAliases: {},
|
|
1123
|
+
enums: {},
|
|
1124
|
+
exports: {},
|
|
1125
|
+
},
|
|
1126
|
+
};
|
|
1127
|
+
|
|
1128
|
+
expect(hasCompatibleConstructor('EmptyService', ctxWithSurface)).toBe(true);
|
|
1129
|
+
});
|
|
1130
|
+
});
|
|
1131
|
+
|
|
1132
|
+
describe('partial service coverage', () => {
|
|
1133
|
+
it('generates methods for uncovered operations in partially covered services', () => {
|
|
1134
|
+
const services: Service[] = [
|
|
1135
|
+
{
|
|
1136
|
+
name: 'AuditLogs',
|
|
1137
|
+
operations: [
|
|
1138
|
+
{
|
|
1139
|
+
name: 'createEvent',
|
|
1140
|
+
httpMethod: 'post',
|
|
1141
|
+
path: '/audit_logs/events',
|
|
1142
|
+
pathParams: [],
|
|
1143
|
+
queryParams: [],
|
|
1144
|
+
headerParams: [],
|
|
1145
|
+
response: { kind: 'model', name: 'AuditLogEvent' },
|
|
1146
|
+
errors: [],
|
|
1147
|
+
injectIdempotencyKey: false,
|
|
1148
|
+
},
|
|
1149
|
+
{
|
|
1150
|
+
name: 'getRetention',
|
|
1151
|
+
httpMethod: 'get',
|
|
1152
|
+
path: '/audit_logs/retention',
|
|
1153
|
+
pathParams: [],
|
|
1154
|
+
queryParams: [],
|
|
1155
|
+
headerParams: [],
|
|
1156
|
+
response: { kind: 'model', name: 'AuditLogRetention' },
|
|
1157
|
+
errors: [],
|
|
1158
|
+
injectIdempotencyKey: false,
|
|
1159
|
+
},
|
|
1160
|
+
],
|
|
1161
|
+
},
|
|
1162
|
+
];
|
|
1163
|
+
|
|
1164
|
+
// createEvent is covered by existing AuditLogs class, getRetention is NOT
|
|
1165
|
+
const ctxPartial: EmitterContext = {
|
|
1166
|
+
...ctx,
|
|
1167
|
+
spec: { ...emptySpec, services, models: [] },
|
|
1168
|
+
overlayLookup: {
|
|
1169
|
+
methodByOperation: new Map([
|
|
1170
|
+
[
|
|
1171
|
+
'POST /audit_logs/events',
|
|
1172
|
+
{
|
|
1173
|
+
className: 'AuditLogs',
|
|
1174
|
+
methodName: 'createEvent',
|
|
1175
|
+
params: [],
|
|
1176
|
+
returnType: 'AuditLogEvent',
|
|
1177
|
+
},
|
|
1178
|
+
],
|
|
1179
|
+
]),
|
|
1180
|
+
httpKeyByMethod: new Map(),
|
|
1181
|
+
interfaceByName: new Map(),
|
|
1182
|
+
typeAliasByName: new Map(),
|
|
1183
|
+
requiredExports: new Map(),
|
|
1184
|
+
modelNameByIR: new Map(),
|
|
1185
|
+
fileBySymbol: new Map(),
|
|
1186
|
+
},
|
|
1187
|
+
apiSurface: {
|
|
1188
|
+
language: 'node',
|
|
1189
|
+
extractedFrom: 'test',
|
|
1190
|
+
extractedAt: '2024-01-01',
|
|
1191
|
+
classes: {
|
|
1192
|
+
AuditLogs: {
|
|
1193
|
+
name: 'AuditLogs',
|
|
1194
|
+
methods: {
|
|
1195
|
+
createEvent: [{ name: 'createEvent', params: [], returnType: 'AuditLogEvent', async: true }],
|
|
1196
|
+
},
|
|
1197
|
+
properties: {},
|
|
1198
|
+
constructorParams: [{ name: 'workos', type: 'WorkOS', optional: false }],
|
|
1199
|
+
},
|
|
1200
|
+
},
|
|
1201
|
+
interfaces: {},
|
|
1202
|
+
typeAliases: {},
|
|
1203
|
+
enums: {},
|
|
1204
|
+
exports: {},
|
|
1205
|
+
},
|
|
1206
|
+
};
|
|
1207
|
+
|
|
1208
|
+
const files = generateResources(services, ctxPartial);
|
|
1209
|
+
expect(files.length).toBe(1);
|
|
1210
|
+
const content = files[0].content;
|
|
1211
|
+
|
|
1212
|
+
// Should generate method for uncovered operation
|
|
1213
|
+
expect(content).toContain('async getRetention');
|
|
1214
|
+
// Should NOT generate method for covered operation
|
|
1215
|
+
expect(content).not.toContain('async createEvent');
|
|
1216
|
+
});
|
|
260
1217
|
});
|