@workos/oagen-emitters 0.12.0 → 0.12.2

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 (53) hide show
  1. package/.github/workflows/ci.yml +1 -1
  2. package/.github/workflows/lint-pr-title.yml +1 -1
  3. package/.github/workflows/lint.yml +1 -1
  4. package/.github/workflows/release-please.yml +2 -2
  5. package/.github/workflows/release.yml +1 -1
  6. package/.node-version +1 -1
  7. package/.release-please-manifest.json +1 -1
  8. package/CHANGELOG.md +14 -0
  9. package/dist/index.d.mts.map +1 -1
  10. package/dist/index.mjs +1 -1
  11. package/dist/{plugin-C408Wh-o.mjs → plugin-eCuvoL1T.mjs} +3914 -2121
  12. package/dist/plugin-eCuvoL1T.mjs.map +1 -0
  13. package/dist/plugin.d.mts.map +1 -1
  14. package/dist/plugin.mjs +1 -1
  15. package/package.json +10 -10
  16. package/renovate.json +46 -6
  17. package/src/node/client.ts +19 -32
  18. package/src/node/enums.ts +67 -30
  19. package/src/node/errors.ts +2 -8
  20. package/src/node/field-plan.ts +188 -52
  21. package/src/node/fixtures.ts +11 -33
  22. package/src/node/index.ts +345 -20
  23. package/src/node/live-surface.ts +378 -0
  24. package/src/node/models.ts +540 -351
  25. package/src/node/naming.ts +119 -25
  26. package/src/node/node-overrides.ts +77 -0
  27. package/src/node/options.ts +41 -0
  28. package/src/node/resources.ts +455 -46
  29. package/src/node/sdk-errors.ts +0 -16
  30. package/src/node/tests.ts +108 -83
  31. package/src/node/type-map.ts +40 -18
  32. package/src/node/utils.ts +89 -102
  33. package/src/node/wrappers.ts +0 -20
  34. package/src/rust/fixtures.ts +87 -1
  35. package/src/rust/models.ts +17 -2
  36. package/src/rust/resources.ts +697 -62
  37. package/src/rust/tests.ts +540 -20
  38. package/test/node/client.test.ts +106 -1201
  39. package/test/node/enums.test.ts +59 -130
  40. package/test/node/errors.test.ts +2 -3
  41. package/test/node/live-surface.test.ts +240 -0
  42. package/test/node/models.test.ts +396 -765
  43. package/test/node/naming.test.ts +69 -234
  44. package/test/node/resources.test.ts +376 -2036
  45. package/test/node/tests.test.ts +119 -0
  46. package/test/node/type-map.test.ts +49 -54
  47. package/test/node/utils.test.ts +29 -80
  48. package/test/rust/fixtures.test.ts +227 -0
  49. package/test/rust/models.test.ts +38 -0
  50. package/test/rust/resources.test.ts +505 -2
  51. package/test/rust/tests.test.ts +504 -0
  52. package/dist/plugin-C408Wh-o.mjs.map +0 -1
  53. package/test/node/serializers.test.ts +0 -444
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.d.mts","names":[],"sources":["../src/plugin.ts"],"mappings":";;;cA0Ba,oBAAA,EAAsB,IAAA,CAAK,WAAA"}
1
+ {"version":3,"file":"plugin.d.mts","names":[],"sources":["../src/plugin.ts"],"mappings":";;;cA0Ba,oBAAA,EAAsB,IAAI,CAAC,WAAA"}
package/dist/plugin.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { t as workosEmittersPlugin } from "./plugin-C408Wh-o.mjs";
1
+ import { t as workosEmittersPlugin } from "./plugin-eCuvoL1T.mjs";
2
2
  export { workosEmittersPlugin };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@workos/oagen-emitters",
3
- "version": "0.12.0",
3
+ "version": "0.12.2",
4
4
  "description": "WorkOS' oagen emitters",
5
5
  "license": "MIT",
6
6
  "author": "WorkOS",
@@ -38,22 +38,22 @@
38
38
  "prepare": "husky"
39
39
  },
40
40
  "devDependencies": {
41
- "@commitlint/cli": "^20.5.3",
42
- "@commitlint/config-conventional": "^20.5.3",
43
- "@types/node": "^25.6.0",
41
+ "@commitlint/cli": "^21.0.1",
42
+ "@commitlint/config-conventional": "^21.0.1",
43
+ "@types/node": "^25.8.0",
44
44
  "husky": "^9.1.7",
45
- "oxfmt": "^0.48.0",
46
- "oxlint": "^1.63.0",
45
+ "oxfmt": "^0.50.0",
46
+ "oxlint": "^1.65.0",
47
47
  "prettier": "^3.8.3",
48
- "tsdown": "^0.21.10",
49
- "tsx": "^4.21.0",
48
+ "tsdown": "^0.22.0",
49
+ "tsx": "^4.22.0",
50
50
  "typescript": "^6.0.3",
51
- "vitest": "^4.1.5"
51
+ "vitest": "^4.1.6"
52
52
  },
53
53
  "engines": {
54
54
  "node": ">=24.10.0"
55
55
  },
56
56
  "dependencies": {
57
- "@workos/oagen": "^0.18.1"
57
+ "@workos/oagen": "^0.19.0"
58
58
  }
59
59
  }
package/renovate.json CHANGED
@@ -1,25 +1,65 @@
1
1
  {
2
- "extends": ["config:recommended"],
2
+ "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3
+ "extends": [
4
+ "config:recommended"
5
+ ],
3
6
  "dependencyDashboard": false,
4
- "schedule": ["on the 15th day of the month before 12pm"],
7
+ "schedule": [
8
+ "on the 15th day of the month before 12pm"
9
+ ],
5
10
  "timezone": "UTC",
6
11
  "rebaseWhen": "conflicted",
7
12
  "packageRules": [
8
13
  {
9
- "matchManagers": ["github-actions"],
14
+ "matchManagers": [
15
+ "github-actions"
16
+ ],
17
+ "pinDigests": true,
10
18
  "extractVersion": "^v(?<version>\\d+\\.\\d+\\.\\d+)$"
11
19
  },
12
20
  {
13
- "matchUpdateTypes": ["minor", "patch"],
21
+ "matchUpdateTypes": [
22
+ "minor",
23
+ "patch"
24
+ ],
14
25
  "automerge": true,
15
26
  "groupName": "minor and patch updates"
16
27
  },
17
28
  {
18
- "matchUpdateTypes": ["major"],
29
+ "matchUpdateTypes": [
30
+ "major"
31
+ ],
19
32
  "automerge": false
20
33
  },
21
34
  {
22
- "matchUpdateTypes": ["digest"],
35
+ "matchUpdateTypes": [
36
+ "digest"
37
+ ],
38
+ "automerge": false
39
+ },
40
+ {
41
+ "matchManagers": [
42
+ "github-actions"
43
+ ],
44
+ "matchUpdateTypes": [
45
+ "minor",
46
+ "patch",
47
+ "digest",
48
+ "pinDigest"
49
+ ],
50
+ "groupName": "github actions non-major",
51
+ "groupSlug": "github-actions-non-major",
52
+ "automerge": true
53
+ },
54
+ {
55
+ "matchManagers": [
56
+ "github-actions"
57
+ ],
58
+ "matchUpdateTypes": [
59
+ "major"
60
+ ],
61
+ "groupName": "github actions major",
62
+ "groupSlug": "github-actions-major",
23
63
  "automerge": false
24
64
  }
25
65
  ]
@@ -1,8 +1,9 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
- import type { ApiSpec, AuthScheme, EmitterContext, GeneratedFile, Service } from '@workos/oagen';
3
+ import type { ApiSpec, AuthScheme, EmitterContext, GeneratedFile } from '@workos/oagen';
4
4
 
5
- import { fileName, resolveServiceDir, servicePropertyName, resolveInterfaceName, wireInterfaceName } from './naming.js';
5
+ import { fileName, servicePropertyName, resolveInterfaceName, wireInterfaceName } from './naming.js';
6
+ import { isInlineEnum } from './type-map.js';
6
7
  import {
7
8
  docComment,
8
9
  createServiceDirResolver,
@@ -11,7 +12,9 @@ import {
11
12
  isListWrapperModel,
12
13
  computeNonEventReachable,
13
14
  } from './utils.js';
14
- import { resolveResourceClassName } from './resources.js';
15
+ import { resolveResourceClassName, resolveResourceDir } from './resources.js';
16
+ import { generatedResourceInterfaceModelNames } from './models.js';
17
+ import { assignEnumsToServices } from './enums.js';
15
18
 
16
19
  export function generateClient(spec: ApiSpec, ctx: EmitterContext): GeneratedFile[] {
17
20
  const files: GeneratedFile[] = [];
@@ -48,7 +51,7 @@ function generateWorkOSClient(spec: ApiSpec, ctx: EmitterContext): GeneratedFile
48
51
  for (const service of spec.services) {
49
52
  if (coveredServices.has(service.name)) continue;
50
53
  const resolvedName = resolveResourceClassName(service, ctx);
51
- const serviceDir = resolveServiceDir(resolvedName);
54
+ const serviceDir = resolveResourceDir(service, ctx);
52
55
  lines.push(`import { ${resolvedName} } from './${serviceDir}/${fileName(resolvedName)}';`);
53
56
  }
54
57
 
@@ -126,6 +129,7 @@ function generateWorkOSClient(spec: ApiSpec, ctx: EmitterContext): GeneratedFile
126
129
  function generateServiceBarrels(spec: ApiSpec, ctx: EmitterContext): GeneratedFile[] {
127
130
  const files: GeneratedFile[] = [];
128
131
  const { modelToService, resolveDir } = createServiceDirResolver(spec.models, spec.services, ctx);
132
+ const enumToService = assignEnumsToServices(spec.enums, spec.services, spec.models, ctx);
129
133
 
130
134
  // Group interface files by directory, tracking exported symbol names
131
135
  // to prevent TS2308 duplicate export errors when two files in the same
@@ -189,7 +193,8 @@ function generateServiceBarrels(spec: ApiSpec, ctx: EmitterContext): GeneratedFi
189
193
  // from common utils, so no per-resource interface file is generated.
190
194
  // Also skip unreachable models — use the same non-event reachability as model
191
195
  // generation so every barrel entry has a corresponding generated file.
192
- const barrelReachable = computeNonEventReachable(spec.services, spec.models);
196
+ const barrelReachable =
197
+ generatedResourceInterfaceModelNames(spec.models, ctx) ?? computeNonEventReachable(spec.services, spec.models);
193
198
  for (const model of spec.models) {
194
199
  if (isListMetadataModel(model) || isListWrapperModel(model)) continue;
195
200
  if (!barrelReachable.has(model.name)) continue;
@@ -226,7 +231,10 @@ function generateServiceBarrels(spec: ApiSpec, ctx: EmitterContext): GeneratedFi
226
231
 
227
232
  // Enums -> service directories
228
233
  for (const enumDef of spec.enums) {
229
- const enumService = findEnumService(enumDef.name, spec.services);
234
+ // Inlined enums have no file to re-export.
235
+ if (isInlineEnum(enumDef.name)) continue;
236
+
237
+ const enumService = enumToService.get(enumDef.name);
230
238
  const dirName = resolveDir(enumService);
231
239
  if (!dirExports.has(dirName)) {
232
240
  dirExports.set(dirName, []);
@@ -368,6 +376,7 @@ function generateServiceBarrels(spec: ApiSpec, ctx: EmitterContext): GeneratedFi
368
376
  function generateBarrel(spec: ApiSpec, ctx: EmitterContext): GeneratedFile {
369
377
  const lines: string[] = [];
370
378
  const { modelToService, resolveDir } = createServiceDirResolver(spec.models, spec.services, ctx);
379
+ const enumToService = assignEnumsToServices(spec.enums, spec.services, spec.models, ctx);
371
380
 
372
381
  // Track all exported names to prevent duplicates.
373
382
  // Pre-seed with names already exported by the existing SDK to avoid generating
@@ -492,7 +501,7 @@ function generateBarrel(spec: ApiSpec, ctx: EmitterContext): GeneratedFile {
492
501
  // Per-service exports: service barrel + resource class
493
502
  for (const service of spec.services) {
494
503
  const resolvedName = resolveResourceClassName(service, ctx);
495
- const serviceDir = resolveServiceDir(resolvedName);
504
+ const serviceDir = resolveResourceDir(service, ctx);
496
505
  // The interfaces directory may differ from the resource class directory when
497
506
  // a service's class name is remapped (e.g., WebhooksEndpoints class lives in
498
507
  // webhooks-endpoints/ but its model interfaces live in webhooks/).
@@ -508,7 +517,7 @@ function generateBarrel(spec: ApiSpec, ctx: EmitterContext): GeneratedFile {
508
517
  return true;
509
518
  });
510
519
  const serviceEnums = spec.enums.filter((e) => {
511
- const enumService = findEnumService(e.name, spec.services);
520
+ const enumService = enumToService.get(e.name);
512
521
  return enumService === service.name;
513
522
  });
514
523
 
@@ -575,7 +584,7 @@ function generateBarrel(spec: ApiSpec, ctx: EmitterContext): GeneratedFile {
575
584
  const reachable = computeNonEventReachable(spec.services, spec.models);
576
585
  const unassignedModels = spec.models.filter((m) => !modelToService.has(m.name) && reachable.has(m.name));
577
586
  const commonEnums = spec.enums.filter((e) => {
578
- const enumService = findEnumService(e.name, spec.services);
587
+ const enumService = enumToService.get(e.name);
579
588
  return !enumService;
580
589
  });
581
590
 
@@ -615,7 +624,7 @@ function generateBarrel(spec: ApiSpec, ctx: EmitterContext): GeneratedFile {
615
624
  if (exportedNames.has(enumDef.name)) continue;
616
625
  if (existingSdkExports.has(enumDef.name)) continue;
617
626
  exportedNames.add(enumDef.name);
618
- const enumService = findEnumService(enumDef.name, spec.services);
627
+ const enumService = enumToService.get(enumDef.name);
619
628
  const dir = resolveDir(enumService);
620
629
  if (!exportedDirs.has(dir)) {
621
630
  const baselineEnum = ctx.apiSurface?.enums?.[enumDef.name];
@@ -643,28 +652,6 @@ function generateBarrel(spec: ApiSpec, ctx: EmitterContext): GeneratedFile {
643
652
  };
644
653
  }
645
654
 
646
- function findEnumService(enumName: string, services: Service[]): string | undefined {
647
- for (const service of services) {
648
- for (const op of service.operations) {
649
- const refs: string[] = [];
650
- const collect = (ref: any) => {
651
- if (ref?.kind === 'enum' && ref.name === enumName) refs.push(ref.name);
652
- if (ref?.items) collect(ref.items);
653
- if (ref?.inner) collect(ref.inner);
654
- if (ref?.variants) ref.variants.forEach(collect);
655
- if (ref?.valueType) collect(ref.valueType);
656
- };
657
- if (op.requestBody) collect(op.requestBody);
658
- collect(op.response);
659
- for (const p of [...op.pathParams, ...op.queryParams]) {
660
- collect(p.type);
661
- }
662
- if (refs.length > 0) return service.name;
663
- }
664
- }
665
- return undefined;
666
- }
667
-
668
655
  /**
669
656
  * Determine whether the spec's auth scheme requires overriding the
670
657
  * default bearer auth in WorkOSBase.setAuthHeaders().
package/src/node/enums.ts CHANGED
@@ -1,42 +1,43 @@
1
- import type { Enum, EmitterContext, GeneratedFile, Service } from '@workos/oagen';
2
- import { toPascalCase, walkTypeRef } from '@workos/oagen';
1
+ import type { Enum, EmitterContext, GeneratedFile, Model, Service } from '@workos/oagen';
2
+ import { assignModelsToServices, collectFieldDependencies, toPascalCase, walkTypeRef } from '@workos/oagen';
3
3
  import { fileName, resolveServiceDir, buildServiceNameMap } from './naming.js';
4
4
  import { docComment } from './utils.js';
5
+ import { isInlineEnum } from './type-map.js';
6
+ import { liveSurfaceConstEnumMembers, liveSurfaceInterfacePath } from './live-surface.js';
5
7
 
6
8
  export function generateEnums(enums: Enum[], ctx: EmitterContext): GeneratedFile[] {
7
9
  if (enums.length === 0) return [];
8
10
 
9
- const enumToService = assignEnumsToServices(enums, ctx.spec.services);
11
+ const enumToService = assignEnumsToServices(enums, ctx.spec.services, ctx.spec.models, ctx);
10
12
  const serviceNameMap = buildServiceNameMap(ctx.spec.services, ctx);
11
13
  const resolveDir = (irService: string | undefined) =>
12
14
  irService ? resolveServiceDir(serviceNameMap.get(irService) ?? irService) : 'common';
13
15
  const files: GeneratedFile[] = [];
14
16
 
15
17
  for (const enumDef of enums) {
18
+ // Inlined enums get expanded at usage sites by `type-map`. No file needed.
19
+ if (isInlineEnum(enumDef.name)) continue;
20
+
16
21
  const service = enumToService.get(enumDef.name);
17
22
  const dirName = resolveDir(service);
18
23
 
19
- // Check baseline surface for representation and values
20
24
  const baselineEnum = ctx.apiSurface?.enums?.[enumDef.name];
21
25
  const baselineAlias = ctx.apiSurface?.typeAliases?.[enumDef.name];
22
26
  const generatedPath = `src/${dirName}/interfaces/${fileName(enumDef.name)}.interface.ts`;
23
27
 
24
- // If the baseline already provides this enum from a different file (e.g., `.enum.ts`),
25
- // skip generation to avoid duplicate exports from the same barrel.
26
- const baselineSourceFile = (baselineEnum as any)?.sourceFile ?? (baselineAlias as any)?.sourceFile;
28
+ const baselineSourceFile =
29
+ (baselineEnum as any)?.sourceFile ?? (baselineAlias as any)?.sourceFile ?? liveSurfaceInterfacePath(enumDef.name);
30
+ if (dirName === 'common' && !baselineSourceFile && (ctx.outputDir || ctx.targetDir || ctx.apiSurface)) {
31
+ continue;
32
+ }
27
33
  if (baselineSourceFile && baselineSourceFile !== generatedPath) {
28
34
  continue;
29
35
  }
30
36
 
31
37
  const lines: string[] = [];
32
-
33
- // Track whether the generated content has new values not in the baseline.
34
- // When it does, skipIfExists must be false so the file gets updated.
35
38
  let hasNewValues = false;
36
39
 
37
40
  if (baselineEnum?.members) {
38
- // Generate TS `enum` using baseline member names and values, merging
39
- // any new IR values that the baseline is missing.
40
41
  const existingValues = new Set(Object.values(baselineEnum.members).map(String));
41
42
  const irValues = enumDef.values.map((v) => String(v.value));
42
43
  const missingValues = irValues.filter((v) => !existingValues.has(v));
@@ -47,21 +48,17 @@ export function generateEnums(enums: Enum[], ctx: EmitterContext): GeneratedFile
47
48
  const valueStr = typeof memberValue === 'string' ? `'${memberValue}'` : String(memberValue);
48
49
  lines.push(` ${memberName} = ${valueStr},`);
49
50
  }
50
- // Append new values from the spec that the baseline is missing
51
51
  for (const val of missingValues) {
52
- // Derive a PascalCase member name from the value
53
52
  const memberName = toPascalCase(val);
54
53
  lines.push(` ${memberName} = '${val}',`);
55
54
  }
56
55
  lines.push('}');
57
56
  } else if (baselineAlias?.value) {
58
- // Use the baseline type alias value, but merge in any new IR values the baseline is missing.
59
57
  const baselineValues = extractLiteralUnionValues(baselineAlias.value);
60
58
  const irValues = enumDef.values.map((v) => String(v.value));
61
59
  const missing = irValues.filter((v) => !baselineValues.has(v));
62
60
  hasNewValues = missing.length > 0;
63
61
  if (missing.length > 0) {
64
- // Baseline is missing values from the spec — regenerate with all values merged
65
62
  const allValues = [...baselineValues, ...missing];
66
63
  const parts = allValues.map((v) => `'${v}'`);
67
64
  lines.push(`export type ${enumDef.name} = ${parts.join(' | ')};`);
@@ -69,11 +66,36 @@ export function generateEnums(enums: Enum[], ctx: EmitterContext): GeneratedFile
69
66
  lines.push(`export type ${enumDef.name} = ${baselineAlias.value};`);
70
67
  }
71
68
  } else {
72
- // No baseline generate string literal union from IR values
69
+ // No baseline form available emit the workos-node house style:
70
+ //
71
+ // export const X = { Member: 'value', ... } as const;
72
+ // export type X = (typeof X)[keyof typeof X];
73
+ //
74
+ // This dual declaration lets callers use either the type (`X`) or the
75
+ // namespace (`X.Member`) without paying for a TypeScript `enum`'s
76
+ // runtime overhead. Emitting only the type alias would compile but
77
+ // break hand-written test files that import the enum as a value.
78
+ //
79
+ // Member name resolution, per value:
80
+ // 1. If the live SDK already declares this enum as a const-object
81
+ // with a member for this exact value, reuse the existing member
82
+ // name. This preserves acronym casing (`DSync`, `SAML`, `JWT`)
83
+ // that the simpler `toPascalCase` would otherwise flatten.
84
+ // 2. Otherwise PascalCase the value.
85
+ // 3. Skip duplicate values and duplicate member names — the union
86
+ // type derived from the const captures every kept value.
73
87
  const values = enumDef.values;
74
- lines.push(`export type ${enumDef.name} =`);
75
- for (let i = 0; i < values.length; i++) {
76
- const v = values[i];
88
+ const existingMembers = liveSurfaceConstEnumMembers(enumDef.name);
89
+ const seenMembers = new Set<string>();
90
+ const seenValues = new Set<string>();
91
+ lines.push(`export const ${enumDef.name} = {`);
92
+ for (const v of values) {
93
+ const valueKey = String(v.value);
94
+ if (seenValues.has(valueKey)) continue;
95
+ seenValues.add(valueKey);
96
+ const memberName = existingMembers?.get(valueKey) ?? toPascalCase(valueKey);
97
+ if (seenMembers.has(memberName)) continue;
98
+ seenMembers.add(memberName);
77
99
  const valueStr = typeof v.value === 'string' ? `'${v.value}'` : String(v.value);
78
100
  if (v.description || v.deprecated) {
79
101
  const parts: string[] = [];
@@ -81,16 +103,17 @@ export function generateEnums(enums: Enum[], ctx: EmitterContext): GeneratedFile
81
103
  if (v.deprecated) parts.push('@deprecated');
82
104
  lines.push(...docComment(parts.join('\n'), 2));
83
105
  }
84
- const suffix = i === values.length - 1 ? ';' : '';
85
- lines.push(` | ${valueStr}${suffix}`);
106
+ lines.push(` ${memberName}: ${valueStr},`);
86
107
  }
108
+ lines.push(`} as const;`);
109
+ lines.push('');
110
+ lines.push(`export type ${enumDef.name} =`);
111
+ lines.push(` (typeof ${enumDef.name})[keyof typeof ${enumDef.name}];`);
87
112
  }
88
113
 
89
114
  files.push({
90
115
  path: `src/${dirName}/interfaces/${fileName(enumDef.name)}.interface.ts`,
91
116
  content: lines.join('\n'),
92
- // When the spec has new values the baseline is missing, allow the file
93
- // to be updated so the SDK picks up the full set of enum values.
94
117
  skipIfExists: !hasNewValues,
95
118
  });
96
119
  }
@@ -98,13 +121,8 @@ export function generateEnums(enums: Enum[], ctx: EmitterContext): GeneratedFile
98
121
  return files;
99
122
  }
100
123
 
101
- /**
102
- * Parse a TypeScript string literal union type alias value (e.g., "'a' | 'b' | 'c'")
103
- * into a set of its string values.
104
- */
105
124
  function extractLiteralUnionValues(aliasValue: string): Set<string> {
106
125
  const values = new Set<string>();
107
- // Match all single-quoted string literals in the union
108
126
  const regex = /'([^']+)'/g;
109
127
  let match;
110
128
  while ((match = regex.exec(aliasValue)) !== null) {
@@ -113,7 +131,12 @@ function extractLiteralUnionValues(aliasValue: string): Set<string> {
113
131
  return values;
114
132
  }
115
133
 
116
- export function assignEnumsToServices(enums: Enum[], services: Service[]): Map<string, string> {
134
+ export function assignEnumsToServices(
135
+ enums: Enum[],
136
+ services: Service[],
137
+ models: Model[] = [],
138
+ ctx?: EmitterContext,
139
+ ): Map<string, string> {
117
140
  const enumToService = new Map<string, string>();
118
141
  const enumNames = new Set(enums.map((e) => e.name));
119
142
 
@@ -136,5 +159,19 @@ export function assignEnumsToServices(enums: Enum[], services: Service[]): Map<s
136
159
  }
137
160
  }
138
161
 
162
+ if (models.length > 0) {
163
+ const modelToService = assignModelsToServices(models, services, ctx?.modelHints);
164
+ for (const model of models) {
165
+ const service = modelToService.get(model.name);
166
+ if (!service) continue;
167
+
168
+ for (const name of collectFieldDependencies(model).enums) {
169
+ if (enumNames.has(name) && !enumToService.has(name)) {
170
+ enumToService.set(name, service);
171
+ }
172
+ }
173
+ }
174
+ }
175
+
139
176
  return enumToService;
140
177
  }
@@ -3,7 +3,7 @@ import { fileName } from './naming.js';
3
3
  import { buildNodeStatusExceptions } from './sdk-errors.js';
4
4
 
5
5
  /**
6
- * Static exception classes are now hand-maintained in the target SDK.
6
+ * Static exception classes are hand-maintained in the target SDK.
7
7
  * Only typed exceptions derived from spec error models are generated here.
8
8
  */
9
9
  export function generateErrors(ctx?: EmitterContext): GeneratedFile[] {
@@ -45,8 +45,6 @@ export function generateErrors(ctx?: EmitterContext): GeneratedFile[] {
45
45
  exportLines.push(`export { ${exceptionClassName} } from './${fileName(modelName)}.exception';`);
46
46
  }
47
47
 
48
- // Generate a barrel for typed errors only (appended to existing exceptions/index.ts
49
- // via region preservation if needed)
50
48
  if (exportLines.length > 0) {
51
49
  files.push({
52
50
  path: 'src/common/exceptions/typed-errors.ts',
@@ -64,11 +62,7 @@ function collectTypedErrors(
64
62
  ): { modelName: string; statusCode: number; baseException: string | null }[] {
65
63
  const statusToBase = buildNodeStatusExceptions(ctx.spec.sdk);
66
64
  const seen = new Set<string>();
67
- const results: {
68
- modelName: string;
69
- statusCode: number;
70
- baseException: string | null;
71
- }[] = [];
65
+ const results: { modelName: string; statusCode: number; baseException: string | null }[] = [];
72
66
 
73
67
  for (const service of ctx.spec.services) {
74
68
  for (const op of service.operations) {