@workos/oagen-emitters 0.14.4 → 0.15.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 (43) hide show
  1. package/.release-please-manifest.json +1 -1
  2. package/CHANGELOG.md +12 -0
  3. package/dist/index.d.mts.map +1 -1
  4. package/dist/index.mjs +1 -1
  5. package/dist/{plugin-BGVaMGqe.mjs → plugin-CO4RFgAW.mjs} +959 -251
  6. package/dist/plugin-CO4RFgAW.mjs.map +1 -0
  7. package/dist/plugin.mjs +1 -1
  8. package/package.json +7 -7
  9. package/renovate.json +1 -61
  10. package/src/go/client.ts +1 -1
  11. package/src/go/enums.ts +77 -0
  12. package/src/kotlin/enums.ts +11 -4
  13. package/src/node/client.ts +119 -2
  14. package/src/node/discriminated-models.ts +8 -0
  15. package/src/node/field-plan.ts +64 -8
  16. package/src/node/index.ts +59 -3
  17. package/src/node/models.ts +73 -30
  18. package/src/node/naming.ts +14 -1
  19. package/src/node/node-overrides.ts +4 -37
  20. package/src/node/options.ts +29 -1
  21. package/src/node/resources.ts +533 -83
  22. package/src/node/tests.ts +108 -7
  23. package/src/php/fixtures.ts +4 -1
  24. package/src/php/models.ts +3 -1
  25. package/src/php/resources.ts +40 -11
  26. package/src/php/tests.ts +22 -12
  27. package/src/python/client.ts +0 -8
  28. package/src/python/enums.ts +41 -15
  29. package/src/python/fixtures.ts +23 -7
  30. package/src/python/models.ts +26 -5
  31. package/src/python/resources.ts +71 -3
  32. package/src/python/tests.ts +70 -12
  33. package/src/python/wrappers.ts +25 -4
  34. package/src/ruby/client.ts +0 -1
  35. package/src/rust/resources.ts +10 -7
  36. package/src/shared/non-spec-services.ts +0 -5
  37. package/test/go/enums.test.ts +24 -0
  38. package/test/node/resources.test.ts +11 -1
  39. package/test/node/tests.test.ts +3 -3
  40. package/test/php/client.test.ts +0 -1
  41. package/test/php/resources.test.ts +50 -0
  42. package/test/rust/resources.test.ts +9 -0
  43. package/dist/plugin-BGVaMGqe.mjs.map +0 -1
@@ -363,9 +363,16 @@ function emitMethodSignature(
363
363
  : className(resolvedItem);
364
364
  returnType = `${pageType}[${itemTypeName}]`;
365
365
  } else if (isArrayResponse) {
366
- returnType = `List[${className(plan.responseModelName!)}]`;
366
+ const arrayItemModel = ctx.spec.models.find((m) => m.name === plan.responseModelName);
367
+ const arrayItemTypeName = (arrayItemModel as any)?.discriminator
368
+ ? className(plan.responseModelName!) + 'Variant'
369
+ : className(plan.responseModelName!);
370
+ returnType = `List[${arrayItemTypeName}]`;
367
371
  } else if (plan.responseModelName) {
368
- returnType = className(plan.responseModelName);
372
+ const singleResponseModel = ctx.spec.models.find((m) => m.name === plan.responseModelName);
373
+ returnType = (singleResponseModel as any)?.discriminator
374
+ ? className(plan.responseModelName) + 'Variant'
375
+ : className(plan.responseModelName);
369
376
  } else {
370
377
  returnType = 'None';
371
378
  }
@@ -772,6 +779,10 @@ function emitMethodBody(
772
779
  lines.push(' )');
773
780
  } else if (plan.hasBody && op.requestBody) {
774
781
  const responseModel = plan.responseModelName ? className(plan.responseModelName) : 'None';
782
+ const bodyResponseModelObj = plan.responseModelName
783
+ ? ctx.spec.models.find((m) => m.name === plan.responseModelName)
784
+ : null;
785
+ const isBodyResponseDiscriminator = !!(bodyResponseModelObj as any)?.discriminator;
775
786
  // Build body dict
776
787
  const bodyGroupedParams = collectGroupedParamNames(op);
777
788
  const bodyModel = ctx.spec.models.find((m) => op.requestBody?.kind === 'model' && m.name === op.requestBody.name);
@@ -847,6 +858,26 @@ function emitMethodBody(
847
858
  lines.push(' request_options=request_options,');
848
859
  lines.push(' )');
849
860
  lines.push(` return [${itemModel}.from_dict(cast(Dict[str, Any], item)) for item in raw]`);
861
+ } else if (isBodyResponseDiscriminator && responseModel !== 'None') {
862
+ const variantType = responseModel + 'Variant';
863
+ lines.push(` return cast(`);
864
+ lines.push(` ${variantType},`);
865
+ lines.push(` ${awaitPrefix}self._client.request(`);
866
+ lines.push(` method="${httpMethod}",`);
867
+ lines.push(` path=${pathStr},`);
868
+ lines.push(` body=${bodyVarName},`);
869
+ if (bodyHasParams) {
870
+ lines.push(' params=params,');
871
+ }
872
+ lines.push(
873
+ ` model=${responseModel}, # type: ignore[arg-type] # dispatcher; request only calls from_dict`,
874
+ );
875
+ if (plan.isIdempotentPost) {
876
+ lines.push(' idempotency_key=idempotency_key,');
877
+ }
878
+ lines.push(' request_options=request_options,');
879
+ lines.push(' )');
880
+ lines.push(' )');
850
881
  } else {
851
882
  const bodyReturnPrefix = responseModel !== 'None' ? 'return ' : '';
852
883
  lines.push(` ${bodyReturnPrefix}${awaitPrefix}self._client.request(`);
@@ -868,6 +899,10 @@ function emitMethodBody(
868
899
  } else {
869
900
  // GET or similar with query params
870
901
  const responseModel = plan.responseModelName ? className(plan.responseModelName) : 'None';
902
+ const getResponseModelObj = plan.responseModelName
903
+ ? ctx.spec.models.find((m) => m.name === plan.responseModelName)
904
+ : null;
905
+ const isGetResponseDiscriminator = !!(getResponseModelObj as any)?.discriminator;
871
906
  const getGroupedParams = collectGroupedParamNames(op);
872
907
  const hasGroups = (op.parameterGroups?.length ?? 0) > 0;
873
908
  const visibleQueryParams = op.queryParams.filter((p) => !hiddenParams.has(p.name) && !getGroupedParams.has(p.name));
@@ -937,6 +972,22 @@ function emitMethodBody(
937
972
  lines.push(' request_options=request_options,');
938
973
  lines.push(' )');
939
974
  lines.push(` return [${itemModel}.from_dict(cast(Dict[str, Any], item)) for item in raw]`);
975
+ } else if (isGetResponseDiscriminator && responseModel !== 'None') {
976
+ const variantType = responseModel + 'Variant';
977
+ lines.push(` return cast(`);
978
+ lines.push(` ${variantType},`);
979
+ lines.push(` ${awaitPrefix}self._client.request(`);
980
+ lines.push(` method="${httpMethod}",`);
981
+ lines.push(` path=${pathStr},`);
982
+ if (emittedParams) {
983
+ lines.push(' params=params,');
984
+ }
985
+ lines.push(
986
+ ` model=${responseModel}, # type: ignore[arg-type] # dispatcher; request only calls from_dict`,
987
+ );
988
+ lines.push(' request_options=request_options,');
989
+ lines.push(' )');
990
+ lines.push(' )');
940
991
  } else {
941
992
  const returnPrefix = responseModel !== 'None' ? 'return ' : '';
942
993
  lines.push(` ${returnPrefix}${awaitPrefix}self._client.request(`);
@@ -1061,6 +1112,13 @@ export function generateResources(services: Service[], ctx: EmitterContext): Gen
1061
1112
  if (!listWrapperNames.has(plan.responseModelName) || !plan.isPaginated) {
1062
1113
  modelImports.add(plan.responseModelName);
1063
1114
  }
1115
+ // For discriminator (dispatcher) models also import the variant union type alias
1116
+ if (!plan.isPaginated) {
1117
+ const responseModelObj = ctx.spec.models.find((m) => m.name === plan.responseModelName);
1118
+ if ((responseModelObj as any)?.discriminator) {
1119
+ modelImports.add(plan.responseModelName + 'Variant');
1120
+ }
1121
+ }
1064
1122
  }
1065
1123
  if (op.requestBody?.kind === 'model') {
1066
1124
  const requestBodyRef = op.requestBody;
@@ -1161,13 +1219,23 @@ export function generateResources(services: Service[], ctx: EmitterContext): Gen
1161
1219
  // Deduplicate: skip cross-service imports for models already available locally
1162
1220
  const localSet = new Set(localModels);
1163
1221
 
1222
+ // Build a mapping from variant type aliases to their dispatcher's file name
1223
+ // (e.g. ConnectApplicationVariant -> connect_application, not connect_application_variant)
1224
+ const variantToFile = new Map<string, string>();
1225
+ for (const model of ctx.spec.models) {
1226
+ if ((model as any).discriminator) {
1227
+ variantToFile.set(model.name + 'Variant', fileName(model.name));
1228
+ }
1229
+ }
1230
+
1164
1231
  if (localModels.length > 0) {
1165
1232
  lines.push(`from .models import ${localModels.map((n) => className(n)).join(', ')}`);
1166
1233
  }
1167
1234
  for (const [csDir, names] of [...crossServiceModels].sort()) {
1168
1235
  const unique = names.filter((n) => !localSet.has(n));
1169
1236
  for (const n of unique) {
1170
- lines.push(`from ${ctx.namespace}.${dirToModule(csDir)}.models.${fileName(n)} import ${className(n)}`);
1237
+ const filePath = variantToFile.get(n) ?? fileName(n);
1238
+ lines.push(`from ${ctx.namespace}.${dirToModule(csDir)}.models.${filePath} import ${className(n)}`);
1171
1239
  }
1172
1240
  }
1173
1241
 
@@ -167,7 +167,27 @@ function generateServiceTest(
167
167
  const enumImports = new Set<string>();
168
168
  for (const op of service.operations) {
169
169
  const plan = planOperation(op);
170
- if (plan.responseModelName) modelImports.add(plan.responseModelName);
170
+ if (plan.responseModelName) {
171
+ modelImports.add(plan.responseModelName);
172
+ // For non-paginated discriminated union responses, import the resolved variant class
173
+ if (!plan.isPaginated) {
174
+ const resolvedVariantClass = resolvePaginatedItemClass(plan.responseModelName, spec);
175
+ if (resolvedVariantClass && resolvedVariantClass !== className(plan.responseModelName)) {
176
+ const responseModel = spec.models.find((m) => m.name === plan.responseModelName);
177
+ const disc =
178
+ responseModel &&
179
+ ((responseModel as any).discriminator as { property: string; mapping: Record<string, string> } | undefined);
180
+ if (disc) {
181
+ for (const variantName of Object.values(disc.mapping)) {
182
+ if (className(variantName) === resolvedVariantClass) {
183
+ modelImports.add(variantName);
184
+ break;
185
+ }
186
+ }
187
+ }
188
+ }
189
+ }
190
+ }
171
191
  if (op.pagination?.itemType.kind === 'model') {
172
192
  modelImports.add(op.pagination.itemType.name);
173
193
  // Unwrap list wrapper to find the inner item model (may be a discriminated union)
@@ -220,11 +240,13 @@ function generateServiceTest(
220
240
  }
221
241
  }
222
242
 
223
- // Filter out list wrapper models
243
+ // Filter out list wrapper models, but keep non-paginated response wrappers
244
+ const nonPaginatedRefs = collectNonPaginatedResponseModelNames(spec.services);
224
245
  const actualImports = [...modelImports].filter((name) => {
225
246
  const model = spec.models.find((m) => m.name === name);
226
247
  if (!model) return true;
227
- return !isListWrapperModel(model);
248
+ if (isListWrapperModel(model) && !nonPaginatedRefs.has(name)) return false;
249
+ return true;
228
250
  });
229
251
 
230
252
  // Group imports by their actual service directory (models may live in different services)
@@ -414,16 +436,33 @@ function generateServiceTest(
414
436
  const fixtureName = `${fileName(modelName)}.json`;
415
437
  const modelClass = className(modelName);
416
438
 
439
+ // For discriminated union responses, resolve to the concrete variant class
440
+ const resolvedClass = resolvePaginatedItemClass(modelName, spec) ?? modelClass;
441
+ // Pick assertable fields from the resolved variant, not the dispatcher
442
+ const resolvedModelName =
443
+ resolvedClass !== modelClass
444
+ ? (() => {
445
+ const responseModel = spec.models.find((m) => m.name === modelName);
446
+ const disc = (responseModel as any)?.discriminator as { mapping: Record<string, string> } | undefined;
447
+ if (disc) {
448
+ for (const variantName of Object.values(disc.mapping)) {
449
+ if (className(variantName) === resolvedClass) return variantName;
450
+ }
451
+ }
452
+ return modelName;
453
+ })()
454
+ : modelName;
455
+
417
456
  lines.push(` def test_${method}(self, workos, httpx_mock):`);
418
457
  lines.push(` httpx_mock.add_response(`);
419
458
  lines.push(` json=load_fixture("${fixtureName}"),`);
420
459
  lines.push(' )');
421
460
  const args = buildTestArgs(op, spec, hiddenParams);
422
461
  lines.push(` result = workos.${propName}.${method}(${args})`);
423
- lines.push(` assert isinstance(result, ${modelClass})`);
462
+ lines.push(` assert isinstance(result, ${resolvedClass})`);
424
463
 
425
464
  // Field-value assertions: verify at least 2 scalar fields from fixture
426
- const assertFields = pickAssertableFields(modelName, spec);
465
+ const assertFields = pickAssertableFields(resolvedModelName, spec);
427
466
  for (const af of assertFields) {
428
467
  const op_ = af.isBool ? 'is' : '==';
429
468
  lines.push(` assert result.${af.field} ${op_} ${af.value}`);
@@ -706,12 +745,29 @@ function generateServiceTest(
706
745
  const modelName = plan.responseModelName;
707
746
  const fixtureName = `${fileName(modelName)}.json`;
708
747
  const modelClass = className(modelName);
748
+
749
+ // For discriminated union responses, resolve to the concrete variant class
750
+ const asyncResolvedClass = resolvePaginatedItemClass(modelName, spec) ?? modelClass;
751
+ const asyncResolvedModelName =
752
+ asyncResolvedClass !== modelClass
753
+ ? (() => {
754
+ const responseModel = spec.models.find((m) => m.name === modelName);
755
+ const disc = (responseModel as any)?.discriminator as { mapping: Record<string, string> } | undefined;
756
+ if (disc) {
757
+ for (const variantName of Object.values(disc.mapping)) {
758
+ if (className(variantName) === asyncResolvedClass) return variantName;
759
+ }
760
+ }
761
+ return modelName;
762
+ })()
763
+ : modelName;
764
+
709
765
  pushAsyncTestDef(lines, ` async def test_${method}(self, async_workos, httpx_mock):`);
710
766
  lines.push(` httpx_mock.add_response(json=load_fixture("${fixtureName}"))`);
711
767
  lines.push(` result = await async_workos.${propName}.${method}(${asyncArgs})`);
712
- lines.push(` assert isinstance(result, ${modelClass})`);
768
+ lines.push(` assert isinstance(result, ${asyncResolvedClass})`);
713
769
  // Field-value assertions
714
- const assertFields = pickAssertableFields(modelName, spec);
770
+ const assertFields = pickAssertableFields(asyncResolvedModelName, spec);
715
771
  for (const af of assertFields) {
716
772
  const op_ = af.isBool ? 'is' : '==';
717
773
  lines.push(` assert result.${af.field} ${op_} ${af.value}`);
@@ -889,7 +945,9 @@ function emitWrapperTests(
889
945
  for (const wrapper of r.wrappers) {
890
946
  const method = wrapper.name;
891
947
  const wrapperParams = resolveWrapperParams(wrapper, ctx);
892
- const responseType = wrapper.responseModelName ? className(wrapper.responseModelName) : null;
948
+ const resolvedResponseClass = wrapper.responseModelName
949
+ ? (resolvePaginatedItemClass(wrapper.responseModelName, spec) ?? className(wrapper.responseModelName))
950
+ : null;
893
951
  const fixtureName = wrapper.responseModelName ? `${fileName(wrapper.responseModelName)}.json` : null;
894
952
 
895
953
  // Build test args for required wrapper params
@@ -908,8 +966,8 @@ function emitWrapperTests(
908
966
  if (fixtureName) {
909
967
  lines.push(` httpx_mock.add_response(json=load_fixture("${fixtureName}"))`);
910
968
  lines.push(` result = await async_workos.${propName}.${method}(${args})`);
911
- if (responseType) {
912
- lines.push(` assert isinstance(result, ${responseType})`);
969
+ if (resolvedResponseClass) {
970
+ lines.push(` assert isinstance(result, ${resolvedResponseClass})`);
913
971
  }
914
972
  } else {
915
973
  lines.push(' httpx_mock.add_response(json={})');
@@ -920,8 +978,8 @@ function emitWrapperTests(
920
978
  if (fixtureName) {
921
979
  lines.push(` httpx_mock.add_response(json=load_fixture("${fixtureName}"))`);
922
980
  lines.push(` result = workos.${propName}.${method}(${args})`);
923
- if (responseType) {
924
- lines.push(` assert isinstance(result, ${responseType})`);
981
+ if (resolvedResponseClass) {
982
+ lines.push(` assert isinstance(result, ${resolvedResponseClass})`);
925
983
  }
926
984
  } else {
927
985
  lines.push(' httpx_mock.add_response(json={})');
@@ -1,4 +1,4 @@
1
- import type { EmitterContext, ResolvedOperation, ResolvedWrapper } from '@workos/oagen';
1
+ import type { EmitterContext, ResolvedOperation, ResolvedWrapper, Model } from '@workos/oagen';
2
2
  import { toSnakeCase } from '@workos/oagen';
3
3
  import { className, fieldName } from './naming.js';
4
4
  import { resolveWrapperParams, formatWrapperDescription } from '../shared/wrapper-utils.js';
@@ -75,8 +75,15 @@ function emitWrapperMethod(
75
75
 
76
76
  lines.push(' request_options: Optional[RequestOptions] = None,');
77
77
 
78
- // Return type
79
- const responseType = wrapper.responseModelName ? className(wrapper.responseModelName) : 'None';
78
+ // Return type — use Variant type for discriminated union responses
79
+ const isDiscriminatorResponse = wrapper.responseModelName
80
+ ? !!(ctx.spec.models.find((m: Model) => m.name === wrapper.responseModelName) as any)?.discriminator
81
+ : false;
82
+ const responseType = wrapper.responseModelName
83
+ ? isDiscriminatorResponse
84
+ ? className(wrapper.responseModelName) + 'Variant'
85
+ : className(wrapper.responseModelName)
86
+ : 'None';
80
87
 
81
88
  lines.push(` ) -> ${responseType}:`);
82
89
 
@@ -122,7 +129,21 @@ function emitWrapperMethod(
122
129
  const awaitPrefix = isAsync ? 'await ' : '';
123
130
  lines.push('');
124
131
 
125
- if (wrapper.responseModelName) {
132
+ if (wrapper.responseModelName && isDiscriminatorResponse) {
133
+ const variantType = className(wrapper.responseModelName) + 'Variant';
134
+ lines.push(` return cast(`);
135
+ lines.push(` ${variantType},`);
136
+ lines.push(` ${awaitPrefix}self._client.request(`);
137
+ lines.push(` method="${op.httpMethod.toUpperCase()}",`);
138
+ lines.push(` path=${pathExpr},`);
139
+ lines.push(' body=body,');
140
+ lines.push(
141
+ ` model=${className(wrapper.responseModelName)}, # type: ignore[arg-type] # dispatcher; request only calls from_dict`,
142
+ );
143
+ lines.push(' request_options=request_options,');
144
+ lines.push(' )');
145
+ lines.push(' )');
146
+ } else if (wrapper.responseModelName) {
126
147
  lines.push(` return ${awaitPrefix}self._client.request(`);
127
148
  lines.push(` method="${op.httpMethod.toUpperCase()}",`);
128
149
  lines.push(` path=${pathExpr},`);
@@ -27,7 +27,6 @@ import { NON_SPEC_SERVICES } from '../shared/non-spec-services.js';
27
27
  */
28
28
  const NON_SPEC_ACCESSORS: Record<string, { prop: string; className: string; ctorArg: 'self' | '' }> = {
29
29
  passwordless: { prop: 'passwordless', className: 'Passwordless', ctorArg: 'self' },
30
- vault: { prop: 'vault', className: 'Vault', ctorArg: 'self' },
31
30
  actions: { prop: 'actions', className: 'Actions', ctorArg: 'self' },
32
31
  session_manager: { prop: 'session_manager', className: 'SessionManager', ctorArg: 'self' },
33
32
  pkce: { prop: 'pkce', className: 'PKCE', ctorArg: '' },
@@ -355,6 +355,7 @@ function renderParamsStruct(
355
355
  ): string {
356
356
  const bodyRequired = isBodyRequired(op);
357
357
  const hidden = new Set<string>([...Object.keys(resolved.defaults ?? {}), ...(resolved.inferFromClient ?? [])]);
358
+ const materializeSpecDefaults = !resolved.urlBuilder;
358
359
 
359
360
  // Names of params that belong to a parameter group; these are folded into
360
361
  // the enum field and must be omitted from the flat params struct.
@@ -412,9 +413,14 @@ function renderParamsStruct(
412
413
  });
413
414
  if (!p.required && !rust.startsWith('Option<')) rust = makeOptional(rust);
414
415
  rust = applySecretRedaction(rust, p.name);
415
- // Spec-level default materialise it as a Rust expression so
416
- // `Default::default()` and `new(…)` actually produce the documented value.
417
- const defaultExpr = p.default != null ? rustDefaultExpr(p.default, p.type, rust.startsWith('Option<'), ctx) : null;
416
+ // Spec-level defaults on HTTP params are materialized so
417
+ // `Default::default()` and `new(…)` produce the documented value. URL
418
+ // builders keep optional query params omitted unless the caller supplies
419
+ // them, because the query string is the public redirect target.
420
+ const defaultExpr =
421
+ materializeSpecDefaults && p.default != null
422
+ ? rustDefaultExpr(p.default, p.type, rust.startsWith('Option<'), ctx)
423
+ : null;
418
424
  // Field-level documentation derived from the spec.
419
425
  const desc = p.description?.trim();
420
426
  if (desc) {
@@ -1317,10 +1323,7 @@ function renderResourcesBarrel(exports: { module: string; struct: string }[]): s
1317
1323
  unique.sort((a, b) => a.module.localeCompare(b.module));
1318
1324
 
1319
1325
  const lines: string[] = [];
1320
- // Declare modules privately see the matching comment in `models.ts`.
1321
- // `pub mod resources::organization_membership` would collide with the
1322
- // same-named module re-exported via `pub use models::*` in lib.rs.
1323
- for (const { module } of unique) lines.push(`mod ${module};`);
1326
+ for (const { module } of unique) lines.push(`pub mod ${module};`);
1324
1327
  lines.push('');
1325
1328
  for (const { module, struct } of unique) lines.push(`pub use ${module}::${struct};`);
1326
1329
  return lines.join('\n') + '\n';
@@ -42,11 +42,6 @@ export const NON_SPEC_SERVICES: readonly NonSpecService[] = [
42
42
  description: 'Passwordless (magic-link) session endpoints, not yet in the OpenAPI spec.',
43
43
  hasClientAccessor: true,
44
44
  },
45
- {
46
- id: 'vault',
47
- description: 'Vault KV storage, key operations, and client-side AES-GCM encrypt/decrypt.',
48
- hasClientAccessor: true,
49
- },
50
45
  {
51
46
  id: 'webhook_verification',
52
47
  description: 'Webhook signature verification and event deserialization (H01/H02).',
@@ -129,4 +129,28 @@ describe('go/enums', () => {
129
129
  const content = files[0].content;
130
130
  expect(content).toContain('type SSOConnectionType string');
131
131
  });
132
+
133
+ it('generates an events compatibility package from webhook event enum values', () => {
134
+ const enums: Enum[] = [
135
+ {
136
+ name: 'CreateWebhookEndpointEvents',
137
+ values: [
138
+ { name: 'USER_CREATED', value: 'user.created' },
139
+ { name: 'API_KEY_CREATED', value: 'api_key.created' },
140
+ { name: 'DSYNC_USER_CREATED', value: 'dsync.user.created' },
141
+ ],
142
+ },
143
+ ];
144
+
145
+ const files = generateEnums(enums, ctx);
146
+ const compat = files.find((file) => file.path === 'pkg/events/events.go');
147
+
148
+ expect(compat).toBeDefined();
149
+ expect(compat!.content).toContain('package events');
150
+ expect(compat!.content).toContain('type Event string');
151
+ expect(compat!.content).toContain('UserCreated = "user.created"');
152
+ expect(compat!.content).toContain('APIKeyCreated = "api_key.created"');
153
+ expect(compat!.content).toContain('DsyncUserCreated = "dsync.user.created"');
154
+ expect(compat!.content).not.toContain('CreateWebhookEndpointEventsUserCreated');
155
+ });
132
156
  });
@@ -101,7 +101,9 @@ describe('generateResources', () => {
101
101
  expect(resourceFile).toBeDefined();
102
102
  expect(resourceFile!.content).toContain('export class Organizations');
103
103
  expect(resourceFile!.content).toContain('constructor(private readonly workos: WorkOS)');
104
- expect(resourceFile!.content).toContain('async getOrganization(id: string): Promise<Organization>');
104
+ expect(resourceFile!.content).toContain(
105
+ 'async getOrganization(options: GetOrganizationOptions): Promise<Organization>',
106
+ );
105
107
  expect(resourceFile!.content).toContain('deserializeOrganization(data)');
106
108
  });
107
109
 
@@ -154,6 +156,14 @@ describe('generateResources', () => {
154
156
  const ctxWithResolved: EmitterContext = {
155
157
  ...ctx,
156
158
  spec,
159
+ emitterOptions: {
160
+ operationOverrides: {
161
+ 'GET /user_management/organization_memberships/{omId}/groups': {
162
+ methodName: 'list_groups_for_organization_membership',
163
+ mountOn: 'UserManagement',
164
+ },
165
+ },
166
+ },
157
167
  resolvedOperations: [
158
168
  {
159
169
  operation,
@@ -86,7 +86,7 @@ describe('node test generation ownership', () => {
86
86
  }
87
87
  });
88
88
 
89
- it('preserves existing hand-written tests and fixtures for owned services', () => {
89
+ it('regenerates over hand-written tests and fixtures for owned services', () => {
90
90
  const tmpRoot = createTrackedSdkRoot(true);
91
91
  try {
92
92
  const result = nodeEmitter.generateTests!(spec, {
@@ -95,8 +95,8 @@ describe('node test generation ownership', () => {
95
95
  emitterOptions: { ownedServices: ['Groups'], regenerateOwnedTests: true },
96
96
  } as EmitterContext);
97
97
 
98
- expect(result.some((f) => f.path === 'src/groups/groups.spec.ts')).toBe(false);
99
- expect(result.some((f) => f.path === 'src/groups/fixtures/group.json')).toBe(false);
98
+ expect(result.some((f) => f.path === 'src/groups/groups.spec.ts')).toBe(true);
99
+ expect(result.some((f) => f.path === 'src/groups/fixtures/group.json')).toBe(true);
100
100
  } finally {
101
101
  fs.rmSync(tmpRoot, { recursive: true, force: true });
102
102
  }
@@ -86,7 +86,6 @@ describe('generateClient', () => {
86
86
  const result = generateClient(emptySpec, ctx);
87
87
 
88
88
  expect(result[0].content).toContain('public function passwordless(): Passwordless');
89
- expect(result[0].content).toContain('public function vault(): Vault');
90
89
  expect(result[0].content).toContain('public function webhookVerification(): WebhookVerification');
91
90
  expect(result[0].content).toContain('public function actions(): Actions');
92
91
  expect(result[0].content).toContain('public function sessionManager(): SessionManager');
@@ -217,6 +217,56 @@ describe('generateResources', () => {
217
217
  expect(content).not.toMatch(/PaginationOrder::Desc/);
218
218
  });
219
219
 
220
+ it('does not materialize optional query defaults for URL builders', () => {
221
+ const screenHintEnum = {
222
+ name: 'UserManagementAuthenticationScreenHint',
223
+ values: [
224
+ { name: 'sign_in', value: 'sign-in' },
225
+ { name: 'sign_up', value: 'sign-up' },
226
+ ],
227
+ };
228
+ const op = {
229
+ name: 'getAuthorizationUrl',
230
+ httpMethod: 'get' as const,
231
+ path: '/user_management/authorize',
232
+ pathParams: [],
233
+ queryParams: [
234
+ { name: 'redirect_uri', type: { kind: 'primitive' as const, type: 'string' as const }, required: true },
235
+ {
236
+ name: 'screen_hint',
237
+ type: { kind: 'enum' as const, name: 'UserManagementAuthenticationScreenHint' },
238
+ required: false,
239
+ default: 'sign-in',
240
+ },
241
+ ],
242
+ headerParams: [],
243
+ response: { kind: 'primitive' as const, type: 'unknown' as const },
244
+ errors: [],
245
+ injectIdempotencyKey: false,
246
+ };
247
+ const service: Service = { name: 'UserManagement', operations: [op] };
248
+ const spec: ApiSpec = { ...emptySpec, enums: [screenHintEnum], services: [service] };
249
+ const content = generateResources([service], {
250
+ ...ctx,
251
+ spec,
252
+ resolvedOperations: [
253
+ {
254
+ operation: op,
255
+ service,
256
+ methodName: 'get_authorization_url',
257
+ mountOn: 'UserManagement',
258
+ defaults: { response_type: 'code' },
259
+ inferFromClient: ['client_id'],
260
+ urlBuilder: true,
261
+ },
262
+ ],
263
+ })[0].content;
264
+
265
+ expect(content).toContain('?\\WorkOS\\Resource\\UserManagementAuthenticationScreenHint $screenHint = null');
266
+ expect(content).toContain("'screen_hint' => $screenHint?->value");
267
+ expect(content).not.toContain('UserManagementAuthenticationScreenHint::SignIn');
268
+ });
269
+
220
270
  it('generates paginated list method', () => {
221
271
  const result = generateResources(services, ctx);
222
272
 
@@ -319,6 +319,12 @@ describe('rust/resources', () => {
319
319
  type: { kind: 'primitive', type: 'string' },
320
320
  required: true,
321
321
  },
322
+ {
323
+ name: 'screen_hint',
324
+ type: { kind: 'enum', name: 'UserManagementAuthenticationScreenHint' },
325
+ required: false,
326
+ default: 'sign-in',
327
+ },
322
328
  {
323
329
  name: 'state',
324
330
  type: { kind: 'primitive', type: 'string' },
@@ -347,6 +353,9 @@ describe('rust/resources', () => {
347
353
  expect(f.content).toContain('pub fn get_authorization_url(');
348
354
  expect(f.content).toContain('-> Result<String, Error>');
349
355
  expect(f.content).toContain('let qs = crate::query::encode_query');
356
+ expect(f.content).toContain('pub screen_hint: Option<UserManagementAuthenticationScreenHint>,');
357
+ expect(f.content).toContain('screen_hint: Default::default(),');
358
+ expect(f.content).not.toContain('screen_hint: Some(UserManagementAuthenticationScreenHint::SignIn)');
350
359
  expect(f.content).not.toContain('get_authorization_url_with_options');
351
360
  expect(f.content).not.toContain('request_with_query_opts');
352
361
  });