@workos/oagen-emitters 0.2.1 → 0.4.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.
Files changed (136) hide show
  1. package/.husky/pre-commit +1 -0
  2. package/.release-please-manifest.json +1 -1
  3. package/CHANGELOG.md +15 -0
  4. package/README.md +129 -0
  5. package/dist/index.d.mts +13 -1
  6. package/dist/index.d.mts.map +1 -1
  7. package/dist/index.mjs +14549 -3385
  8. package/dist/index.mjs.map +1 -1
  9. package/docs/sdk-architecture/dotnet.md +336 -0
  10. package/docs/sdk-architecture/go.md +338 -0
  11. package/docs/sdk-architecture/php.md +315 -0
  12. package/docs/sdk-architecture/python.md +511 -0
  13. package/oagen.config.ts +328 -2
  14. package/package.json +9 -5
  15. package/scripts/generate-php.js +13 -0
  16. package/scripts/git-push-with-published-oagen.sh +21 -0
  17. package/smoke/sdk-dotnet.ts +45 -12
  18. package/smoke/sdk-go.ts +116 -42
  19. package/smoke/sdk-php.ts +28 -26
  20. package/smoke/sdk-python.ts +5 -2
  21. package/src/dotnet/client.ts +89 -0
  22. package/src/dotnet/enums.ts +323 -0
  23. package/src/dotnet/fixtures.ts +236 -0
  24. package/src/dotnet/index.ts +246 -0
  25. package/src/dotnet/manifest.ts +36 -0
  26. package/src/dotnet/models.ts +344 -0
  27. package/src/dotnet/naming.ts +330 -0
  28. package/src/dotnet/resources.ts +622 -0
  29. package/src/dotnet/tests.ts +693 -0
  30. package/src/dotnet/type-map.ts +201 -0
  31. package/src/dotnet/wrappers.ts +186 -0
  32. package/src/go/client.ts +141 -0
  33. package/src/go/enums.ts +196 -0
  34. package/src/go/fixtures.ts +212 -0
  35. package/src/go/index.ts +84 -0
  36. package/src/go/manifest.ts +36 -0
  37. package/src/go/models.ts +254 -0
  38. package/src/go/naming.ts +179 -0
  39. package/src/go/resources.ts +827 -0
  40. package/src/go/tests.ts +751 -0
  41. package/src/go/type-map.ts +82 -0
  42. package/src/go/wrappers.ts +261 -0
  43. package/src/index.ts +4 -0
  44. package/src/kotlin/client.ts +53 -0
  45. package/src/kotlin/enums.ts +162 -0
  46. package/src/kotlin/index.ts +92 -0
  47. package/src/kotlin/manifest.ts +55 -0
  48. package/src/kotlin/models.ts +395 -0
  49. package/src/kotlin/naming.ts +223 -0
  50. package/src/kotlin/overrides.ts +25 -0
  51. package/src/kotlin/resources.ts +667 -0
  52. package/src/kotlin/tests.ts +1019 -0
  53. package/src/kotlin/type-map.ts +123 -0
  54. package/src/kotlin/wrappers.ts +168 -0
  55. package/src/node/client.ts +128 -115
  56. package/src/node/enums.ts +9 -0
  57. package/src/node/errors.ts +37 -232
  58. package/src/node/field-plan.ts +726 -0
  59. package/src/node/fixtures.ts +9 -1
  60. package/src/node/index.ts +3 -9
  61. package/src/node/models.ts +178 -21
  62. package/src/node/naming.ts +49 -111
  63. package/src/node/resources.ts +527 -397
  64. package/src/node/sdk-errors.ts +41 -0
  65. package/src/node/tests.ts +69 -19
  66. package/src/node/type-map.ts +4 -2
  67. package/src/node/utils.ts +13 -71
  68. package/src/node/wrappers.ts +151 -0
  69. package/src/php/client.ts +179 -0
  70. package/src/php/enums.ts +67 -0
  71. package/src/php/errors.ts +9 -0
  72. package/src/php/fixtures.ts +181 -0
  73. package/src/php/index.ts +96 -0
  74. package/src/php/manifest.ts +36 -0
  75. package/src/php/models.ts +310 -0
  76. package/src/php/naming.ts +279 -0
  77. package/src/php/resources.ts +636 -0
  78. package/src/php/tests.ts +609 -0
  79. package/src/php/type-map.ts +90 -0
  80. package/src/php/utils.ts +18 -0
  81. package/src/php/wrappers.ts +152 -0
  82. package/src/python/client.ts +345 -0
  83. package/src/python/enums.ts +313 -0
  84. package/src/python/fixtures.ts +196 -0
  85. package/src/python/index.ts +95 -0
  86. package/src/python/manifest.ts +38 -0
  87. package/src/python/models.ts +688 -0
  88. package/src/python/naming.ts +189 -0
  89. package/src/python/resources.ts +1322 -0
  90. package/src/python/tests.ts +1335 -0
  91. package/src/python/type-map.ts +93 -0
  92. package/src/python/wrappers.ts +191 -0
  93. package/src/shared/model-utils.ts +472 -0
  94. package/src/shared/naming-utils.ts +154 -0
  95. package/src/shared/non-spec-services.ts +54 -0
  96. package/src/shared/resolved-ops.ts +109 -0
  97. package/src/shared/wrapper-utils.ts +70 -0
  98. package/test/dotnet/client.test.ts +121 -0
  99. package/test/dotnet/enums.test.ts +193 -0
  100. package/test/dotnet/errors.test.ts +9 -0
  101. package/test/dotnet/manifest.test.ts +82 -0
  102. package/test/dotnet/models.test.ts +260 -0
  103. package/test/dotnet/resources.test.ts +255 -0
  104. package/test/dotnet/tests.test.ts +202 -0
  105. package/test/go/client.test.ts +92 -0
  106. package/test/go/enums.test.ts +132 -0
  107. package/test/go/errors.test.ts +9 -0
  108. package/test/go/models.test.ts +265 -0
  109. package/test/go/resources.test.ts +408 -0
  110. package/test/go/tests.test.ts +143 -0
  111. package/test/kotlin/models.test.ts +135 -0
  112. package/test/kotlin/tests.test.ts +176 -0
  113. package/test/node/client.test.ts +92 -12
  114. package/test/node/enums.test.ts +2 -0
  115. package/test/node/errors.test.ts +2 -41
  116. package/test/node/models.test.ts +2 -0
  117. package/test/node/naming.test.ts +23 -0
  118. package/test/node/resources.test.ts +315 -84
  119. package/test/node/serializers.test.ts +3 -1
  120. package/test/node/type-map.test.ts +11 -0
  121. package/test/php/client.test.ts +95 -0
  122. package/test/php/enums.test.ts +173 -0
  123. package/test/php/errors.test.ts +9 -0
  124. package/test/php/models.test.ts +497 -0
  125. package/test/php/resources.test.ts +682 -0
  126. package/test/php/tests.test.ts +185 -0
  127. package/test/python/client.test.ts +200 -0
  128. package/test/python/enums.test.ts +228 -0
  129. package/test/python/errors.test.ts +16 -0
  130. package/test/python/manifest.test.ts +74 -0
  131. package/test/python/models.test.ts +716 -0
  132. package/test/python/resources.test.ts +617 -0
  133. package/test/python/tests.test.ts +202 -0
  134. package/src/node/common.ts +0 -273
  135. package/src/node/config.ts +0 -71
  136. package/src/node/serializers.ts +0 -746
@@ -0,0 +1,176 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { generateTests } from '../../src/kotlin/tests.js';
3
+ import { generateEnums } from '../../src/kotlin/enums.js';
4
+ import type { EmitterContext, ApiSpec, Service, Model, ResolvedOperation } from '@workos/oagen';
5
+ import { defaultSdkBehavior } from '@workos/oagen';
6
+
7
+ const models: Model[] = [
8
+ {
9
+ name: 'Organization',
10
+ fields: [
11
+ { name: 'id', type: { kind: 'primitive', type: 'string' }, required: true },
12
+ { name: 'name', type: { kind: 'primitive', type: 'string' }, required: true },
13
+ ],
14
+ },
15
+ ];
16
+
17
+ const services: Service[] = [
18
+ {
19
+ name: 'Organizations',
20
+ operations: [
21
+ {
22
+ name: 'getOrganization',
23
+ httpMethod: 'get',
24
+ path: '/organizations/{id}',
25
+ pathParams: [{ name: 'id', type: { kind: 'primitive', type: 'string' }, required: true }],
26
+ queryParams: [],
27
+ headerParams: [],
28
+ response: { kind: 'model', name: 'Organization' },
29
+ errors: [],
30
+ injectIdempotencyKey: false,
31
+ },
32
+ {
33
+ name: 'deleteOrganization',
34
+ httpMethod: 'delete',
35
+ path: '/organizations/{id}',
36
+ pathParams: [{ name: 'id', type: { kind: 'primitive', type: 'string' }, required: true }],
37
+ queryParams: [],
38
+ headerParams: [],
39
+ response: { kind: 'primitive', type: 'unknown' },
40
+ errors: [],
41
+ injectIdempotencyKey: false,
42
+ },
43
+ ],
44
+ },
45
+ ];
46
+
47
+ const spec: ApiSpec = {
48
+ name: 'TestAPI',
49
+ version: '1.0.0',
50
+ baseUrl: 'https://api.workos.com',
51
+ services,
52
+ models,
53
+ enums: [],
54
+ sdk: defaultSdkBehavior(),
55
+ };
56
+
57
+ function buildResolvedOps(services: Service[]): ResolvedOperation[] {
58
+ return services.flatMap((svc) =>
59
+ svc.operations.map((op) => ({
60
+ service: svc,
61
+ operation: op,
62
+ methodName: op.name,
63
+ mountOn: svc.name,
64
+ })),
65
+ ) as ResolvedOperation[];
66
+ }
67
+
68
+ const ctx: EmitterContext = {
69
+ namespace: 'workos',
70
+ namespacePascal: 'WorkOS',
71
+ spec,
72
+ resolvedOperations: buildResolvedOps(services),
73
+ };
74
+
75
+ describe('kotlin/tests', () => {
76
+ it('generates per-mount-group test files', () => {
77
+ generateEnums([], ctx);
78
+ const files = generateTests(spec, ctx);
79
+ const testFile = files.find((f) => f.path.includes('OrganizationsTest.kt'));
80
+ expect(testFile).toBeDefined();
81
+
82
+ const content = testFile!.content;
83
+ expect(content).toContain('class OrganizationsTest');
84
+ expect(content).toContain('TestBase');
85
+ expect(content).toContain('@Test');
86
+ });
87
+
88
+ it('generates happy-path test for void/delete methods', () => {
89
+ generateEnums([], ctx);
90
+ const files = generateTests(spec, ctx);
91
+ const testFile = files.find((f) => f.path.includes('OrganizationsTest.kt'))!;
92
+ const content = testFile.content;
93
+
94
+ // Delete method should have an active test, not a @Disabled placeholder.
95
+ // Method name is trimmed from deleteOrganization -> delete by resolveMethodName.
96
+ expect(content).toContain('delete completes without throwing');
97
+ expect(content).not.toContain('@Disabled("generator: could not synthesize required arguments for delete")');
98
+ });
99
+
100
+ it('generates field-value assertions for non-void responses', () => {
101
+ generateEnums([], ctx);
102
+ const files = generateTests(spec, ctx);
103
+ const testFile = files.find((f) => f.path.includes('OrganizationsTest.kt'))!;
104
+ const content = testFile.content;
105
+
106
+ // GET method returning Organization should assert field values
107
+ expect(content).toContain('assertEquals("sample", result.id)');
108
+ expect(content).toContain('assertEquals("sample", result.name)');
109
+ });
110
+
111
+ it('generates error-mapping tests', () => {
112
+ generateEnums([], ctx);
113
+ const files = generateTests(spec, ctx);
114
+ const testFile = files.find((f) => f.path.includes('OrganizationsTest.kt'))!;
115
+ const content = testFile.content;
116
+
117
+ expect(content).toContain('UnauthorizedException');
118
+ expect(content).toContain('NotFoundException');
119
+ expect(content).toContain('RateLimitException');
120
+ expect(content).toContain('GenericServerException');
121
+ });
122
+
123
+ it('generates round-trip test using synthJson for broader coverage', () => {
124
+ generateEnums([], ctx);
125
+ const files = generateTests(spec, ctx);
126
+ const roundTrip = files.find((f) => f.path.includes('GeneratedModelRoundTripTest.kt'));
127
+ expect(roundTrip).toBeDefined();
128
+
129
+ const content = roundTrip!.content;
130
+ expect(content).toContain('Organization round-trips through Jackson');
131
+ expect(content).toContain('assertEquals(tree1, tree2)');
132
+ });
133
+
134
+ it('generates forward-compat test with OffsetDateTime round-trip', () => {
135
+ generateEnums([], ctx);
136
+ const files = generateTests(spec, ctx);
137
+ const fwdCompat = files.find((f) => f.path.includes('GeneratedForwardCompatTest.kt'));
138
+ expect(fwdCompat).toBeDefined();
139
+
140
+ const content = fwdCompat!.content;
141
+ expect(content).toContain('OffsetDateTime round-trips');
142
+ expect(content).toContain('assertEquals(parsed.toInstant(), reparsed.toInstant())');
143
+ });
144
+
145
+ it('emits valid ISO-8601 for date-time fields in round-trip fixtures', () => {
146
+ const dtModels: Model[] = [
147
+ {
148
+ name: 'Event',
149
+ fields: [
150
+ { name: 'id', type: { kind: 'primitive', type: 'string' }, required: true },
151
+ {
152
+ name: 'created_at',
153
+ type: { kind: 'primitive', type: 'string', format: 'date-time' },
154
+ required: true,
155
+ },
156
+ ],
157
+ },
158
+ ];
159
+ const dtSpec: ApiSpec = { ...spec, models: dtModels };
160
+ const dtCtx: EmitterContext = {
161
+ ...ctx,
162
+ spec: dtSpec,
163
+ resolvedOperations: buildResolvedOps(services),
164
+ };
165
+
166
+ generateEnums([], dtCtx);
167
+ const files = generateTests(dtSpec, dtCtx);
168
+ const roundTrip = files.find((f) => f.path.includes('GeneratedModelRoundTripTest.kt'));
169
+ expect(roundTrip).toBeDefined();
170
+
171
+ const content = roundTrip!.content;
172
+ // Should use ISO-8601 timestamp, not "sample"
173
+ expect(content).toContain('2024-01-01T00:00:00Z');
174
+ expect(content).not.toMatch(/created_at.*"sample"/);
175
+ });
176
+ });
@@ -2,6 +2,7 @@ import { describe, it, expect } from 'vitest';
2
2
  import { generateClient } from '../../src/node/client.js';
3
3
  import { isServiceCoveredByExisting } from '../../src/node/utils.js';
4
4
  import type { EmitterContext, ApiSpec, Service, Model, Enum } from '@workos/oagen';
5
+ import { defaultSdkBehavior } from '@workos/oagen';
5
6
  import type { ApiSurface } from '@workos/oagen/compat';
6
7
 
7
8
  const service: Service = {
@@ -46,6 +47,7 @@ const spec: ApiSpec = {
46
47
  services: [service],
47
48
  models: [model],
48
49
  enums: [],
50
+ sdk: defaultSdkBehavior(),
49
51
  };
50
52
 
51
53
  const ctx: EmitterContext = {
@@ -98,16 +100,11 @@ describe('generateClient', () => {
98
100
  expect(serviceBarrel!.skipIfExists).toBe(true);
99
101
  });
100
102
 
101
- it('generates package.json and tsconfig.json', () => {
103
+ it('does not generate package.json, tsconfig.json, or worker barrel (now hand-maintained)', () => {
102
104
  const files = generateClient(spec, ctx);
103
- const pkg = files.find((f) => f.path === 'package.json');
104
- const tsconfig = files.find((f) => f.path === 'tsconfig.json');
105
-
106
- expect(pkg).toBeDefined();
107
- expect(pkg!.skipIfExists).toBe(true);
108
-
109
- expect(tsconfig).toBeDefined();
110
- expect(tsconfig!.skipIfExists).toBe(true);
105
+ expect(files.find((f) => f.path === 'package.json')).toBeUndefined();
106
+ expect(files.find((f) => f.path === 'tsconfig.json')).toBeUndefined();
107
+ expect(files.find((f) => f.path === 'src/index.worker.ts')).toBeUndefined();
111
108
  });
112
109
 
113
110
  it('uses overlay-resolved names for imports and accessors', () => {
@@ -146,6 +143,7 @@ describe('generateClient', () => {
146
143
  services: [mfaService],
147
144
  models: [mfaModel],
148
145
  enums: [],
146
+ sdk: defaultSdkBehavior(),
149
147
  };
150
148
 
151
149
  const overlayCtx: EmitterContext = {
@@ -196,6 +194,80 @@ describe('generateClient', () => {
196
194
  expect(serviceBarrel!.content).toContain("export * from './authentication-factor.interface';");
197
195
  });
198
196
 
197
+ it('propagates @deprecated from baseline service class to the property declaration', () => {
198
+ // Regression test for PR #1535 reviewer comment r3075509969: when a
199
+ // service class has `@deprecated` in its JSDoc, TS's deprecation-lint
200
+ // only fires at `new X()` call sites — not at `workos.x` access unless
201
+ // the property itself is annotated. The emitter propagates the class
202
+ // deprecation to the property JSDoc so IDEs surface the strikethrough
203
+ // on every access.
204
+ const deprecatedCtx: EmitterContext = {
205
+ ...ctx,
206
+ apiSurface: {
207
+ language: 'node',
208
+ extractedFrom: 'test',
209
+ extractedAt: '2026-01-01',
210
+ classes: {
211
+ Organizations: {
212
+ name: 'Organizations',
213
+ methods: {},
214
+ properties: {},
215
+ constructorParams: [{ name: 'workos', type: 'WorkOS', optional: false }],
216
+ deprecationMessage: 'Use `workos.connect` instead.',
217
+ },
218
+ },
219
+ interfaces: {},
220
+ typeAliases: {},
221
+ enums: {},
222
+ exports: {},
223
+ },
224
+ };
225
+
226
+ const files = generateClient(spec, deprecatedCtx);
227
+ const workosFile = files.find((f) => f.path === 'src/workos.ts')!;
228
+ const content = workosFile.content;
229
+
230
+ // Property JSDoc carries the deprecation and the directive is preserved
231
+ // on the line immediately above the accessor.
232
+ expect(content).toMatch(
233
+ /\/\*\* @deprecated Use `workos\.connect` instead\. \*\/\s+readonly organizations = new Organizations\(this\);/,
234
+ );
235
+ });
236
+
237
+ it('emits a bare @deprecated when the baseline class deprecation has no message', () => {
238
+ const deprecatedCtx: EmitterContext = {
239
+ ...ctx,
240
+ apiSurface: {
241
+ language: 'node',
242
+ extractedFrom: 'test',
243
+ extractedAt: '2026-01-01',
244
+ classes: {
245
+ Organizations: {
246
+ name: 'Organizations',
247
+ methods: {},
248
+ properties: {},
249
+ constructorParams: [{ name: 'workos', type: 'WorkOS', optional: false }],
250
+ deprecationMessage: '',
251
+ },
252
+ },
253
+ interfaces: {},
254
+ typeAliases: {},
255
+ enums: {},
256
+ exports: {},
257
+ },
258
+ };
259
+
260
+ const files = generateClient(spec, deprecatedCtx);
261
+ const workosFile = files.find((f) => f.path === 'src/workos.ts')!;
262
+ expect(workosFile.content).toMatch(/\/\*\* @deprecated \*\/\s+readonly organizations = new Organizations\(this\);/);
263
+ });
264
+
265
+ it('does not emit @deprecated when the baseline class has no deprecationMessage', () => {
266
+ const files = generateClient(spec, ctx);
267
+ const workosFile = files.find((f) => f.path === 'src/workos.ts')!;
268
+ expect(workosFile.content).not.toContain('@deprecated');
269
+ });
270
+
199
271
  it('does not generate error handling in WorkOS client (lives in WorkOSBase)', () => {
200
272
  const files = generateClient(spec, ctx);
201
273
  const workosFile = files.find((f) => f.path === 'src/workos.ts')!;
@@ -261,6 +333,7 @@ describe('generateClient', () => {
261
333
  services: [eventService],
262
334
  models: [eventModel, otherModel],
263
335
  enums: [],
336
+ sdk: defaultSdkBehavior(),
264
337
  };
265
338
 
266
339
  const surface: ApiSurface = {
@@ -300,9 +373,10 @@ describe('generateClient', () => {
300
373
  expect(content).not.toContain('export type { Event,');
301
374
  expect(content).not.toContain('export type { Event }');
302
375
 
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'");
376
+ // EventCursor is unreachable (not referenced by any service), so it should
377
+ // NOT be exported oagen only generates interface files for reachable models
378
+ expect(content).not.toContain("export * from './common/interfaces'");
379
+ expect(content).not.toContain('EventCursor');
306
380
 
307
381
  // The resource class export should still be present
308
382
  expect(content).toContain("export { Events } from './events/events'");
@@ -324,6 +398,7 @@ describe('generateClient', () => {
324
398
  ],
325
399
  },
326
400
  ],
401
+ sdk: defaultSdkBehavior(),
327
402
  };
328
403
 
329
404
  const surface: ApiSurface = {
@@ -454,6 +529,7 @@ describe('generateClient', () => {
454
529
  services: [service, enumService, dirService],
455
530
  models: [model],
456
531
  enums: [enumDef, aliasEnumDef],
532
+ sdk: defaultSdkBehavior(),
457
533
  };
458
534
  const enumCtx: EmitterContext = {
459
535
  namespace: 'workos',
@@ -575,6 +651,7 @@ describe('generateClient', () => {
575
651
  services: [connectionsService, radarService],
576
652
  models: [connectionModel, radarModel],
577
653
  enums: [],
654
+ sdk: defaultSdkBehavior(),
578
655
  };
579
656
 
580
657
  const coveredCtx: EmitterContext = {
@@ -715,6 +792,7 @@ describe('generateClient', () => {
715
792
  services: [partialService],
716
793
  models: [dirModel],
717
794
  enums: [],
795
+ sdk: defaultSdkBehavior(),
718
796
  };
719
797
 
720
798
  const partialCtx: EmitterContext = {
@@ -818,6 +896,7 @@ describe('generateClient', () => {
818
896
  services: [mfaService],
819
897
  models: [mfaModel],
820
898
  enums: [],
899
+ sdk: defaultSdkBehavior(),
821
900
  };
822
901
 
823
902
  const namingOnlyCtx: EmitterContext = {
@@ -859,6 +938,7 @@ describe('isServiceCoveredByExisting', () => {
859
938
  services: [],
860
939
  models: [],
861
940
  enums: [],
941
+ sdk: defaultSdkBehavior(),
862
942
  };
863
943
 
864
944
  it('returns false when no overlay is provided', () => {
@@ -1,6 +1,7 @@
1
1
  import { describe, it, expect } from 'vitest';
2
2
  import { generateEnums } from '../../src/node/enums.js';
3
3
  import type { EmitterContext, ApiSpec, Enum, Service } from '@workos/oagen';
4
+ import { defaultSdkBehavior } from '@workos/oagen';
4
5
 
5
6
  const emptySpec: ApiSpec = {
6
7
  name: 'Test',
@@ -9,6 +10,7 @@ const emptySpec: ApiSpec = {
9
10
  services: [],
10
11
  models: [],
11
12
  enums: [],
13
+ sdk: defaultSdkBehavior(),
12
14
  };
13
15
 
14
16
  const ctx: EmitterContext = {
@@ -2,47 +2,8 @@ import { describe, it, expect } from 'vitest';
2
2
  import { generateErrors } from '../../src/node/errors.js';
3
3
 
4
4
  describe('generateErrors', () => {
5
- it('generates all exception classes', () => {
5
+ it('returns empty array without context (static exceptions now hand-maintained)', () => {
6
6
  const files = generateErrors();
7
-
8
- const names = files.map((f) => f.path);
9
- expect(names).toContain('src/common/exceptions/bad-request.exception.ts');
10
- expect(names).toContain('src/common/exceptions/unauthorized.exception.ts');
11
- expect(names).toContain('src/common/exceptions/not-found.exception.ts');
12
- expect(names).toContain('src/common/exceptions/conflict.exception.ts');
13
- expect(names).toContain('src/common/exceptions/unprocessable-entity.exception.ts');
14
- expect(names).toContain('src/common/exceptions/rate-limit-exceeded.exception.ts');
15
- expect(names).toContain('src/common/exceptions/generic-server.exception.ts');
16
- expect(names).toContain('src/common/exceptions/no-api-key-provided.exception.ts');
17
- expect(names).toContain('src/common/exceptions/index.ts');
18
- });
19
-
20
- it('generates NotFoundException with correct status', () => {
21
- const files = generateErrors();
22
- const notFoundFile = files.find((f) => f.path.includes('not-found.exception.ts'))!;
23
-
24
- expect(notFoundFile.content).toContain('export class NotFoundException extends Error');
25
- expect(notFoundFile.content).toContain('readonly status = 404;');
26
- expect(notFoundFile.content).toContain('requestID: string');
27
- });
28
-
29
- it('generates RateLimitExceededException with retryAfter', () => {
30
- const files = generateErrors();
31
- const rateLimitFile = files.find((f) => f.path.includes('rate-limit-exceeded.exception.ts'))!;
32
-
33
- expect(rateLimitFile.content).toContain('export class RateLimitExceededException extends Error');
34
- expect(rateLimitFile.content).toContain('readonly status = 429;');
35
- expect(rateLimitFile.content).toContain('retryAfter?: number');
36
- });
37
-
38
- it('generates exception barrel with all exports', () => {
39
- const files = generateErrors();
40
- const barrel = files.find((f) => f.path === 'src/common/exceptions/index.ts')!;
41
-
42
- expect(barrel.content).toContain('export { BadRequestException }');
43
- expect(barrel.content).toContain('export { UnauthorizedException }');
44
- expect(barrel.content).toContain('export { NotFoundException }');
45
- expect(barrel.content).toContain('export { RateLimitExceededException }');
46
- expect(barrel.content).toContain('export { NoApiKeyProvidedException }');
7
+ expect(files).toEqual([]);
47
8
  });
48
9
  });
@@ -1,6 +1,7 @@
1
1
  import { describe, it, expect } from 'vitest';
2
2
  import { generateModels } from '../../src/node/models.js';
3
3
  import type { EmitterContext, ApiSpec, Model, Service } from '@workos/oagen';
4
+ import { defaultSdkBehavior } from '@workos/oagen';
4
5
 
5
6
  const emptySpec: ApiSpec = {
6
7
  name: 'Test',
@@ -9,6 +10,7 @@ const emptySpec: ApiSpec = {
9
10
  services: [],
10
11
  models: [],
11
12
  enums: [],
13
+ sdk: defaultSdkBehavior(),
12
14
  };
13
15
 
14
16
  const ctx: EmitterContext = {
@@ -10,10 +10,31 @@ import {
10
10
  servicePropertyName,
11
11
  resolveServiceName,
12
12
  buildServiceNameMap,
13
+ stripNoiseSuffixes,
13
14
  } from '../../src/node/naming.js';
14
15
  import type { EmitterContext, ApiSpec, Service } from '@workos/oagen';
16
+ import { defaultSdkBehavior } from '@workos/oagen';
15
17
 
16
18
  describe('naming', () => {
19
+ describe('stripNoiseSuffixes', () => {
20
+ it('strips trailing Dto', () => {
21
+ expect(stripNoiseSuffixes('OrganizationDto')).toBe('Organization');
22
+ expect(stripNoiseSuffixes('UpdateOrganizationDto')).toBe('UpdateOrganization');
23
+ });
24
+
25
+ it('is case-insensitive for the Dto suffix', () => {
26
+ expect(stripNoiseSuffixes('OrganizationDTO')).toBe('Organization');
27
+ });
28
+
29
+ it('does not strip Dto from the middle of a name', () => {
30
+ expect(stripNoiseSuffixes('DtoFactory')).toBe('DtoFactory');
31
+ });
32
+
33
+ it('leaves names without Dto unchanged', () => {
34
+ expect(stripNoiseSuffixes('Organization')).toBe('Organization');
35
+ });
36
+ });
37
+
17
38
  describe('className', () => {
18
39
  it('converts to PascalCase', () => {
19
40
  expect(className('organizations')).toBe('Organizations');
@@ -88,6 +109,7 @@ describe('naming', () => {
88
109
  services: [],
89
110
  models: [],
90
111
  enums: [],
112
+ sdk: defaultSdkBehavior(),
91
113
  };
92
114
 
93
115
  it('returns overlay class name when available', () => {
@@ -160,6 +182,7 @@ describe('naming', () => {
160
182
  services: [],
161
183
  models: [],
162
184
  enums: [],
185
+ sdk: defaultSdkBehavior(),
163
186
  };
164
187
 
165
188
  it('maps IR names to resolved names', () => {