@workos/oagen 0.16.0 → 0.17.1

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/index.mjs CHANGED
@@ -208,17 +208,64 @@ function extractInlineModelsFromProperties(schema, results, parentName) {
208
208
  }
209
209
  }
210
210
  if (fieldSchema.oneOf) {
211
- const objectVariant = fieldSchema.oneOf.find((v) => v.properties && (v.type === "object" || !v.type));
212
- if (objectVariant) {
211
+ const inlineObjectVariants = fieldSchema.oneOf.filter((v) => !v.$ref && v.properties && (v.type === "object" || !v.type));
212
+ if (inlineObjectVariants.length > 0) {
213
213
  const modelName = qualifyInlineModelName(toPascalCase(fieldName), parentName);
214
- if (!new Set(results.map((r) => r.name)).has(modelName)) {
215
- results.push(buildInlineModel(modelName, objectVariant));
216
- extractInlineModelsFromProperties(objectVariant, results, modelName);
214
+ const existingNames = new Set(results.map((r) => r.name));
215
+ const namingDiscProp = deriveOneOfNamingDiscriminator(inlineObjectVariants);
216
+ const emittedNames = [];
217
+ for (const variant of inlineObjectVariants) {
218
+ const variantName = nameOneOfVariant(variant, modelName, emittedNames, namingDiscProp);
219
+ emittedNames.push(variantName);
220
+ if (!existingNames.has(variantName)) {
221
+ existingNames.add(variantName);
222
+ results.push(buildInlineModel(variantName, variant));
223
+ extractInlineModelsFromProperties(variant, results, variantName);
224
+ }
217
225
  }
218
226
  }
219
227
  }
220
228
  }
221
229
  }
230
+ /** Find a single string-const-valued property shared by every variant whose
231
+ * values are all distinct — the implicit discriminator. Returns null when no
232
+ * such property exists. Mirrors `deriveConstNamingDiscriminator` in schemas.ts. */
233
+ function deriveOneOfNamingDiscriminator(variants) {
234
+ if (variants.length < 2) return null;
235
+ const candidates = Object.keys(variants[0]?.properties ?? {});
236
+ for (const propName of candidates) {
237
+ const values = variants.map((v) => readConstString(v.properties?.[propName]));
238
+ if (values.some((v) => v === null)) continue;
239
+ if (new Set(values).size !== values.length) continue;
240
+ return propName;
241
+ }
242
+ return null;
243
+ }
244
+ function readConstString(prop) {
245
+ if (!prop) return null;
246
+ if (typeof prop.const === "string") return prop.const;
247
+ if (Array.isArray(prop.enum) && prop.enum.length === 1 && typeof prop.enum[0] === "string") return prop.enum[0];
248
+ return null;
249
+ }
250
+ /** Produce a per-variant model name. Variant 0 keeps the bare parent name;
251
+ * later variants are prefixed by the const-derived label. Mirrors
252
+ * `nameVariantModel` in schemas.ts. Falls back to a numeric suffix when no
253
+ * discriminator is available, the const value PascalCases to nothing, or
254
+ * the derived candidate collides with the parent or an already-emitted name. */
255
+ function nameOneOfVariant(variant, parentName, alreadyEmitted, discriminatorProperty) {
256
+ if (alreadyEmitted.length === 0) return parentName;
257
+ if (discriminatorProperty) {
258
+ const constValue = readConstString(variant.properties?.[discriminatorProperty]);
259
+ if (constValue) {
260
+ const prefix = toPascalCase(constValue);
261
+ if (prefix) {
262
+ const candidate = parentName.startsWith(prefix) ? parentName : `${prefix}${parentName}`;
263
+ if (!(candidate === parentName || alreadyEmitted.includes(candidate))) return candidate;
264
+ }
265
+ }
266
+ }
267
+ return `${parentName}${alreadyEmitted.length + 1}`;
268
+ }
222
269
  function buildInlineModel(name, schema) {
223
270
  const requiredSet = new Set(schema.required ?? []);
224
271
  const fields = [];
@@ -239,6 +286,14 @@ function buildInlineModel(name, schema) {
239
286
  * to apply the same name transform to $ref model/enum references.
240
287
  */
241
288
  let activeSchemaNameTransform = null;
289
+ /**
290
+ * Module-level set of resolved schema names whose components/schemas entry is
291
+ * an enum (i.e. has `enum:`). Populated by extractSchemas() so schemaToTypeRef()
292
+ * can classify a `$ref` to a top-level enum schema as `kind: 'enum'` instead of
293
+ * `kind: 'model'`. Without this, every `$ref` was naively returned as a model
294
+ * ref, even when the target was a shared enum like `PaginationOrder`.
295
+ */
296
+ let activeEnumSchemaNames = /* @__PURE__ */ new Set();
242
297
  /** Apply cleanSchemaName + the active transform (if any) to a raw schema name. */
243
298
  function resolveSchemaName(rawName) {
244
299
  let name = cleanSchemaName(toPascalCase(rawName));
@@ -264,6 +319,8 @@ function extractSchemas(schemas, options) {
264
319
  for (const [, originals] of transformedToOriginals) if (originals.length > 1) for (const n of originals) unsafeToTransform.add(n);
265
320
  activeSchemaNameTransform = (name) => unsafeToTransform.has(name) ? name : transform(name);
266
321
  } else activeSchemaNameTransform = null;
322
+ activeEnumSchemaNames = /* @__PURE__ */ new Set();
323
+ for (const [name, schema] of Object.entries(schemas)) if (schema.enum) activeEnumSchemaNames.add(resolveSchemaName(name));
267
324
  const modelsByCleanName = /* @__PURE__ */ new Map();
268
325
  for (const [name, schema] of Object.entries(schemas)) {
269
326
  const pascalName = resolveSchemaName(name);
@@ -298,6 +355,7 @@ function extractSchemas(schemas, options) {
298
355
  /** Clear the module-level schemaNameTransform. Called by parseSpec() after all extraction phases. */
299
356
  function clearSchemaNameTransform() {
300
357
  activeSchemaNameTransform = null;
358
+ activeEnumSchemaNames = /* @__PURE__ */ new Set();
301
359
  }
302
360
  /**
303
361
  * Walk a single TypeRef and extract inline enum refs as top-level enum definitions.
@@ -585,6 +643,51 @@ function resolveVariantModelName(schema) {
585
643
  if (title) return resolveSchemaName(title);
586
644
  return null;
587
645
  }
646
+ /** Look up the discriminator's mapped variant name for an inline object
647
+ * variant by reading its const-valued discriminator property. Returns null
648
+ * when the variant doesn't pin the property to a string const, or when
649
+ * the value isn't in the mapping. */
650
+ function mapVariantToDiscriminatorEntry(variant, discriminator) {
651
+ const value = getConstPropertyValue(variant, discriminator.property);
652
+ if (value === null) return null;
653
+ return discriminator.mapping[value] ?? null;
654
+ }
655
+ /**
656
+ * Derive a discriminator value → variant model name mapping for an inline-
657
+ * variant `oneOf` whose explicit `discriminator:` has no `mapping:` clause.
658
+ *
659
+ * Each variant model name is reproduced via `nameVariantModel` so it matches
660
+ * what `extractNestedSchema` registered when it walked the same `oneOf` to
661
+ * pull variants out as named models. Variant 0 keeps the bare parent name
662
+ * (e.g. `ApiKeyOwner`); subsequent variants get a const-derived prefix
663
+ * (e.g. `UserApiKeyOwner` for the `type: const: user` variant).
664
+ *
665
+ * The "parent name" used here mirrors the one `collectNestedInlineModels`
666
+ * passes to `extractNestedSchema`: it's the qualified inline model name
667
+ * built from `contextName` (the field name) plus `parentModelName` (the
668
+ * containing model). Returns null when any non-object variant is present
669
+ * or when a variant lacks a const value on the discriminator property.
670
+ */
671
+ function deriveInlineVariantMapping(variants, discriminatorProperty, contextName, parentModelName) {
672
+ if (variants.length === 0 || !contextName) return null;
673
+ const inlineObjectVariants = variants.filter((v) => !v.$ref && v.properties && (v.type === "object" || !v.type));
674
+ if (inlineObjectVariants.length !== variants.length) return null;
675
+ const baseInlineName = qualifyInlineModelName(toPascalCase(contextName), parentModelName);
676
+ const namingDiscriminator = { property: discriminatorProperty };
677
+ const mapping = {};
678
+ const emittedSoFar = [];
679
+ for (const variant of inlineObjectVariants) {
680
+ const constValue = getConstPropertyValue(variant, discriminatorProperty);
681
+ if (constValue === null) return null;
682
+ const variantName = nameVariantModel(variant, baseInlineName, emittedSoFar, namingDiscriminator);
683
+ mapping[constValue] = variantName;
684
+ emittedSoFar.push({
685
+ name: variantName,
686
+ fields: []
687
+ });
688
+ }
689
+ return mapping;
690
+ }
588
691
  function extractEnum(name, schema) {
589
692
  return {
590
693
  name,
@@ -783,8 +886,20 @@ function extractInlineModelDeep(name, schema) {
783
886
  }));
784
887
  }
785
888
  if (fieldSchema.oneOf) {
786
- const objectVariant = fieldSchema.oneOf.find((v) => v.properties && (v.type === "object" || !v.type));
787
- if (objectVariant) nestedModels.push(...extractInlineModelDeep(qualifyNestedInlineName(name, fieldName), objectVariant));
889
+ const inlineObjectVariants = fieldSchema.oneOf.filter((v) => !v.$ref && v.properties && (v.type === "object" || !v.type));
890
+ if (inlineObjectVariants.length > 0) {
891
+ const baseQualified = qualifyNestedInlineName(name, fieldName);
892
+ const namingDisc = deriveConstNamingDiscriminator(inlineObjectVariants);
893
+ const emitted = [];
894
+ for (const variant of inlineObjectVariants) {
895
+ const variantName = nameVariantModel(variant, baseQualified, emitted, namingDisc);
896
+ emitted.push({
897
+ name: variantName,
898
+ fields: []
899
+ });
900
+ nestedModels.push(...extractInlineModelDeep(variantName, variant));
901
+ }
902
+ }
788
903
  }
789
904
  }
790
905
  return [{
@@ -805,9 +920,14 @@ function schemaToTypeRef(schema, contextName, parentModelName) {
805
920
  if (schema.$ref) {
806
921
  const segments = schema.$ref.split("/");
807
922
  const rawName = segments[segments.length - 1];
923
+ const resolved = resolveSchemaName(rawName);
924
+ if (activeEnumSchemaNames.has(resolved)) return {
925
+ kind: "enum",
926
+ name: resolved
927
+ };
808
928
  return {
809
929
  kind: "model",
810
- name: resolveSchemaName(rawName)
930
+ name: resolved
811
931
  };
812
932
  }
813
933
  if (Array.isArray(schema.type)) {
@@ -881,17 +1001,49 @@ function schemaToTypeRef(schema, contextName, parentModelName) {
881
1001
  inner: enumRef
882
1002
  } : enumRef;
883
1003
  }
884
- const syntheticDiscriminator = !schema.discriminator && compositionKind === "oneOf" ? detectConstPropertyDiscriminator(nonNullVariants) : null;
885
- const variants = rawVariants.filter((v) => v.type !== "null").map((v) => schemaToTypeRef(v, contextName, parentModelName));
1004
+ let resolvedDiscriminator;
1005
+ if (schema.discriminator) {
1006
+ const property = schema.discriminator.propertyName;
1007
+ const explicitMapping = schema.discriminator.mapping ?? {};
1008
+ let mapping = Object.fromEntries(Object.entries(explicitMapping).map(([k, v]) => [k, v.replace(/^#\/components\/schemas\//, "")]));
1009
+ if (Object.keys(mapping).length === 0) {
1010
+ const inlineMapping = deriveInlineVariantMapping(nonNullVariants, property, contextName, parentModelName);
1011
+ if (inlineMapping) mapping = inlineMapping;
1012
+ }
1013
+ resolvedDiscriminator = {
1014
+ property,
1015
+ mapping
1016
+ };
1017
+ } else if (compositionKind === "oneOf") {
1018
+ const synthetic = detectConstPropertyDiscriminator(nonNullVariants);
1019
+ if (synthetic) resolvedDiscriminator = synthetic;
1020
+ else {
1021
+ const namingDisc = deriveConstNamingDiscriminator(nonNullVariants);
1022
+ if (namingDisc) {
1023
+ const inlineMapping = deriveInlineVariantMapping(nonNullVariants, namingDisc.property, contextName, parentModelName);
1024
+ if (inlineMapping) resolvedDiscriminator = {
1025
+ property: namingDisc.property,
1026
+ mapping: inlineMapping
1027
+ };
1028
+ }
1029
+ }
1030
+ }
1031
+ const variants = [];
1032
+ for (const v of rawVariants) {
1033
+ if (v === nullVariant) continue;
1034
+ const inlineMappingName = resolvedDiscriminator && !v.$ref && v.properties && (v.type === "object" || !v.type) ? mapVariantToDiscriminatorEntry(v, resolvedDiscriminator) : null;
1035
+ if (inlineMappingName) variants.push({
1036
+ kind: "model",
1037
+ name: inlineMappingName
1038
+ });
1039
+ else variants.push(schemaToTypeRef(v, contextName, parentModelName));
1040
+ }
886
1041
  const hasNull = !!nullVariant;
887
1042
  const union = {
888
1043
  kind: "union",
889
1044
  variants,
890
1045
  compositionKind,
891
- ...schema.discriminator ? { discriminator: {
892
- property: schema.discriminator.propertyName,
893
- mapping: Object.fromEntries(Object.entries(schema.discriminator.mapping ?? {}).map(([k, v]) => [k, v.replace(/^#\/components\/schemas\//, "")]))
894
- } } : syntheticDiscriminator ? { discriminator: syntheticDiscriminator } : {}
1046
+ ...resolvedDiscriminator ? { discriminator: resolvedDiscriminator } : {}
895
1047
  };
896
1048
  return hasNull ? {
897
1049
  kind: "nullable",
@@ -1463,10 +1615,23 @@ function inferServiceName(path, tag) {
1463
1615
  if (first.startsWith("{")) return "Default";
1464
1616
  return toPascalCase(first);
1465
1617
  }
1618
+ /**
1619
+ * Strip overload indices (e.g. NestJS emits `Foo_bar[0]` when a controller
1620
+ * method handles multiple routes) from an operationId so derived names never
1621
+ * leak the bracketed digit. Without this, `authenticate[0]` would surface as
1622
+ * `authenticate0` in method names and `Authenticate0Request` in body model
1623
+ * names. Brackets at the end (or with trailing whitespace) are common in
1624
+ * NestJS-style backends; this regex is intentionally narrow to avoid eating
1625
+ * legitimate digits like `oauth2`.
1626
+ */
1627
+ function normalizeOperationIdForNaming(operationId) {
1628
+ return operationId.replace(/\[\d+\]\s*$/, "");
1629
+ }
1466
1630
  function inferOperationName(method, path, operationId, operationIdTransform) {
1467
1631
  if (operationId) {
1468
- if (operationIdTransform) return operationIdTransform(operationId);
1469
- return toCamelCase(operationId);
1632
+ const normalized = normalizeOperationIdForNaming(operationId);
1633
+ if (operationIdTransform) return operationIdTransform(normalized);
1634
+ return toCamelCase(normalized);
1470
1635
  }
1471
1636
  const verb = inferVerb(method, path);
1472
1637
  const resource = inferResourceFromPath(path);
@@ -1784,7 +1949,7 @@ function extractRequestBody(body, op, inlineModels) {
1784
1949
  };
1785
1950
  return {};
1786
1951
  }
1787
- const contextName = cleanSchemaName(op?.operationId ? toPascalCase(op.operationId) + "Request" : "RequestBody");
1952
+ const contextName = cleanSchemaName(op?.operationId ? toPascalCase(normalizeOperationIdForNaming(op.operationId)) + "Request" : "RequestBody");
1788
1953
  if (schema.properties && (schema.type === "object" || !schema.type)) {
1789
1954
  const requiredSet = new Set(schema.required ?? []);
1790
1955
  const fields = [];
@@ -1865,7 +2030,7 @@ function deriveOneOfVariantName(variant, baseName, existingModels) {
1865
2030
  return name;
1866
2031
  }
1867
2032
  function deriveResponseName(op, path, method) {
1868
- if (op?.operationId) return cleanSchemaName(toPascalCase(op.operationId) + "Response");
2033
+ if (op?.operationId) return cleanSchemaName(toPascalCase(normalizeOperationIdForNaming(op.operationId)) + "Response");
1869
2034
  return toPascalCase(path.split("/").filter(Boolean)[0] ?? "Unknown") + toPascalCase(method) + "Response";
1870
2035
  }
1871
2036
  function extractResponses(responses, op, path, method) {