@workos/oagen-emitters 0.12.5 → 0.13.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/dist/plugin.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { t as workosEmittersPlugin } from "./plugin-Ca9LUkWW.mjs";
1
+ import { t as workosEmittersPlugin } from "./plugin-B9F2jmwy.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.5",
3
+ "version": "0.13.0",
4
4
  "description": "WorkOS' oagen emitters",
5
5
  "license": "MIT",
6
6
  "author": "WorkOS",
@@ -40,20 +40,20 @@
40
40
  "devDependencies": {
41
41
  "@commitlint/cli": "^21.0.1",
42
42
  "@commitlint/config-conventional": "^21.0.1",
43
- "@types/node": "^25.9.0",
43
+ "@types/node": "^25.9.1",
44
44
  "husky": "^9.1.7",
45
- "oxfmt": "^0.50.0",
46
- "oxlint": "^1.65.0",
45
+ "oxfmt": "^0.51.0",
46
+ "oxlint": "^1.66.0",
47
47
  "prettier": "^3.8.3",
48
48
  "tsdown": "^0.22.0",
49
- "tsx": "^4.22.2",
49
+ "tsx": "^4.22.3",
50
50
  "typescript": "^6.0.3",
51
- "vitest": "^4.1.6"
51
+ "vitest": "^4.1.7"
52
52
  },
53
53
  "engines": {
54
54
  "node": ">=24.10.0"
55
55
  },
56
56
  "dependencies": {
57
- "@workos/oagen": "^0.19.1"
57
+ "@workos/oagen": "^0.19.5"
58
58
  }
59
59
  }
@@ -1,5 +1,5 @@
1
1
  import type { ApiSpec, EmitterContext, GeneratedFile, Service } from '@workos/oagen';
2
- import { apiClassName, packageSegment, servicePropertyName } from './naming.js';
2
+ import { resolveApiClassName, packageSegment, servicePropertyName, buildExportedClassNameSet } from './naming.js';
3
3
  import { getMountTarget } from '../shared/resolved-ops.js';
4
4
 
5
5
  const KOTLIN_SRC_PREFIX = 'src/main/kotlin/';
@@ -23,8 +23,9 @@ export function generateClient(spec: ApiSpec, ctx: EmitterContext): GeneratedFil
23
23
 
24
24
  const imports = new Set<string>();
25
25
  const accessorLines: string[] = [];
26
+ const exportedClasses = buildExportedClassNameSet(ctx);
26
27
  for (const mount of targets) {
27
- const apiCls = apiClassName(mount);
28
+ const apiCls = resolveApiClassName(mount, exportedClasses);
28
29
  const fqn = `com.workos.${packageSegment(mount)}.${apiCls}`;
29
30
  imports.add(fqn);
30
31
  const prop = servicePropertyName(mount);
@@ -1,4 +1,8 @@
1
1
  import type { Operation, Service, EmitterContext, TypeRef } from '@workos/oagen';
2
+ import {
3
+ buildExportedClassNameSet as buildExportedClassNameSetShared,
4
+ resolveServiceTarget as resolveServiceTargetShared,
5
+ } from '../shared/service-name-collision.js';
2
6
  import { toPascalCase, toCamelCase, toSnakeCase } from '@workos/oagen';
3
7
  import { buildResolvedLookup, lookupMethodName, getMountTarget } from '../shared/resolved-ops.js';
4
8
  import { stripUrnPrefix } from '../shared/naming-utils.js';
@@ -73,6 +77,36 @@ export function apiClassName(name: string): string {
73
77
  return className(name);
74
78
  }
75
79
 
80
+ /**
81
+ * Resolve the Kotlin service class name with the collision suffix applied
82
+ * when needed. Wraps `apiClassName` so callers don't need to thread the
83
+ * exported-classes set through unrelated emission logic.
84
+ */
85
+ export function resolveApiClassName(name: string, exportedClasses: Set<string>): string {
86
+ return apiClassName(resolveServiceTarget(name, exportedClasses));
87
+ }
88
+
89
+ /**
90
+ * Build the set of model + enum class names exported by the SDK. Used to
91
+ * detect collisions with operation-client class names — a colliding service
92
+ * gets a `Service` suffix appended.
93
+ */
94
+ export function buildExportedClassNameSet(ctx: EmitterContext): Set<string> {
95
+ return buildExportedClassNameSetShared(ctx, className);
96
+ }
97
+
98
+ /**
99
+ * Resolve a service's mount-target identifier, appending `Service` on
100
+ * collision with an exported model/enum class. Kotlin sees the collision
101
+ * when a service class shares a simple name with an imported model class
102
+ * (e.g. `com.workos.models.OrganizationMembership` vs
103
+ * `com.workos.organizationmembership.OrganizationMembership`) — the file's
104
+ * local declaration shadows the import for unqualified references.
105
+ */
106
+ export function resolveServiceTarget(target: string, exportedClasses: Set<string>): string {
107
+ return resolveServiceTargetShared(target, exportedClasses, className);
108
+ }
109
+
76
110
  /** Accessor property exposed on the WorkOS client (camelCase). */
77
111
  export function servicePropertyName(name: string): string {
78
112
  return toCamelCase(name);
@@ -16,7 +16,7 @@ import { enumCanonicalMap } from './enums.js';
16
16
  import {
17
17
  className,
18
18
  propertyName,
19
- apiClassName,
19
+ resolveApiClassName,
20
20
  packageSegment,
21
21
  resolveMethodName,
22
22
  ktLiteral,
@@ -24,6 +24,7 @@ import {
24
24
  escapeReserved,
25
25
  humanize,
26
26
  maybeShortenEnumParamDescription,
27
+ buildExportedClassNameSet,
27
28
  } from './naming.js';
28
29
  import {
29
30
  buildResolvedLookup,
@@ -96,13 +97,14 @@ export function generateResources(services: Service[], ctx: EmitterContext): Gen
96
97
 
97
98
  const files: GeneratedFile[] = [];
98
99
  const resolvedLookup = buildResolvedLookup(ctx);
100
+ const exportedClasses = buildExportedClassNameSet(ctx);
99
101
 
100
102
  for (const [mountName, group] of mountGroups) {
101
103
  const classCode = generateApiClass(mountName, group.operations, ctx, resolvedLookup);
102
104
  if (!classCode) continue;
103
105
  const pkg = packageSegment(mountName);
104
106
  files.push({
105
- path: `${KOTLIN_SRC_PREFIX}com/workos/${pkg}/${apiClassName(mountName)}.kt`,
107
+ path: `${KOTLIN_SRC_PREFIX}com/workos/${pkg}/${resolveApiClassName(mountName, exportedClasses)}.kt`,
106
108
  content: classCode,
107
109
  overwriteExisting: true,
108
110
  });
@@ -118,7 +120,7 @@ function generateApiClass(
118
120
  resolvedLookup: Map<string, ResolvedOperation>,
119
121
  ): string | null {
120
122
  if (operations.length === 0) return null;
121
- const apiClass = apiClassName(mountName);
123
+ const apiClass = resolveApiClassName(mountName, buildExportedClassNameSet(ctx));
122
124
  const pkg = `com.workos.${packageSegment(mountName)}`;
123
125
 
124
126
  const imports = new Set<string>();
@@ -10,7 +10,15 @@ import type {
10
10
  ResolvedWrapper,
11
11
  } from '@workos/oagen';
12
12
  import { planOperation } from '@workos/oagen';
13
- import { apiClassName, packageSegment, resolveMethodName, ktStringLiteral, className, propertyName } from './naming.js';
13
+ import {
14
+ resolveApiClassName,
15
+ packageSegment,
16
+ resolveMethodName,
17
+ ktStringLiteral,
18
+ className,
19
+ propertyName,
20
+ buildExportedClassNameSet,
21
+ } from './naming.js';
14
22
  import { mapTypeRef } from './type-map.js';
15
23
  import {
16
24
  groupByMount,
@@ -70,12 +78,13 @@ export function generateTests(spec: ApiSpec, ctx: EmitterContext): GeneratedFile
70
78
  const mountGroups = groupByMount(ctx);
71
79
  const resolvedLookup = buildResolvedLookup(ctx);
72
80
 
81
+ const exportedClasses = buildExportedClassNameSet(ctx);
73
82
  for (const [mountName, group] of mountGroups) {
74
83
  const content = generateServiceTestClass(mountName, group.operations, ctx, resolvedLookup);
75
84
  if (!content) continue;
76
85
  const pkg = packageSegment(mountName);
77
86
  files.push({
78
- path: `${TEST_PREFIX}com/workos/${pkg}/${apiClassName(mountName)}Test.kt`,
87
+ path: `${TEST_PREFIX}com/workos/${pkg}/${resolveApiClassName(mountName, exportedClasses)}Test.kt`,
79
88
  content,
80
89
  overwriteExisting: true,
81
90
  });
@@ -203,7 +212,7 @@ function generateServiceTestClass(
203
212
  }
204
213
 
205
214
  const pkg = packageSegment(mountName);
206
- const apiCls = apiClassName(mountName);
215
+ const apiCls = resolveApiClassName(mountName, buildExportedClassNameSet(ctx));
207
216
 
208
217
  // If any operation would emit a disabled placeholder test, preregister
209
218
  // the `Disabled` import before we serialize the header.
@@ -2,7 +2,13 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import type { ApiSpec, AuthScheme, EmitterContext, GeneratedFile } from '@workos/oagen';
4
4
 
5
- import { fileName, servicePropertyName, resolveInterfaceName, wireInterfaceName } from './naming.js';
5
+ import {
6
+ fileName,
7
+ servicePropertyName,
8
+ resolveInterfaceName,
9
+ wireInterfaceName,
10
+ resolveServiceName,
11
+ } from './naming.js';
6
12
  import { isInlineEnum } from './type-map.js';
7
13
  import {
8
14
  docComment,
@@ -89,7 +95,10 @@ function generateWorkOSClient(spec: ApiSpec, ctx: EmitterContext): GeneratedFile
89
95
  for (const service of spec.services) {
90
96
  if (coveredServices.has(service.name)) continue;
91
97
  const resolvedName = resolveResourceClassName(service, ctx);
92
- const propName = servicePropertyName(resolvedName);
98
+ // Accessor name uses the un-suffixed service name so that the public API
99
+ // stays `client.organizationMembership` even when the class itself was
100
+ // renamed to `OrganizationMembershipService` to avoid a model collision.
101
+ const propName = servicePropertyName(resolveServiceName(service, ctx));
93
102
  if (existingProps.has(propName)) continue;
94
103
  // Propagate `@deprecated` from the service class to the property so
95
104
  // IDEs surface the strikethrough at `workos.xyz` access sites, not
@@ -1,6 +1,5 @@
1
1
  import type { ApiSpec, EmitterContext, OperationsMap } from '@workos/oagen';
2
- import { resolveMethodName, servicePropertyName } from './naming.js';
3
- import { resolveResourceClassName } from './resources.js';
2
+ import { resolveMethodName, servicePropertyName, resolveServiceName } from './naming.js';
4
3
  import { buildResolvedLookup, lookupResolved } from '../shared/resolved-ops.js';
5
4
 
6
5
  export function buildOperationsMap(spec: ApiSpec, ctx: EmitterContext): OperationsMap {
@@ -8,7 +7,10 @@ export function buildOperationsMap(spec: ApiSpec, ctx: EmitterContext): Operatio
8
7
  const resolvedLookup = buildResolvedLookup(ctx);
9
8
 
10
9
  for (const service of spec.services) {
11
- const serviceProp = servicePropertyName(resolveResourceClassName(service, ctx));
10
+ // Accessor name reflects the un-suffixed service mount target so the
11
+ // manifest matches `client.organizationMembership` (not the suffixed
12
+ // class name used to dodge model collisions).
13
+ const serviceProp = servicePropertyName(resolveServiceName(service, ctx));
12
14
  for (const op of service.operations) {
13
15
  const httpKey = `${op.httpMethod.toUpperCase()} ${op.path}`;
14
16
  const method = resolveMethodName(op, service, ctx);
@@ -2,6 +2,10 @@ import type { Operation, Service, EmitterContext } from '@workos/oagen';
2
2
  import { toPascalCase, toCamelCase, toKebabCase, toSnakeCase } from '@workos/oagen';
3
3
  import { buildResolvedLookup, lookupMethodName } from '../shared/resolved-ops.js';
4
4
  import { stripUrnPrefix } from '../shared/naming-utils.js';
5
+ import {
6
+ buildExportedClassNameSet as buildExportedClassNameSetShared,
7
+ resolveServiceTarget as resolveServiceTargetShared,
8
+ } from '../shared/service-name-collision.js';
5
9
 
6
10
  /** Strip spec-noise suffixes (e.g., "Dto") from an IR name. */
7
11
  export function stripNoiseSuffixes(name: string): string {
@@ -117,6 +121,26 @@ export function resolveServiceName(service: Service, ctx: EmitterContext): strin
117
121
  return resolveClassName(service, ctx);
118
122
  }
119
123
 
124
+ /**
125
+ * Build the set of model + enum class names exported by the SDK. Used to
126
+ * detect collisions with operation-client class names — a colliding service
127
+ * gets a `Service` suffix appended.
128
+ */
129
+ export function buildExportedClassNameSet(ctx: EmitterContext): Set<string> {
130
+ return buildExportedClassNameSetShared(ctx, className);
131
+ }
132
+
133
+ /**
134
+ * Resolve a service's mount-target identifier, appending `Service` on
135
+ * collision with an exported model/enum class. The result feeds `className`
136
+ * and `fileName` so both the `export class` declaration and its file name
137
+ * stay aligned (e.g. `OrganizationMembershipService` /
138
+ * `organization-membership-service.ts`).
139
+ */
140
+ export function resolveServiceTarget(target: string, exportedClasses: Set<string>): string {
141
+ return resolveServiceTargetShared(target, exportedClasses, className);
142
+ }
143
+
120
144
  /**
121
145
  * Build a map from IR service name -> resolved service name.
122
146
  */
@@ -53,6 +53,8 @@ import {
53
53
  resolveInterfaceName,
54
54
  resolveServiceName,
55
55
  wireInterfaceName,
56
+ buildExportedClassNameSet,
57
+ resolveServiceTarget,
56
58
  } from './naming.js';
57
59
  import {
58
60
  docComment,
@@ -102,21 +104,31 @@ export function hasCompatibleConstructor(className: string, ctx: EmitterContext)
102
104
  */
103
105
  export function resolveResourceClassName(service: Service, ctx: EmitterContext): string {
104
106
  const overlayName = resolveServiceName(service, ctx);
107
+ let base: string;
105
108
  if (hasCompatibleConstructor(overlayName, ctx)) {
106
- return overlayName;
107
- }
108
- // Incompatible constructor — fall back to IR name
109
- const irName = toPascalCase(service.name);
110
- if (irName === overlayName) {
111
- return irName + 'Endpoints';
112
- }
113
- return irName;
109
+ base = overlayName;
110
+ } else {
111
+ // Incompatible constructor — fall back to IR name, with `Endpoints` suffix
112
+ // if it would collide with the overlay name.
113
+ const irName = toPascalCase(service.name);
114
+ base = irName === overlayName ? `${irName}Endpoints` : irName;
115
+ }
116
+ // Cross-language `Service` suffix when the chosen class name would shadow
117
+ // an exported model/enum (e.g. tag `OrganizationMembership` + schema
118
+ // `OrganizationMembership`).
119
+ return resolveServiceTarget(base, buildExportedClassNameSet(ctx));
114
120
  }
115
121
 
116
122
  export function resolveResourceDir(service: Service, ctx: EmitterContext): string {
117
123
  const resolvedName = resolveResourceClassName(service, ctx);
118
124
  if (resolvedName === 'WebhooksEndpoints') return 'webhooks';
119
- return resolveServiceDir(resolvedName);
125
+ // Drop the collision-`Service` suffix when picking the directory so the
126
+ // resource and its model-interfaces share a folder (e.g.
127
+ // `organization-membership/` houses both `OrganizationMembershipService`
128
+ // and `OrganizationMembership`'s interface files).
129
+ const overlayName = resolveServiceName(service, ctx);
130
+ const dirBase = resolvedName === `${overlayName}Service` ? overlayName : resolvedName;
131
+ return resolveServiceDir(dirBase);
120
132
  }
121
133
 
122
134
  /** Standard pagination query params handled by PaginationOptions — not imported individually. */
package/src/node/tests.ts CHANGED
@@ -11,6 +11,7 @@ import {
11
11
  servicePropertyName,
12
12
  resolveMethodName,
13
13
  resolveInterfaceName,
14
+ resolveServiceName,
14
15
  wireInterfaceName,
15
16
  } from './naming.js';
16
17
  import { generateFixtures } from './fixtures.js';
@@ -142,7 +143,9 @@ function generateServiceTest(
142
143
  const resolvedName = resolveResourceClassName(service, ctx);
143
144
  const serviceDir = resolveResourceDir(service, ctx);
144
145
  const serviceClass = resolvedName;
145
- const serviceProp = mountAccessors?.get(service.name) ?? servicePropertyName(resolvedName);
146
+ // Accessor stays un-suffixed so `client.organizationMembership` resolves even
147
+ // when the class was renamed to dodge a model-name collision.
148
+ const serviceProp = mountAccessors?.get(service.name) ?? servicePropertyName(resolveServiceName(service, ctx));
146
149
  const testPath = `src/${serviceDir}/${fileName(resolvedName)}.spec.ts`;
147
150
 
148
151
  const plans = service.operations.map((op) => ({
package/src/php/client.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { ApiSpec, Service, EmitterContext, GeneratedFile } from '@workos/oagen';
2
2
  import { toPascalCase, toCamelCase } from '@workos/oagen';
3
- import { className, servicePropertyName } from './naming.js';
3
+ import { className, servicePropertyName, buildExportedClassNameSet, resolveServiceTarget } from './naming.js';
4
4
  import { getMountTarget } from '../shared/resolved-ops.js';
5
5
  import { NON_SPEC_SERVICES } from '../shared/non-spec-services.js';
6
6
 
@@ -61,11 +61,12 @@ export function buildServiceAccessPaths(services: Service[], ctx: EmitterContext
61
61
 
62
62
  function deduplicateByMount(services: Service[], ctx: EmitterContext): { name: string; propName: string }[] {
63
63
  const seen = new Map<string, { name: string; propName: string }>();
64
+ const exportedClasses = buildExportedClassNameSet(ctx);
64
65
  for (const service of services) {
65
66
  const target = getMountTarget(service, ctx);
66
67
  if (!seen.has(target)) {
67
68
  seen.set(target, {
68
- name: className(target),
69
+ name: className(resolveServiceTarget(target, exportedClasses)),
69
70
  propName: servicePropertyName(target),
70
71
  });
71
72
  }
package/src/php/naming.ts CHANGED
@@ -2,6 +2,10 @@ import type { Service, Operation, EmitterContext, Enum } from '@workos/oagen';
2
2
  import { toPascalCase, toCamelCase, toSnakeCase } from '@workos/oagen';
3
3
  import { buildResolvedLookup, lookupMethodName } from '../shared/resolved-ops.js';
4
4
  import { stripUrnPrefix, applyAcronymFixes } from '../shared/naming-utils.js';
5
+ import {
6
+ buildExportedClassNameSet as buildExportedClassNameSetShared,
7
+ resolveServiceTarget as resolveServiceTargetShared,
8
+ } from '../shared/service-name-collision.js';
5
9
 
6
10
  /** Namespace grouping result (shared with client.ts). */
7
11
  export interface NamespaceGroup {
@@ -90,6 +94,24 @@ export function className(name: string): string {
90
94
  return result;
91
95
  }
92
96
 
97
+ /**
98
+ * Build the set of model + enum class names exported by the SDK. Used to
99
+ * detect collisions with operation-client class names — a colliding service
100
+ * gets a `Service` suffix appended.
101
+ */
102
+ export function buildExportedClassNameSet(ctx: EmitterContext): Set<string> {
103
+ return buildExportedClassNameSetShared(ctx, className);
104
+ }
105
+
106
+ /**
107
+ * Resolve a service's mount-target identifier, appending `Service` on
108
+ * collision with an exported model/enum class. Used in `\Service\…` files
109
+ * to avoid `use WorkOS\Resource\X; class X` PHP fatal errors.
110
+ */
111
+ export function resolveServiceTarget(target: string, exportedClasses: Set<string>): string {
112
+ return resolveServiceTargetShared(target, exportedClasses, className);
113
+ }
114
+
93
115
  /** PascalCase file name (without extension) — PSR-4 convention. */
94
116
  export function fileName(name: string): string {
95
117
  return className(name);
@@ -1,7 +1,7 @@
1
1
  import type { Service, Operation, Model, EmitterContext, GeneratedFile, ResolvedOperation } from '@workos/oagen';
2
2
  import { planOperation, toCamelCase, toPascalCase } from '@workos/oagen';
3
3
  import { mapTypeRef, mapTypeRefForPHPDoc } from './type-map.js';
4
- import { className, fieldName, resolveMethodName } from './naming.js';
4
+ import { className, fieldName, resolveMethodName, buildExportedClassNameSet, resolveServiceTarget } from './naming.js';
5
5
  import { isListWrapperModel } from './models.js';
6
6
  import {
7
7
  groupByMount,
@@ -43,9 +43,10 @@ export function generateResources(services: Service[], ctx: EmitterContext): Gen
43
43
  ? [...mountGroups].map(([name, group]) => ({ name, operations: group.operations }))
44
44
  : services.map((s) => ({ name: className(s.name), operations: s.operations }));
45
45
 
46
+ const exportedClasses = buildExportedClassNameSet(ctx);
46
47
  for (const { name: mountName, operations } of entries) {
47
48
  if (operations.length === 0) continue;
48
- const resourceName = className(mountName);
49
+ const resourceName = className(resolveServiceTarget(mountName, exportedClasses));
49
50
  const mergedService: Service = { name: mountName, operations };
50
51
  const lines: string[] = [];
51
52
 
@@ -1,6 +1,14 @@
1
1
  import type { ApiSpec, EmitterContext, GeneratedFile, Service } from '@workos/oagen';
2
2
  import { toPascalCase } from '@workos/oagen';
3
- import { className, resolveServiceDir, servicePropertyName, buildMountDirMap, dirToModule } from './naming.js';
3
+ import {
4
+ className,
5
+ resolveServiceDir,
6
+ servicePropertyName,
7
+ buildMountDirMap,
8
+ dirToModule,
9
+ buildExportedClassNameSet,
10
+ resolveServiceTarget,
11
+ } from './naming.js';
4
12
  import { resolveResourceClassName, collectParameterGroupClassNames } from './resources.js';
5
13
  import { getMountTarget, groupByMount } from '../shared/resolved-ops.js';
6
14
  import { NON_SPEC_SERVICES } from '../shared/non-spec-services.js';
@@ -149,9 +157,10 @@ function generateWorkOSClient(spec: ApiSpec, ctx: EmitterContext): GeneratedFile
149
157
 
150
158
  // Import resource classes (both sync and async)
151
159
  const serviceDirMap = buildMountDirMap(ctx);
160
+ const exportedClasses = buildExportedClassNameSet(ctx);
152
161
  for (const service of topLevelServices) {
153
162
  const resolvedName = resolveResourceClassName(service, ctx);
154
- const clsName = className(resolvedName);
163
+ const clsName = className(resolveServiceTarget(resolvedName, exportedClasses));
155
164
  const dirName = serviceDirMap.get(service.name) ?? resolveServiceDir(resolvedName);
156
165
  const importLine = `from .${dirToModule(dirName)}._resource import ${clsName}, Async${clsName}`;
157
166
  if (importLine.length > 88) {
@@ -185,7 +194,7 @@ function generateWorkOSClient(spec: ApiSpec, ctx: EmitterContext): GeneratedFile
185
194
  const generatedProps = new Set<string>();
186
195
  for (const service of topLevelServices) {
187
196
  const resolvedName = resolveResourceClassName(service, ctx);
188
- const clsName = className(resolvedName);
197
+ const clsName = className(resolveServiceTarget(resolvedName, exportedClasses));
189
198
  const prop = servicePropertyName(resolvedName);
190
199
  const readable = clsName.replace(/([a-z])([A-Z])/g, '$1 $2').replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2');
191
200
  lines.push('');
@@ -208,7 +217,7 @@ function generateWorkOSClient(spec: ApiSpec, ctx: EmitterContext): GeneratedFile
208
217
  const asyncGeneratedProps = new Set<string>();
209
218
  for (const service of topLevelServices) {
210
219
  const resolvedName = resolveResourceClassName(service, ctx);
211
- const clsName = className(resolvedName);
220
+ const clsName = className(resolveServiceTarget(resolvedName, exportedClasses));
212
221
  const prop = servicePropertyName(resolvedName);
213
222
  const readable = clsName.replace(/([a-z])([A-Z])/g, '$1 $2').replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2');
214
223
  lines.push('');
@@ -255,6 +264,7 @@ function generateServiceInits(spec: ApiSpec, ctx: EmitterContext): GeneratedFile
255
264
  const files: GeneratedFile[] = [];
256
265
  const topLevel = deduplicateByMount(spec.services, ctx);
257
266
  const serviceDirMap = buildMountDirMap(ctx);
267
+ const exportedClasses = buildExportedClassNameSet(ctx);
258
268
 
259
269
  // Build a map from mount target -> operations so we can discover parameter
260
270
  // group dataclasses that need re-exporting from __init__.py.
@@ -262,6 +272,7 @@ function generateServiceInits(spec: ApiSpec, ctx: EmitterContext): GeneratedFile
262
272
 
263
273
  for (const service of topLevel) {
264
274
  const resolvedName = resolveResourceClassName(service, ctx);
275
+ const clsName = className(resolveServiceTarget(resolvedName, exportedClasses));
265
276
  const dirName = serviceDirMap.get(service.name) ?? resolveServiceDir(resolvedName);
266
277
  const lines: string[] = [];
267
278
 
@@ -274,7 +285,7 @@ function generateServiceInits(spec: ApiSpec, ctx: EmitterContext): GeneratedFile
274
285
  // public re-exports. Otherwise consumers importing
275
286
  // `from workos.user_management import RoleSingle` get a private-import
276
287
  // warning under strict mode. Models barrel uses the same convention.
277
- const resourceImports = [resolvedName, `Async${resolvedName}`, ...groupClassNames];
288
+ const resourceImports = [clsName, `Async${clsName}`, ...groupClassNames];
278
289
  const aliasedImports = resourceImports.map((n) => `${n} as ${n}`);
279
290
  lines.push(`from ._resource import ${aliasedImports.join(', ')}`);
280
291
  lines.push('from .models import *');
@@ -2,6 +2,10 @@ import type { Operation, Service, EmitterContext } from '@workos/oagen';
2
2
  import { toPascalCase, toSnakeCase } from '@workos/oagen';
3
3
  import { buildResolvedLookup, lookupMethodName, getMountTarget } from '../shared/resolved-ops.js';
4
4
  import { stripUrnPrefix, applyAcronymFixes } from '../shared/naming-utils.js';
5
+ import {
6
+ buildExportedClassNameSet as buildExportedClassNameSetShared,
7
+ resolveServiceTarget as resolveServiceTargetShared,
8
+ } from '../shared/service-name-collision.js';
5
9
 
6
10
  /**
7
11
  * Python class names that collide with builtins or typing imports.
@@ -136,6 +140,27 @@ export function resolveMethodName(op: Operation, _service: Service, ctx: Emitter
136
140
  return toSnakeCase(op.name);
137
141
  }
138
142
 
143
+ /**
144
+ * Build the set of model + enum class names exported by the SDK. Used to
145
+ * detect collisions with operation-client class names — a colliding service
146
+ * gets a `Service` suffix appended.
147
+ */
148
+ export function buildExportedClassNameSet(ctx: EmitterContext): Set<string> {
149
+ return buildExportedClassNameSetShared(ctx, className);
150
+ }
151
+
152
+ /**
153
+ * Resolve a service's mount-target identifier, appending `Service` on
154
+ * collision with an exported model/enum class. Feeds `className`/`fileName`
155
+ * so the class declaration, file, and any qualified references stay aligned.
156
+ *
157
+ * Accessor names (`servicePropertyName`) intentionally use the RAW target —
158
+ * `client.organization_membership` reads better than the suffixed form.
159
+ */
160
+ export function resolveServiceTarget(target: string, exportedClasses: Set<string>): string {
161
+ return resolveServiceTargetShared(target, exportedClasses, className);
162
+ }
163
+
139
164
  /** Resolve the SDK class name for a service, using resolved operations' mountOn. */
140
165
  export function resolveClassName(service: Service, ctx: EmitterContext): string {
141
166
  // Use resolved ops mountOn as canonical class name (flat pattern like PHP)
@@ -23,6 +23,8 @@ import {
23
23
  buildMountDirMap,
24
24
  dirToModule,
25
25
  relativeImportPrefix,
26
+ buildExportedClassNameSet,
27
+ resolveServiceTarget,
26
28
  } from './naming.js';
27
29
  import {
28
30
  buildResolvedLookup,
@@ -1012,10 +1014,11 @@ export function generateResources(services: Service[], ctx: EmitterContext): Gen
1012
1014
  ? [...mountGroups].map(([name, group]) => ({ name, operations: group.operations }))
1013
1015
  : services.map((s) => ({ name: resolveClassName(s, ctx), operations: s.operations }));
1014
1016
 
1017
+ const exportedClasses = buildExportedClassNameSet(ctx);
1015
1018
  for (const { name: mountName, operations: allOperations } of entries) {
1016
1019
  if (allOperations.length === 0) continue;
1017
1020
  const dirName = moduleName(mountName);
1018
- const resourceClassName = className(mountName);
1021
+ const resourceClassName = className(resolveServiceTarget(mountName, exportedClasses));
1019
1022
  const importPrefix = relativeImportPrefix(dirName);
1020
1023
 
1021
1024
  const lines: string[] = [];
@@ -1,6 +1,14 @@
1
1
  import type { ApiSpec, EmitterContext, GeneratedFile, Service, Model, Enum } from '@workos/oagen';
2
2
  import { assignModelsToServices } from '@workos/oagen';
3
- import { servicePropertyName, resolveClassName, className, fileName, buildMountDirMap } from './naming.js';
3
+ import {
4
+ servicePropertyName,
5
+ resolveClassName,
6
+ className,
7
+ fileName,
8
+ buildMountDirMap,
9
+ buildExportedClassNameSet,
10
+ resolveServiceTarget,
11
+ } from './naming.js';
4
12
  import { classifyUnassignedModel } from './models.js';
5
13
  import { getMountTarget } from '../shared/resolved-ops.js';
6
14
  import { isListWrapperModel, isListMetadataModel } from '../shared/model-utils.js';
@@ -89,8 +97,12 @@ function buildInflectionMap(spec: ApiSpec, ctx: EmitterContext): Map<string, str
89
97
 
90
98
  inflections.set('workos', 'WorkOS');
91
99
 
100
+ const exportedClasses = buildExportedClassNameSet(ctx);
92
101
  for (const service of buildTopLevelServices(spec, ctx)) {
93
- const target = getMountTarget(service, ctx) || resolveClassName(service, ctx);
102
+ const target = resolveServiceTarget(
103
+ getMountTarget(service, ctx) || resolveClassName(service, ctx),
104
+ exportedClasses,
105
+ );
94
106
  const cls = className(target);
95
107
  const file = fileName(target);
96
108
  if (rubyCamelize(file) !== cls) inflections.set(file, cls);
@@ -203,10 +215,11 @@ function generateClientClass(spec: ApiSpec, ctx: EmitterContext): GeneratedFile
203
215
  lines.push(' class Client < BaseClient');
204
216
 
205
217
  const topLevelServices = buildTopLevelServices(spec, ctx);
218
+ const exportedClasses = buildExportedClassNameSet(ctx);
206
219
  for (const service of topLevelServices) {
207
- const target = getMountTarget(service, ctx) || resolveClassName(service, ctx);
208
- const cls = className(target);
209
- const prop = servicePropertyName(target);
220
+ const rawTarget = getMountTarget(service, ctx) || resolveClassName(service, ctx);
221
+ const cls = className(resolveServiceTarget(rawTarget, exportedClasses));
222
+ const prop = servicePropertyName(rawTarget);
210
223
  lines.push('');
211
224
  lines.push(` def ${prop}`);
212
225
  lines.push(` @${prop} ||= WorkOS::${cls}.new(self)`);
@@ -7,9 +7,14 @@ import { servicePropertyName } from './naming.js';
7
7
  * Uses each resolved operation's actual mountOn (not the service default) so
8
8
  * operations remounted via operationHints land on the correct service prop.
9
9
  * Split operations emit one entry per wrapper (keyed by wrapper name + variant).
10
+ *
11
+ * The accessor (`service` field) uses the raw mountOn — accessor names stay
12
+ * unsuffixed even when the underlying service class gets a `Service` suffix
13
+ * on collision.
10
14
  */
11
15
  export function buildOperationsMap(spec: ApiSpec, ctx: EmitterContext): OperationsMap {
12
16
  void spec;
17
+ void ctx;
13
18
  const manifest: OperationsMap = {};
14
19
 
15
20
  for (const r of ctx.resolvedOperations ?? []) {