@workos/oagen-emitters 0.7.5 → 0.8.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.
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "0.7.5"
2
+ ".": "0.8.0"
3
3
  }
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.8.0](https://github.com/workos/oagen-emitters/compare/v0.7.5...v0.8.0) (2026-05-05)
4
+
5
+
6
+ ### Features
7
+
8
+ * **emitters:** dispatch field-level discriminated unions and drop dead request bodies ([#81](https://github.com/workos/oagen-emitters/issues/81)) ([4d38d24](https://github.com/workos/oagen-emitters/commit/4d38d249dcc7079e2a61d8faeabb681d6798618f))
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * **go:** stop SSO auth code leaking into request URL ([#83](https://github.com/workos/oagen-emitters/issues/83)) ([bc520e6](https://github.com/workos/oagen-emitters/commit/bc520e6a3d966abdf785262e5c49736b5e105b92))
14
+
3
15
  ## [0.7.5](https://github.com/workos/oagen-emitters/compare/v0.7.4...v0.7.5) (2026-05-03)
4
16
 
5
17
 
package/dist/index.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { _ as nodeEmitter, a as rustExtractor, c as pythonExtractor, d as rubyEmitter, f as kotlinEmitter, g as pythonEmitter, h as phpEmitter, i as kotlinExtractor, l as rubyExtractor, m as goEmitter, n as elixirExtractor, o as goExtractor, p as dotnetEmitter, r as dotnetExtractor, s as phpExtractor, t as workosEmittersPlugin, u as nodeExtractor } from "./plugin-BoTAX4nl.mjs";
1
+ import { _ as nodeEmitter, a as rustExtractor, c as pythonExtractor, d as rubyEmitter, f as kotlinEmitter, g as pythonEmitter, h as phpEmitter, i as kotlinExtractor, l as rubyExtractor, m as goEmitter, n as elixirExtractor, o as goExtractor, p as dotnetEmitter, r as dotnetExtractor, s as phpExtractor, t as workosEmittersPlugin, u as nodeExtractor } from "./plugin-bCMdV7KX.mjs";
2
2
  export { dotnetEmitter, dotnetExtractor, elixirExtractor, goEmitter, goExtractor, kotlinEmitter, kotlinExtractor, nodeEmitter, nodeExtractor, phpEmitter, phpExtractor, pythonEmitter, pythonExtractor, rubyEmitter, rubyExtractor, rustExtractor, workosEmittersPlugin };
@@ -7633,6 +7633,17 @@ function deserializeField(ref, accessor, isRequired, walrusVar = "_v") {
7633
7633
  case "union": {
7634
7634
  const modelVariants = (ref.variants ?? []).filter((v) => v.kind === "model");
7635
7635
  const uniqueModels = [...new Set(modelVariants.map((v) => v.name))];
7636
+ if (ref.discriminator && ref.discriminator.mapping) {
7637
+ const entries = Object.entries(ref.discriminator.mapping);
7638
+ if (entries.length > 0) {
7639
+ const dispatchMap = entries.map(([value, modelName]) => `"${value}": ${className$5(modelName)}`).join(", ");
7640
+ const dataExpr = isRequired ? accessor : walrusVar;
7641
+ const dataCast = `cast(Dict[str, Any], ${dataExpr})`;
7642
+ const branch = `(_disc.from_dict(${dataCast}) if (_disc := ${`{${dispatchMap}}.get(${dataCast}.get("${ref.discriminator.property}"))`}) is not None else ${dataExpr})`;
7643
+ if (isRequired) return branch;
7644
+ return `(${branch}) if (${walrusVar} := ${accessor}) is not None else None`;
7645
+ }
7646
+ }
7636
7647
  if (uniqueModels.length === 1) return deserializeField({
7637
7648
  kind: "model",
7638
7649
  name: uniqueModels[0]
@@ -10781,6 +10792,13 @@ function generateFromArrayValue(ref, accessor) {
10781
10792
  return accessor;
10782
10793
  case "nullable": return generateFromArrayValue(ref.inner, accessor);
10783
10794
  case "union": {
10795
+ if (ref.discriminator && ref.discriminator.mapping) {
10796
+ const entries = Object.entries(ref.discriminator.mapping);
10797
+ if (entries.length > 0) {
10798
+ const arms = entries.map(([value, modelName]) => `'${value}' => ${className$4(modelName)}::fromArray(${accessor})`).join(", ");
10799
+ return `match (${accessor}['${ref.discriminator.property}'] ?? null) { ${arms}, default => ${accessor} }`;
10800
+ }
10801
+ }
10784
10802
  const resolved = resolveDegenerateUnion(ref);
10785
10803
  if (resolved) return generateFromArrayValue(resolved, accessor);
10786
10804
  return accessor;
@@ -12442,11 +12460,70 @@ function joinUnionVariants$2(_ref, variants) {
12442
12460
  if (_ref.compositionKind === "allOf") return variants[0] ?? "interface{}";
12443
12461
  const unique = [...new Set(variants)];
12444
12462
  if (unique.length === 1) return unique[0];
12463
+ if (_ref.discriminator && _ref.discriminator.mapping) {
12464
+ const resolverName = unionResolverName(_ref);
12465
+ if (resolverName) return resolverName;
12466
+ }
12445
12467
  return "interface{}";
12446
12468
  }
12469
+ /**
12470
+ * Pick a stable type name for a discriminated union's runtime resolver. Today
12471
+ * we emit no resolver struct, so we treat the union's first model variant as
12472
+ * the public type — matching the pre-discriminator behavior where Go just
12473
+ * referenced one variant directly. The Owner field stays typed, callers
12474
+ * can still inspect Type to detect the user variant (data loss on
12475
+ * non-overlapping fields like organization_id is documented in the SDK
12476
+ * compat report rather than fixed in the emitter — Go callers who need the
12477
+ * user-only fields can json.Unmarshal the raw payload manually).
12478
+ */
12479
+ function unionResolverName(ref) {
12480
+ for (const v of ref.variants) if (v.kind === "model") return `*${className$3(v.name)}`;
12481
+ return null;
12482
+ }
12447
12483
  //#endregion
12448
12484
  //#region src/go/models.ts
12449
12485
  /**
12486
+ * Collect names of models that are referenced **only** as a named request body
12487
+ * model on an operation, with no other consumers (response types, pagination
12488
+ * item types, error types, or fields on other models).
12489
+ *
12490
+ * The Go emitter synthesizes a `{Service}{Method}Params` struct from those
12491
+ * request bodies (see `resources.ts:392`), and the method signature uses the
12492
+ * synthesized struct — never the named model. So emitting the named model in
12493
+ * `models.go` would leave callers with a duplicate, unused struct (the bug
12494
+ * surfaced in workos-go#544 with `CreateUserAPIKey` /
12495
+ * `UserManagementCreateAPIKeyParams`).
12496
+ *
12497
+ * Models in this set are skipped during model emission. Callers parameterize
12498
+ * the API surface through `*Params` exclusively, which is the sole consumer
12499
+ * of the spec's named request body schema.
12500
+ */
12501
+ function collectRequestBodyOnlyModelNames$1(services, models) {
12502
+ const requestBodyNames = /* @__PURE__ */ new Set();
12503
+ const otherReferences = /* @__PURE__ */ new Set();
12504
+ const collect = (ref, into) => {
12505
+ if (!ref) return;
12506
+ walkTypeRef(ref, { model: (r) => into.add(r.name) });
12507
+ };
12508
+ for (const service of services) for (const op of service.operations) {
12509
+ if (op.requestBody?.kind === "model") requestBodyNames.add(op.requestBody.name);
12510
+ collect(op.response, otherReferences);
12511
+ if (op.pagination) collect(op.pagination.itemType, otherReferences);
12512
+ for (const p of [
12513
+ ...op.pathParams,
12514
+ ...op.queryParams,
12515
+ ...op.headerParams,
12516
+ ...op.cookieParams ?? []
12517
+ ]) collect(p.type, otherReferences);
12518
+ if (op.successResponses) for (const sr of op.successResponses) collect(sr.type, otherReferences);
12519
+ for (const err of op.errors) if (err.type) collect(err.type, otherReferences);
12520
+ }
12521
+ for (const model of models) for (const field of model.fields) collect(field.type, otherReferences);
12522
+ const result = /* @__PURE__ */ new Set();
12523
+ for (const name of requestBodyNames) if (!otherReferences.has(name)) result.add(name);
12524
+ return result;
12525
+ }
12526
+ /**
12450
12527
  * Generate Go struct definitions from IR Models.
12451
12528
  * All models go into a single models.go file for the flat package.
12452
12529
  */
@@ -12456,10 +12533,12 @@ function generateModels$3(models, ctx) {
12456
12533
  const lines = [];
12457
12534
  lines.push(`package ${ctx.namespace}`);
12458
12535
  lines.push("");
12536
+ const requestBodyOnly = collectRequestBodyOnlyModelNames$1(ctx.spec.services, models);
12459
12537
  const modelHashMap = /* @__PURE__ */ new Map();
12460
12538
  const hashGroups = /* @__PURE__ */ new Map();
12461
12539
  for (const model of models) {
12462
12540
  if (isListWrapperModel(model) || isListMetadataModel(model)) continue;
12541
+ if (requestBodyOnly.has(model.name)) continue;
12463
12542
  const hash = structuralHash$2(model);
12464
12543
  modelHashMap.set(model.name, hash);
12465
12544
  if (!hashGroups.has(hash)) hashGroups.set(hash, []);
@@ -12476,6 +12555,7 @@ function generateModels$3(models, ctx) {
12476
12555
  const batchedAliases = /* @__PURE__ */ new Set();
12477
12556
  for (const model of models) {
12478
12557
  if (isListWrapperModel(model) || isListMetadataModel(model)) continue;
12558
+ if (requestBodyOnly.has(model.name)) continue;
12479
12559
  const structName = className$3(model.name);
12480
12560
  const canonicalName = aliasOf.get(model.name);
12481
12561
  if (canonicalName) {
@@ -13242,7 +13322,7 @@ function generateParamsStruct(mountName, method, op, plan, ctx, resolvedOp) {
13242
13322
  emittedFields.add(goField);
13243
13323
  const goType = !field.required ? makeOptional(mapTypeRef$3(field.type)) : mapTypeRef$3(field.type);
13244
13324
  const jsonTag = field.required ? `json:"${field.name}"` : `json:"${field.name},omitempty"`;
13245
- const urlTag = op.queryParams.some((qp) => !hidden.has(qp.name) && fieldName$2(qp.name) === goField) ? ` url:"${field.name}${field.required ? "" : ",omitempty"}"` : "";
13325
+ const urlTag = " url:\"-\"";
13246
13326
  if (field.description) {
13247
13327
  const fdLines = field.description.split("\n").filter((l) => l.trim());
13248
13328
  lines.push(`\t// ${fieldDocComment(goField, fdLines[0])}`);
@@ -15094,9 +15174,8 @@ const discriminatedUnions$1 = /* @__PURE__ */ new Map();
15094
15174
  function joinUnionVariants$1(_ref, variants) {
15095
15175
  if (_ref.compositionKind === "allOf") return variants[0] ?? "object";
15096
15176
  const unique = [...new Set(variants)];
15097
- if (unique.length === 1) return unique[0];
15098
15177
  if (_ref.discriminator && _ref.discriminator.mapping) {
15099
- const baseName = unique[0];
15178
+ const baseName = _ref.variants.filter((v) => v.kind === "model").map((v) => v.kind === "model" ? v.name : "").filter(Boolean)[0] ?? unique[0];
15100
15179
  discriminatedUnions$1.set(baseName, {
15101
15180
  property: _ref.discriminator.property,
15102
15181
  mapping: _ref.discriminator.mapping,
@@ -15104,6 +15183,7 @@ function joinUnionVariants$1(_ref, variants) {
15104
15183
  });
15105
15184
  return "object";
15106
15185
  }
15186
+ if (unique.length === 1) return unique[0];
15107
15187
  if (unique.length >= 2 && unique.length <= 9) return `OneOf.OneOf<${unique.join(", ")}>`;
15108
15188
  if (unique.length >= 10) console.warn(`[oagen:dotnet] Union with ${unique.length} variants exceeds OneOf<T0..T8> arity; falling back to object. Variants: ${unique.join(", ")}`);
15109
15189
  return "object";
@@ -15119,6 +15199,7 @@ function generateModels$2(models, ctx, discCtx) {
15119
15199
  if (models.length === 0) return [];
15120
15200
  const enumConstByName = /* @__PURE__ */ new Map();
15121
15201
  for (const e of ctx.spec.enums) if (e.values.length === 1) enumConstByName.set(e.name, String(e.values[0].value));
15202
+ const requestBodyOnlyNames = collectRequestBodyOnlyModelNames(ctx.spec.services, models);
15122
15203
  const files = [];
15123
15204
  primeModelAliases(models);
15124
15205
  const baseFieldLookup = /* @__PURE__ */ new Map();
@@ -15131,6 +15212,7 @@ function generateModels$2(models, ctx, discCtx) {
15131
15212
  }
15132
15213
  for (const model of models) {
15133
15214
  if (isListWrapperModel(model) || isListMetadataModel(model)) continue;
15215
+ if (requestBodyOnlyNames.has(model.name)) continue;
15134
15216
  const csClassName = modelClassName(model.name);
15135
15217
  if (isModelAlias(model.name)) continue;
15136
15218
  const lines = [];
@@ -15209,6 +15291,8 @@ function generateModels$2(models, ctx, discCtx) {
15209
15291
  }
15210
15292
  const isRequiredEnum = field.required && isEnumRef(field.type) && constInit === null;
15211
15293
  lines.push(...emitJsonPropertyAttributes(field.name, { isRequiredEnum }));
15294
+ const discriminatedUnionConverter = discriminatedUnionConverterName(field.type);
15295
+ if (discriminatedUnionConverter) lines.push(` [Newtonsoft.Json.JsonConverter(typeof(${discriminatedUnionConverter}))]`);
15212
15296
  const newMod = useNewModifier ? "new " : "";
15213
15297
  lines.push(` public ${newMod}${csType} ${csFieldName} { get; ${setterModifier}set; }${initializer}`);
15214
15298
  if (isDictionaryOfObject(csType) && !field.deprecated) dictObjectFields.push({
@@ -15274,6 +15358,23 @@ function generateModels$2(models, ctx, discCtx) {
15274
15358
  return files;
15275
15359
  }
15276
15360
  /**
15361
+ * Compute the name of the discriminator converter class for a field whose
15362
+ * type is a discriminated union, mirroring the keying used in
15363
+ * `joinUnionVariants` (first IR model variant name + "DiscriminatorConverter").
15364
+ * Returns null when the type isn't a discriminated union with a populated
15365
+ * mapping. Also walks through `nullable` so an optional discriminated field
15366
+ * still gets the converter applied.
15367
+ */
15368
+ function discriminatedUnionConverterName(ref) {
15369
+ const inner = ref.kind === "nullable" ? ref.inner : ref;
15370
+ if (inner.kind !== "union") return null;
15371
+ if (!inner.discriminator || !inner.discriminator.mapping) return null;
15372
+ if (Object.keys(inner.discriminator.mapping).length === 0) return null;
15373
+ const firstModel = inner.variants.find((v) => v.kind === "model");
15374
+ if (!firstModel || firstModel.kind !== "model") return null;
15375
+ return `${modelClassName(firstModel.name)}DiscriminatorConverter`;
15376
+ }
15377
+ /**
15277
15378
  * Whether the emitted C# type is `Dictionary<string, object>` or its
15278
15379
  * nullable variant — the usual shape of metadata / additional-properties
15279
15380
  * fields that get typed accessors.
@@ -15375,6 +15476,39 @@ function normalizeTypeForHash$1(ref, aliasOf) {
15375
15476
  function structuralHash$1(model, aliasOf = /* @__PURE__ */ new Map()) {
15376
15477
  return model.fields.map((f) => `${f.name}:${JSON.stringify(normalizeTypeForHash$1(f.type, aliasOf))}:${f.required}`).sort().join("|");
15377
15478
  }
15479
+ /**
15480
+ * Names of models referenced **only** as a named operation request body —
15481
+ * i.e. never appearing in a response, an error, a paginated item type, or as
15482
+ * a field type on another model. The .NET wrapper generator emits a
15483
+ * per-operation `*Options` class containing the same fields, so the named
15484
+ * entity is never instantiated by callers and just clutters the SDK
15485
+ * (workos-dotnet#248: `CreateUserApiKey` vs `UserManagementCreateApiKeyOptions`).
15486
+ */
15487
+ function collectRequestBodyOnlyModelNames(services, models) {
15488
+ const requestBodyNames = /* @__PURE__ */ new Set();
15489
+ const otherReferences = /* @__PURE__ */ new Set();
15490
+ const collect = (ref, into) => {
15491
+ if (!ref) return;
15492
+ walkTypeRef(ref, { model: (r) => into.add(r.name) });
15493
+ };
15494
+ for (const service of services) for (const op of service.operations) {
15495
+ if (op.requestBody?.kind === "model") requestBodyNames.add(op.requestBody.name);
15496
+ collect(op.response, otherReferences);
15497
+ if (op.pagination) collect(op.pagination.itemType, otherReferences);
15498
+ for (const p of [
15499
+ ...op.pathParams,
15500
+ ...op.queryParams,
15501
+ ...op.headerParams,
15502
+ ...op.cookieParams ?? []
15503
+ ]) collect(p.type, otherReferences);
15504
+ if (op.successResponses) for (const sr of op.successResponses) collect(sr.type, otherReferences);
15505
+ for (const err of op.errors) if (err.type) collect(err.type, otherReferences);
15506
+ }
15507
+ for (const model of models) for (const field of model.fields) collect(field.type, otherReferences);
15508
+ const result = /* @__PURE__ */ new Set();
15509
+ for (const name of requestBodyNames) if (!otherReferences.has(name)) result.add(name);
15510
+ return result;
15511
+ }
15378
15512
  //#endregion
15379
15513
  //#region src/dotnet/enums.ts
15380
15514
  /**
@@ -17100,7 +17234,7 @@ const dotnetEmitter = {
17100
17234
  lines.push(" switch (discriminatorValue)");
17101
17235
  lines.push(" {");
17102
17236
  for (const [value, modelName] of Object.entries(disc.mapping)) {
17103
- const csName = modelClassName(modelName);
17237
+ const csName = modelClassName(resolveModelName(modelName));
17104
17238
  lines.push(` case "${value}": return jObject.ToObject<${csName}>(serializer);`);
17105
17239
  }
17106
17240
  lines.push(" default: return jObject.ToObject<object>(serializer);");
@@ -17585,9 +17719,8 @@ const discriminatedUnions = /* @__PURE__ */ new Map();
17585
17719
  function joinUnionVariants(ref, variants) {
17586
17720
  if (ref.compositionKind === "allOf") return variants[0] ?? "Any";
17587
17721
  const unique = [...new Set(variants)];
17588
- if (unique.length === 1) return unique[0];
17589
17722
  if (ref.discriminator && ref.discriminator.mapping) {
17590
- const baseName = unique[0];
17723
+ const baseName = ref.variants.filter((v) => v.kind === "model").map((v) => v.kind === "model" ? v.name : "").filter(Boolean)[0] ?? unique[0];
17591
17724
  discriminatedUnions.set(baseName, {
17592
17725
  property: ref.discriminator.property,
17593
17726
  mapping: ref.discriminator.mapping,
@@ -17595,6 +17728,7 @@ function joinUnionVariants(ref, variants) {
17595
17728
  });
17596
17729
  return baseName;
17597
17730
  }
17731
+ if (unique.length === 1) return unique[0];
17598
17732
  return "Any";
17599
17733
  }
17600
17734
  /** Kotlin imports implied by a given type expression. Caller collects into a set. */
@@ -20095,8 +20229,15 @@ function deserializeExpression(accessor, ref, required, enumNames, modelNames) {
20095
20229
  }
20096
20230
  if (ref.kind === "enum" && enumNames.has(ref.name)) return accessor;
20097
20231
  if (ref.kind === "union") {
20098
- const modelVariants = ref.variants.filter((v) => v.kind === "model" && modelNames.has(v.name));
20099
- if (modelVariants.length > 0 && modelVariants.length === ref.variants.length) return accessor;
20232
+ if (ref.discriminator && ref.discriminator.mapping) {
20233
+ const entries = Object.entries(ref.discriminator.mapping).filter(([, name]) => modelNames.has(name));
20234
+ if (entries.length > 0) return `${accessor} ? ${`(case ${rubyHashAccessor(accessor, ref.discriminator.property)} ${entries.map(([value, modelName]) => {
20235
+ const cls = `WorkOS::${className(modelName)}`;
20236
+ return `when ${JSON.stringify(value)} then ${cls}.new(${accessor})`;
20237
+ }).join(" ")} else ${accessor} end)`} : nil`;
20238
+ }
20239
+ const firstModelVariant = ref.variants.find((v) => v.kind === "model" && modelNames.has(v.name));
20240
+ if (firstModelVariant && firstModelVariant.kind === "model") return `${accessor} ? ${`WorkOS::${className(firstModelVariant.name)}`}.new(${accessor}) : nil`;
20100
20241
  return accessor;
20101
20242
  }
20102
20243
  if (ref.kind === "literal") return accessor;
@@ -22153,4 +22294,4 @@ const workosEmittersPlugin = {
22153
22294
  //#endregion
22154
22295
  export { nodeEmitter as _, rustExtractor as a, pythonExtractor as c, rubyEmitter as d, kotlinEmitter as f, pythonEmitter as g, phpEmitter as h, kotlinExtractor as i, rubyExtractor as l, goEmitter as m, elixirExtractor as n, goExtractor as o, dotnetEmitter as p, dotnetExtractor as r, phpExtractor as s, workosEmittersPlugin as t, nodeExtractor as u };
22155
22296
 
22156
- //# sourceMappingURL=plugin-BoTAX4nl.mjs.map
22297
+ //# sourceMappingURL=plugin-bCMdV7KX.mjs.map