@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
package/test/node/enums.test.ts
CHANGED
package/test/node/errors.test.ts
CHANGED
|
@@ -1,26 +1,9 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
2
|
import { generateErrors } from '../../src/node/errors.js';
|
|
3
|
-
import type { EmitterContext, ApiSpec } from '@workos/oagen';
|
|
4
|
-
|
|
5
|
-
const emptySpec: ApiSpec = {
|
|
6
|
-
name: 'Test',
|
|
7
|
-
version: '1.0.0',
|
|
8
|
-
baseUrl: '',
|
|
9
|
-
services: [],
|
|
10
|
-
models: [],
|
|
11
|
-
enums: [],
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
const ctx: EmitterContext = {
|
|
15
|
-
namespace: 'workos',
|
|
16
|
-
namespacePascal: 'WorkOS',
|
|
17
|
-
spec: emptySpec,
|
|
18
|
-
irVersion: 6,
|
|
19
|
-
};
|
|
20
3
|
|
|
21
4
|
describe('generateErrors', () => {
|
|
22
5
|
it('generates all exception classes', () => {
|
|
23
|
-
const files = generateErrors(
|
|
6
|
+
const files = generateErrors();
|
|
24
7
|
|
|
25
8
|
const names = files.map((f) => f.path);
|
|
26
9
|
expect(names).toContain('src/common/exceptions/bad-request.exception.ts');
|
|
@@ -35,7 +18,7 @@ describe('generateErrors', () => {
|
|
|
35
18
|
});
|
|
36
19
|
|
|
37
20
|
it('generates NotFoundException with correct status', () => {
|
|
38
|
-
const files = generateErrors(
|
|
21
|
+
const files = generateErrors();
|
|
39
22
|
const notFoundFile = files.find((f) => f.path.includes('not-found.exception.ts'))!;
|
|
40
23
|
|
|
41
24
|
expect(notFoundFile.content).toContain('export class NotFoundException extends Error');
|
|
@@ -44,7 +27,7 @@ describe('generateErrors', () => {
|
|
|
44
27
|
});
|
|
45
28
|
|
|
46
29
|
it('generates RateLimitExceededException with retryAfter', () => {
|
|
47
|
-
const files = generateErrors(
|
|
30
|
+
const files = generateErrors();
|
|
48
31
|
const rateLimitFile = files.find((f) => f.path.includes('rate-limit-exceeded.exception.ts'))!;
|
|
49
32
|
|
|
50
33
|
expect(rateLimitFile.content).toContain('export class RateLimitExceededException extends Error');
|
|
@@ -53,7 +36,7 @@ describe('generateErrors', () => {
|
|
|
53
36
|
});
|
|
54
37
|
|
|
55
38
|
it('generates exception barrel with all exports', () => {
|
|
56
|
-
const files = generateErrors(
|
|
39
|
+
const files = generateErrors();
|
|
57
40
|
const barrel = files.find((f) => f.path === 'src/common/exceptions/index.ts')!;
|
|
58
41
|
|
|
59
42
|
expect(barrel.content).toContain('export { BadRequestException }');
|
package/test/node/models.test.ts
CHANGED
|
@@ -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('generateModels', () => {
|
|
@@ -85,7 +84,7 @@ describe('generateModels', () => {
|
|
|
85
84
|
expect(files[0].content).toContain('export interface Organization {');
|
|
86
85
|
expect(files[0].content).toContain(' id: string;');
|
|
87
86
|
expect(files[0].content).toContain(' name: string;');
|
|
88
|
-
expect(files[0].content).toContain(' createdAt:
|
|
87
|
+
expect(files[0].content).toContain(' createdAt: Date;');
|
|
89
88
|
expect(files[0].content).toContain(' externalId?: string | null;');
|
|
90
89
|
|
|
91
90
|
// Response interface has snake_case fields
|
|
@@ -298,4 +297,412 @@ describe('generateModels', () => {
|
|
|
298
297
|
// Field with only deprecated gets single-line JSDoc
|
|
299
298
|
expect(content).toContain(' /** @deprecated */');
|
|
300
299
|
});
|
|
300
|
+
|
|
301
|
+
it('renders field-level JSDoc from OpenAPI descriptions', () => {
|
|
302
|
+
const service: Service = {
|
|
303
|
+
name: 'Organizations',
|
|
304
|
+
operations: [
|
|
305
|
+
{
|
|
306
|
+
name: 'getOrganization',
|
|
307
|
+
httpMethod: 'get',
|
|
308
|
+
path: '/organizations/{id}',
|
|
309
|
+
pathParams: [{ name: 'id', type: { kind: 'primitive', type: 'string' }, required: true }],
|
|
310
|
+
queryParams: [],
|
|
311
|
+
headerParams: [],
|
|
312
|
+
response: { kind: 'model', name: 'Organization' },
|
|
313
|
+
errors: [],
|
|
314
|
+
injectIdempotencyKey: false,
|
|
315
|
+
},
|
|
316
|
+
],
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
const models: Model[] = [
|
|
320
|
+
{
|
|
321
|
+
name: 'Organization',
|
|
322
|
+
description: 'An organization in the WorkOS system.',
|
|
323
|
+
fields: [
|
|
324
|
+
{
|
|
325
|
+
name: 'id',
|
|
326
|
+
type: { kind: 'primitive', type: 'string' },
|
|
327
|
+
required: true,
|
|
328
|
+
description: 'Unique identifier for the organization.',
|
|
329
|
+
},
|
|
330
|
+
{
|
|
331
|
+
name: 'name',
|
|
332
|
+
type: { kind: 'primitive', type: 'string' },
|
|
333
|
+
required: true,
|
|
334
|
+
description: 'The display name of the organization.',
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
name: 'created_at',
|
|
338
|
+
type: { kind: 'primitive', type: 'string', format: 'date-time' },
|
|
339
|
+
required: true,
|
|
340
|
+
// No description — should not get JSDoc
|
|
341
|
+
},
|
|
342
|
+
{
|
|
343
|
+
name: 'allow_profiles_outside_organization',
|
|
344
|
+
type: { kind: 'primitive', type: 'boolean' },
|
|
345
|
+
required: false,
|
|
346
|
+
description:
|
|
347
|
+
'Whether connections within the organization allow profiles\nthat do not have a domain that is verified by the organization.',
|
|
348
|
+
},
|
|
349
|
+
],
|
|
350
|
+
},
|
|
351
|
+
];
|
|
352
|
+
|
|
353
|
+
const ctxWithServices: EmitterContext = {
|
|
354
|
+
...ctx,
|
|
355
|
+
spec: { ...emptySpec, services: [service], models },
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
const files = generateModels(models, ctxWithServices);
|
|
359
|
+
const content = files[0].content;
|
|
360
|
+
|
|
361
|
+
// Model-level JSDoc is emitted
|
|
362
|
+
expect(content).toContain('/** An organization in the WorkOS system. */');
|
|
363
|
+
|
|
364
|
+
// Fields with description get per-field JSDoc
|
|
365
|
+
expect(content).toContain('/** Unique identifier for the organization. */');
|
|
366
|
+
expect(content).toContain('/** The display name of the organization. */');
|
|
367
|
+
|
|
368
|
+
// Multiline description renders correctly
|
|
369
|
+
expect(content).toContain(
|
|
370
|
+
' /**\n * Whether connections within the organization allow profiles\n * that do not have a domain that is verified by the organization.\n */',
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
// Field without description does NOT get JSDoc
|
|
374
|
+
const lines = content.split('\n');
|
|
375
|
+
const createdAtIdx = lines.findIndex((l) => l.includes('createdAt'));
|
|
376
|
+
expect(createdAtIdx).toBeGreaterThan(0);
|
|
377
|
+
// The line before createdAt should not be a JSDoc closing tag
|
|
378
|
+
expect(lines[createdAtIdx - 1].trim()).not.toBe('*/');
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
it('renders readOnly/writeOnly/default annotations', () => {
|
|
382
|
+
const service: Service = {
|
|
383
|
+
name: 'Organizations',
|
|
384
|
+
operations: [
|
|
385
|
+
{
|
|
386
|
+
name: 'getOrganization',
|
|
387
|
+
httpMethod: 'get',
|
|
388
|
+
path: '/organizations/{id}',
|
|
389
|
+
pathParams: [{ name: 'id', type: { kind: 'primitive', type: 'string' }, required: true }],
|
|
390
|
+
queryParams: [],
|
|
391
|
+
headerParams: [],
|
|
392
|
+
response: { kind: 'model', name: 'Organization' },
|
|
393
|
+
errors: [],
|
|
394
|
+
injectIdempotencyKey: false,
|
|
395
|
+
},
|
|
396
|
+
],
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
const models: Model[] = [
|
|
400
|
+
{
|
|
401
|
+
name: 'Organization',
|
|
402
|
+
fields: [
|
|
403
|
+
{
|
|
404
|
+
name: 'id',
|
|
405
|
+
type: { kind: 'primitive', type: 'string' },
|
|
406
|
+
required: true,
|
|
407
|
+
readOnly: true,
|
|
408
|
+
},
|
|
409
|
+
{
|
|
410
|
+
name: 'secret_key',
|
|
411
|
+
type: { kind: 'primitive', type: 'string' },
|
|
412
|
+
required: true,
|
|
413
|
+
writeOnly: true,
|
|
414
|
+
},
|
|
415
|
+
{
|
|
416
|
+
name: 'status',
|
|
417
|
+
type: { kind: 'primitive', type: 'string' },
|
|
418
|
+
required: false,
|
|
419
|
+
default: 'active',
|
|
420
|
+
},
|
|
421
|
+
],
|
|
422
|
+
},
|
|
423
|
+
];
|
|
424
|
+
|
|
425
|
+
const ctxWithServices: EmitterContext = {
|
|
426
|
+
...ctx,
|
|
427
|
+
spec: { ...emptySpec, services: [service], models },
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
const files = generateModels(models, ctxWithServices);
|
|
431
|
+
const content = files[0].content;
|
|
432
|
+
|
|
433
|
+
// readOnly field gets @readonly JSDoc and readonly TS modifier
|
|
434
|
+
expect(content).toContain('/** @readonly */');
|
|
435
|
+
expect(content).toContain(' readonly id: string;');
|
|
436
|
+
|
|
437
|
+
// writeOnly field gets @writeonly JSDoc
|
|
438
|
+
expect(content).toContain('/** @writeonly */');
|
|
439
|
+
|
|
440
|
+
// default field gets @default JSDoc
|
|
441
|
+
expect(content).toContain('@default "active"');
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
it('skips per-domain ListMetadata models (Fix #4)', () => {
|
|
445
|
+
const service: Service = {
|
|
446
|
+
name: 'Connections',
|
|
447
|
+
operations: [
|
|
448
|
+
{
|
|
449
|
+
name: 'listConnections',
|
|
450
|
+
httpMethod: 'get',
|
|
451
|
+
path: '/connections',
|
|
452
|
+
pathParams: [],
|
|
453
|
+
queryParams: [],
|
|
454
|
+
headerParams: [],
|
|
455
|
+
response: { kind: 'model', name: 'ConnectionList' },
|
|
456
|
+
errors: [],
|
|
457
|
+
injectIdempotencyKey: false,
|
|
458
|
+
pagination: {
|
|
459
|
+
strategy: 'cursor',
|
|
460
|
+
param: 'after',
|
|
461
|
+
itemType: { kind: 'model', name: 'Connection' },
|
|
462
|
+
},
|
|
463
|
+
},
|
|
464
|
+
],
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
const models: Model[] = [
|
|
468
|
+
{
|
|
469
|
+
name: 'ConnectionListListMetadata',
|
|
470
|
+
fields: [
|
|
471
|
+
{
|
|
472
|
+
name: 'before',
|
|
473
|
+
type: { kind: 'nullable', inner: { kind: 'primitive', type: 'string' } },
|
|
474
|
+
required: false,
|
|
475
|
+
},
|
|
476
|
+
{
|
|
477
|
+
name: 'after',
|
|
478
|
+
type: { kind: 'nullable', inner: { kind: 'primitive', type: 'string' } },
|
|
479
|
+
required: false,
|
|
480
|
+
},
|
|
481
|
+
],
|
|
482
|
+
},
|
|
483
|
+
{
|
|
484
|
+
name: 'Connection',
|
|
485
|
+
fields: [
|
|
486
|
+
{
|
|
487
|
+
name: 'id',
|
|
488
|
+
type: { kind: 'primitive', type: 'string' },
|
|
489
|
+
required: true,
|
|
490
|
+
},
|
|
491
|
+
],
|
|
492
|
+
},
|
|
493
|
+
];
|
|
494
|
+
|
|
495
|
+
const ctxWithServices: EmitterContext = {
|
|
496
|
+
...ctx,
|
|
497
|
+
spec: { ...emptySpec, services: [service], models },
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
const files = generateModels(models, ctxWithServices);
|
|
501
|
+
|
|
502
|
+
// The ListMetadata model should be skipped entirely
|
|
503
|
+
const listMetadataFile = files.find((f) => f.path.includes('list-metadata'));
|
|
504
|
+
expect(listMetadataFile).toBeUndefined();
|
|
505
|
+
|
|
506
|
+
// The Connection model should still be generated
|
|
507
|
+
const connectionFile = files.find((f) => f.path.includes('connection.interface.ts'));
|
|
508
|
+
expect(connectionFile).toBeDefined();
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
it('skips per-domain list wrapper models (Fix #6)', () => {
|
|
512
|
+
const service: Service = {
|
|
513
|
+
name: 'Connections',
|
|
514
|
+
operations: [
|
|
515
|
+
{
|
|
516
|
+
name: 'listConnections',
|
|
517
|
+
httpMethod: 'get',
|
|
518
|
+
path: '/connections',
|
|
519
|
+
pathParams: [],
|
|
520
|
+
queryParams: [],
|
|
521
|
+
headerParams: [],
|
|
522
|
+
response: { kind: 'model', name: 'ConnectionList' },
|
|
523
|
+
errors: [],
|
|
524
|
+
injectIdempotencyKey: false,
|
|
525
|
+
pagination: {
|
|
526
|
+
strategy: 'cursor',
|
|
527
|
+
param: 'after',
|
|
528
|
+
itemType: { kind: 'model', name: 'Connection' },
|
|
529
|
+
},
|
|
530
|
+
},
|
|
531
|
+
],
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
const models: Model[] = [
|
|
535
|
+
{
|
|
536
|
+
name: 'ConnectionList',
|
|
537
|
+
fields: [
|
|
538
|
+
{
|
|
539
|
+
name: 'object',
|
|
540
|
+
type: { kind: 'literal', value: 'list' },
|
|
541
|
+
required: true,
|
|
542
|
+
},
|
|
543
|
+
{
|
|
544
|
+
name: 'data',
|
|
545
|
+
type: { kind: 'array', items: { kind: 'model', name: 'Connection' } },
|
|
546
|
+
required: true,
|
|
547
|
+
},
|
|
548
|
+
{
|
|
549
|
+
name: 'list_metadata',
|
|
550
|
+
type: { kind: 'model', name: 'ConnectionListListMetadata' },
|
|
551
|
+
required: true,
|
|
552
|
+
},
|
|
553
|
+
],
|
|
554
|
+
},
|
|
555
|
+
{
|
|
556
|
+
name: 'ConnectionListListMetadata',
|
|
557
|
+
fields: [
|
|
558
|
+
{
|
|
559
|
+
name: 'before',
|
|
560
|
+
type: { kind: 'nullable', inner: { kind: 'primitive', type: 'string' } },
|
|
561
|
+
required: false,
|
|
562
|
+
},
|
|
563
|
+
{
|
|
564
|
+
name: 'after',
|
|
565
|
+
type: { kind: 'nullable', inner: { kind: 'primitive', type: 'string' } },
|
|
566
|
+
required: false,
|
|
567
|
+
},
|
|
568
|
+
],
|
|
569
|
+
},
|
|
570
|
+
{
|
|
571
|
+
name: 'Connection',
|
|
572
|
+
fields: [
|
|
573
|
+
{
|
|
574
|
+
name: 'id',
|
|
575
|
+
type: { kind: 'primitive', type: 'string' },
|
|
576
|
+
required: true,
|
|
577
|
+
},
|
|
578
|
+
],
|
|
579
|
+
},
|
|
580
|
+
];
|
|
581
|
+
|
|
582
|
+
const ctxWithServices: EmitterContext = {
|
|
583
|
+
...ctx,
|
|
584
|
+
spec: { ...emptySpec, services: [service], models },
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
const files = generateModels(models, ctxWithServices);
|
|
588
|
+
|
|
589
|
+
// The list wrapper model should be skipped
|
|
590
|
+
const listFile = files.find((f) => f.path.includes('connection-list.interface.ts'));
|
|
591
|
+
expect(listFile).toBeUndefined();
|
|
592
|
+
|
|
593
|
+
// The ListMetadata model should also be skipped
|
|
594
|
+
const listMetadataFile = files.find((f) => f.path.includes('list-metadata'));
|
|
595
|
+
expect(listMetadataFile).toBeUndefined();
|
|
596
|
+
|
|
597
|
+
// The Connection model should still be generated
|
|
598
|
+
const connectionFile = files.find((f) => f.path.includes('connection.interface.ts'));
|
|
599
|
+
expect(connectionFile).toBeDefined();
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
it('does not skip models that only partially match list-metadata shape', () => {
|
|
603
|
+
const service: Service = {
|
|
604
|
+
name: 'Organizations',
|
|
605
|
+
operations: [
|
|
606
|
+
{
|
|
607
|
+
name: 'getOrganization',
|
|
608
|
+
httpMethod: 'get',
|
|
609
|
+
path: '/organizations/{id}',
|
|
610
|
+
pathParams: [{ name: 'id', type: { kind: 'primitive', type: 'string' }, required: true }],
|
|
611
|
+
queryParams: [],
|
|
612
|
+
headerParams: [],
|
|
613
|
+
response: { kind: 'model', name: 'Pagination' },
|
|
614
|
+
errors: [],
|
|
615
|
+
injectIdempotencyKey: false,
|
|
616
|
+
},
|
|
617
|
+
],
|
|
618
|
+
};
|
|
619
|
+
|
|
620
|
+
const models: Model[] = [
|
|
621
|
+
{
|
|
622
|
+
name: 'Pagination',
|
|
623
|
+
fields: [
|
|
624
|
+
{
|
|
625
|
+
name: 'before',
|
|
626
|
+
type: { kind: 'nullable', inner: { kind: 'primitive', type: 'string' } },
|
|
627
|
+
required: false,
|
|
628
|
+
},
|
|
629
|
+
{
|
|
630
|
+
name: 'after',
|
|
631
|
+
type: { kind: 'nullable', inner: { kind: 'primitive', type: 'string' } },
|
|
632
|
+
required: false,
|
|
633
|
+
},
|
|
634
|
+
{
|
|
635
|
+
name: 'total',
|
|
636
|
+
type: { kind: 'primitive', type: 'integer' },
|
|
637
|
+
required: true,
|
|
638
|
+
},
|
|
639
|
+
],
|
|
640
|
+
},
|
|
641
|
+
];
|
|
642
|
+
|
|
643
|
+
const ctxWithServices: EmitterContext = {
|
|
644
|
+
...ctx,
|
|
645
|
+
spec: { ...emptySpec, services: [service], models },
|
|
646
|
+
};
|
|
647
|
+
|
|
648
|
+
const files = generateModels(models, ctxWithServices);
|
|
649
|
+
// Model with 3 fields should NOT be skipped even if it has before/after
|
|
650
|
+
expect(files.length).toBe(1);
|
|
651
|
+
expect(files[0].path).toContain('pagination.interface.ts');
|
|
652
|
+
});
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
describe('model deduplication', () => {
|
|
656
|
+
it('emits type alias for structurally identical models', () => {
|
|
657
|
+
const service: Service = {
|
|
658
|
+
name: 'Roles',
|
|
659
|
+
operations: [
|
|
660
|
+
{
|
|
661
|
+
name: 'getRole',
|
|
662
|
+
httpMethod: 'get',
|
|
663
|
+
path: '/roles/{id}',
|
|
664
|
+
pathParams: [{ name: 'id', type: { kind: 'primitive', type: 'string' }, required: true }],
|
|
665
|
+
queryParams: [],
|
|
666
|
+
headerParams: [],
|
|
667
|
+
response: { kind: 'model', name: 'EnvironmentRole' },
|
|
668
|
+
errors: [],
|
|
669
|
+
injectIdempotencyKey: false,
|
|
670
|
+
},
|
|
671
|
+
],
|
|
672
|
+
};
|
|
673
|
+
|
|
674
|
+
const models: Model[] = [
|
|
675
|
+
{
|
|
676
|
+
name: 'EnvironmentRole',
|
|
677
|
+
fields: [
|
|
678
|
+
{ name: 'id', type: { kind: 'primitive', type: 'string' }, required: true },
|
|
679
|
+
{ name: 'name', type: { kind: 'primitive', type: 'string' }, required: true },
|
|
680
|
+
{ name: 'type', type: { kind: 'literal', value: 'environment_role' }, required: true },
|
|
681
|
+
],
|
|
682
|
+
},
|
|
683
|
+
{
|
|
684
|
+
name: 'OrganizationRole',
|
|
685
|
+
fields: [
|
|
686
|
+
{ name: 'id', type: { kind: 'primitive', type: 'string' }, required: true },
|
|
687
|
+
{ name: 'name', type: { kind: 'primitive', type: 'string' }, required: true },
|
|
688
|
+
{ name: 'type', type: { kind: 'literal', value: 'environment_role' }, required: true },
|
|
689
|
+
],
|
|
690
|
+
},
|
|
691
|
+
];
|
|
692
|
+
|
|
693
|
+
const ctxWithServices: EmitterContext = {
|
|
694
|
+
...ctx,
|
|
695
|
+
spec: { ...emptySpec, services: [service], models },
|
|
696
|
+
};
|
|
697
|
+
|
|
698
|
+
const files = generateModels(models, ctxWithServices);
|
|
699
|
+
expect(files.length).toBe(2);
|
|
700
|
+
|
|
701
|
+
// First model: full interface
|
|
702
|
+
expect(files[0].content).toContain('export interface EnvironmentRole');
|
|
703
|
+
|
|
704
|
+
// Second model: type alias referencing canonical
|
|
705
|
+
expect(files[1].content).toContain('export type OrganizationRole = EnvironmentRole');
|
|
706
|
+
expect(files[1].content).toContain('export type OrganizationRoleResponse = EnvironmentRoleResponse');
|
|
707
|
+
});
|
|
301
708
|
});
|
package/test/node/naming.test.ts
CHANGED
|
@@ -112,7 +112,6 @@ describe('naming', () => {
|
|
|
112
112
|
namespace: 'workos',
|
|
113
113
|
namespacePascal: 'WorkOS',
|
|
114
114
|
spec: emptySpec,
|
|
115
|
-
irVersion: 6,
|
|
116
115
|
overlayLookup: {
|
|
117
116
|
methodByOperation: new Map([
|
|
118
117
|
[
|
|
@@ -142,7 +141,6 @@ describe('naming', () => {
|
|
|
142
141
|
namespace: 'workos',
|
|
143
142
|
namespacePascal: 'WorkOS',
|
|
144
143
|
spec: emptySpec,
|
|
145
|
-
irVersion: 6,
|
|
146
144
|
};
|
|
147
145
|
|
|
148
146
|
expect(resolveServiceName(service, ctx)).toBe('MultiFactorAuth');
|
|
@@ -187,7 +185,6 @@ describe('naming', () => {
|
|
|
187
185
|
namespace: 'workos',
|
|
188
186
|
namespacePascal: 'WorkOS',
|
|
189
187
|
spec: emptySpec,
|
|
190
|
-
irVersion: 6,
|
|
191
188
|
overlayLookup: {
|
|
192
189
|
methodByOperation: new Map([
|
|
193
190
|
[
|