@workos/oagen 0.15.0 → 0.17.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.
@@ -456,17 +456,64 @@ function extractInlineModelsFromProperties(schema, results, parentName) {
456
456
  }
457
457
  }
458
458
  if (fieldSchema.oneOf) {
459
- const objectVariant = fieldSchema.oneOf.find((v) => v.properties && (v.type === "object" || !v.type));
460
- if (objectVariant) {
459
+ const inlineObjectVariants = fieldSchema.oneOf.filter((v) => !v.$ref && v.properties && (v.type === "object" || !v.type));
460
+ if (inlineObjectVariants.length > 0) {
461
461
  const modelName = qualifyInlineModelName(toPascalCase(fieldName), parentName);
462
- if (!new Set(results.map((r) => r.name)).has(modelName)) {
463
- results.push(buildInlineModel(modelName, objectVariant));
464
- extractInlineModelsFromProperties(objectVariant, results, modelName);
462
+ const existingNames = new Set(results.map((r) => r.name));
463
+ const namingDiscProp = deriveOneOfNamingDiscriminator(inlineObjectVariants);
464
+ const emittedNames = [];
465
+ for (const variant of inlineObjectVariants) {
466
+ const variantName = nameOneOfVariant(variant, modelName, emittedNames, namingDiscProp);
467
+ emittedNames.push(variantName);
468
+ if (!existingNames.has(variantName)) {
469
+ existingNames.add(variantName);
470
+ results.push(buildInlineModel(variantName, variant));
471
+ extractInlineModelsFromProperties(variant, results, variantName);
472
+ }
465
473
  }
466
474
  }
467
475
  }
468
476
  }
469
477
  }
478
+ /** Find a single string-const-valued property shared by every variant whose
479
+ * values are all distinct — the implicit discriminator. Returns null when no
480
+ * such property exists. Mirrors `deriveConstNamingDiscriminator` in schemas.ts. */
481
+ function deriveOneOfNamingDiscriminator(variants) {
482
+ if (variants.length < 2) return null;
483
+ const candidates = Object.keys(variants[0]?.properties ?? {});
484
+ for (const propName of candidates) {
485
+ const values = variants.map((v) => readConstString(v.properties?.[propName]));
486
+ if (values.some((v) => v === null)) continue;
487
+ if (new Set(values).size !== values.length) continue;
488
+ return propName;
489
+ }
490
+ return null;
491
+ }
492
+ function readConstString(prop) {
493
+ if (!prop) return null;
494
+ if (typeof prop.const === "string") return prop.const;
495
+ if (Array.isArray(prop.enum) && prop.enum.length === 1 && typeof prop.enum[0] === "string") return prop.enum[0];
496
+ return null;
497
+ }
498
+ /** Produce a per-variant model name. Variant 0 keeps the bare parent name;
499
+ * later variants are prefixed by the const-derived label. Mirrors
500
+ * `nameVariantModel` in schemas.ts. Falls back to a numeric suffix when no
501
+ * discriminator is available, the const value PascalCases to nothing, or
502
+ * the derived candidate collides with the parent or an already-emitted name. */
503
+ function nameOneOfVariant(variant, parentName, alreadyEmitted, discriminatorProperty) {
504
+ if (alreadyEmitted.length === 0) return parentName;
505
+ if (discriminatorProperty) {
506
+ const constValue = readConstString(variant.properties?.[discriminatorProperty]);
507
+ if (constValue) {
508
+ const prefix = toPascalCase(constValue);
509
+ if (prefix) {
510
+ const candidate = parentName.startsWith(prefix) ? parentName : `${prefix}${parentName}`;
511
+ if (!(candidate === parentName || alreadyEmitted.includes(candidate))) return candidate;
512
+ }
513
+ }
514
+ }
515
+ return `${parentName}${alreadyEmitted.length + 1}`;
516
+ }
470
517
  function buildInlineModel(name, schema) {
471
518
  const requiredSet = new Set(schema.required ?? []);
472
519
  const fields = [];
@@ -628,6 +675,7 @@ function findNestedFieldSchema(fieldName, parentSchema, schemas) {
628
675
  function extractNestedSchema(name, schema) {
629
676
  if (schema.oneOf) {
630
677
  const models = [];
678
+ const constNamingDiscriminator = deriveConstNamingDiscriminator(schema.oneOf.filter((v) => !v.$ref && v.properties && (v.type === "object" || !v.type)));
631
679
  for (const variant of schema.oneOf) {
632
680
  if (variant.$ref) continue;
633
681
  if (variant.properties && (variant.type === "object" || !variant.type)) {
@@ -637,7 +685,7 @@ function extractNestedSchema(name, schema) {
637
685
  if (!fs) continue;
638
686
  fields.push(buildFieldFromSchema(fn, fs, name, requiredSet));
639
687
  }
640
- const variantName = models.length === 0 ? name : `${name}${models.length + 1}`;
688
+ const variantName = nameVariantModel(variant, name, models, constNamingDiscriminator);
641
689
  models.push({
642
690
  name: variantName,
643
691
  description: variant.description,
@@ -675,6 +723,69 @@ function extractNestedSchema(name, schema) {
675
723
  }
676
724
  return [];
677
725
  }
726
+ /**
727
+ * Derive a meaningful name for a oneOf object variant past the first,
728
+ * falling back to the numeric-suffix scheme (`Foo2`, `Foo3`) when no
729
+ * better signal is available. The "better signal" is a const-valued
730
+ * discriminator property that distinguishes one variant from the others.
731
+ *
732
+ * For a schema like `ApiKey.owner` whose oneOf has two variants — one
733
+ * with `type: { const: "organization" }` and one with `type: { const:
734
+ * "user" }` — this returns `ApiKeyOwner` for the first variant (the
735
+ * baseline name that the union TypeRef will reference, so consumer-
736
+ * visible typed fields stay backward-compatible) and `UserApiKeyOwner`
737
+ * for the second instead of `ApiKeyOwner2`. SDK consumers reading the
738
+ * generated types can tell the variants apart at a glance.
739
+ *
740
+ * The first variant always keeps the bare parent name because
741
+ * `schemaToTypeRef` builds union variants whose model refs collapse to
742
+ * `qualifyInlineModelName(field, parent)` — so the first model name
743
+ * must match for the union's degenerate-collapse to land on a valid
744
+ * type. Renaming variant 0 would orphan it.
745
+ *
746
+ * Falls back to numeric suffix when:
747
+ * - The variant has no discriminator property (no shared const
748
+ * property across all object variants), OR
749
+ * - The const value PascalCases to an empty/whitespace name, OR
750
+ * - The derived name collides with the parent name or an already-
751
+ * emitted variant name (numeric keeps uniqueness).
752
+ */
753
+ function nameVariantModel(variant, parentName, alreadyEmitted, discriminator) {
754
+ if (alreadyEmitted.length === 0) return parentName;
755
+ if (discriminator) {
756
+ const constValue = getConstPropertyValue(variant, discriminator.property);
757
+ if (constValue) {
758
+ const prefix = toPascalCase(constValue);
759
+ if (prefix) {
760
+ const candidate = parentName.startsWith(prefix) ? parentName : `${prefix}${parentName}`;
761
+ if (!(candidate === parentName || alreadyEmitted.some((m) => m.name === candidate))) return candidate;
762
+ }
763
+ }
764
+ }
765
+ return `${parentName}${alreadyEmitted.length + 1}`;
766
+ }
767
+ /**
768
+ * Returns a discriminator descriptor when every inline object variant in a
769
+ * oneOf has a string-const value on the same property — the signature of a
770
+ * discriminated union without an explicit `discriminator:` keyword. Used by
771
+ * `nameVariantModel` to derive variant names from the const values.
772
+ *
773
+ * Distinct from `detectConstPropertyDiscriminator` (which requires every
774
+ * variant to carry a `$ref` or `title` so a name mapping can be built):
775
+ * this pass operates on inline anonymous variants, where naming is exactly
776
+ * what we're trying to derive.
777
+ */
778
+ function deriveConstNamingDiscriminator(variants) {
779
+ if (variants.length < 2) return null;
780
+ const candidates = Object.keys(variants[0]?.properties ?? {});
781
+ for (const propName of candidates) {
782
+ const values = variants.map((v) => getConstPropertyValue(v, propName));
783
+ if (values.some((v) => v === null)) continue;
784
+ if (new Set(values).size !== values.length) continue;
785
+ return { property: propName };
786
+ }
787
+ return null;
788
+ }
678
789
  /** Build a single Field from a schema property entry. Shared across all extraction sites. */
679
790
  function buildFieldFromSchema(fieldName, fieldSchema, contextName, requiredSet) {
680
791
  return {
@@ -769,6 +880,51 @@ function resolveVariantModelName(schema) {
769
880
  if (title) return resolveSchemaName(title);
770
881
  return null;
771
882
  }
883
+ /** Look up the discriminator's mapped variant name for an inline object
884
+ * variant by reading its const-valued discriminator property. Returns null
885
+ * when the variant doesn't pin the property to a string const, or when
886
+ * the value isn't in the mapping. */
887
+ function mapVariantToDiscriminatorEntry(variant, discriminator) {
888
+ const value = getConstPropertyValue(variant, discriminator.property);
889
+ if (value === null) return null;
890
+ return discriminator.mapping[value] ?? null;
891
+ }
892
+ /**
893
+ * Derive a discriminator value → variant model name mapping for an inline-
894
+ * variant `oneOf` whose explicit `discriminator:` has no `mapping:` clause.
895
+ *
896
+ * Each variant model name is reproduced via `nameVariantModel` so it matches
897
+ * what `extractNestedSchema` registered when it walked the same `oneOf` to
898
+ * pull variants out as named models. Variant 0 keeps the bare parent name
899
+ * (e.g. `ApiKeyOwner`); subsequent variants get a const-derived prefix
900
+ * (e.g. `UserApiKeyOwner` for the `type: const: user` variant).
901
+ *
902
+ * The "parent name" used here mirrors the one `collectNestedInlineModels`
903
+ * passes to `extractNestedSchema`: it's the qualified inline model name
904
+ * built from `contextName` (the field name) plus `parentModelName` (the
905
+ * containing model). Returns null when any non-object variant is present
906
+ * or when a variant lacks a const value on the discriminator property.
907
+ */
908
+ function deriveInlineVariantMapping(variants, discriminatorProperty, contextName, parentModelName) {
909
+ if (variants.length === 0 || !contextName) return null;
910
+ const inlineObjectVariants = variants.filter((v) => !v.$ref && v.properties && (v.type === "object" || !v.type));
911
+ if (inlineObjectVariants.length !== variants.length) return null;
912
+ const baseInlineName = qualifyInlineModelName(toPascalCase(contextName), parentModelName);
913
+ const namingDiscriminator = { property: discriminatorProperty };
914
+ const mapping = {};
915
+ const emittedSoFar = [];
916
+ for (const variant of inlineObjectVariants) {
917
+ const constValue = getConstPropertyValue(variant, discriminatorProperty);
918
+ if (constValue === null) return null;
919
+ const variantName = nameVariantModel(variant, baseInlineName, emittedSoFar, namingDiscriminator);
920
+ mapping[constValue] = variantName;
921
+ emittedSoFar.push({
922
+ name: variantName,
923
+ fields: []
924
+ });
925
+ }
926
+ return mapping;
927
+ }
772
928
  function extractEnum(name, schema) {
773
929
  return {
774
930
  name,
@@ -967,8 +1123,20 @@ function extractInlineModelDeep(name, schema) {
967
1123
  }));
968
1124
  }
969
1125
  if (fieldSchema.oneOf) {
970
- const objectVariant = fieldSchema.oneOf.find((v) => v.properties && (v.type === "object" || !v.type));
971
- if (objectVariant) nestedModels.push(...extractInlineModelDeep(qualifyNestedInlineName(name, fieldName), objectVariant));
1126
+ const inlineObjectVariants = fieldSchema.oneOf.filter((v) => !v.$ref && v.properties && (v.type === "object" || !v.type));
1127
+ if (inlineObjectVariants.length > 0) {
1128
+ const baseQualified = qualifyNestedInlineName(name, fieldName);
1129
+ const namingDisc = deriveConstNamingDiscriminator(inlineObjectVariants);
1130
+ const emitted = [];
1131
+ for (const variant of inlineObjectVariants) {
1132
+ const variantName = nameVariantModel(variant, baseQualified, emitted, namingDisc);
1133
+ emitted.push({
1134
+ name: variantName,
1135
+ fields: []
1136
+ });
1137
+ nestedModels.push(...extractInlineModelDeep(variantName, variant));
1138
+ }
1139
+ }
972
1140
  }
973
1141
  }
974
1142
  return [{
@@ -1065,17 +1233,49 @@ function schemaToTypeRef(schema, contextName, parentModelName) {
1065
1233
  inner: enumRef
1066
1234
  } : enumRef;
1067
1235
  }
1068
- const syntheticDiscriminator = !schema.discriminator && compositionKind === "oneOf" ? detectConstPropertyDiscriminator(nonNullVariants) : null;
1069
- const variants = rawVariants.filter((v) => v.type !== "null").map((v) => schemaToTypeRef(v, contextName, parentModelName));
1236
+ let resolvedDiscriminator;
1237
+ if (schema.discriminator) {
1238
+ const property = schema.discriminator.propertyName;
1239
+ const explicitMapping = schema.discriminator.mapping ?? {};
1240
+ let mapping = Object.fromEntries(Object.entries(explicitMapping).map(([k, v]) => [k, v.replace(/^#\/components\/schemas\//, "")]));
1241
+ if (Object.keys(mapping).length === 0) {
1242
+ const inlineMapping = deriveInlineVariantMapping(nonNullVariants, property, contextName, parentModelName);
1243
+ if (inlineMapping) mapping = inlineMapping;
1244
+ }
1245
+ resolvedDiscriminator = {
1246
+ property,
1247
+ mapping
1248
+ };
1249
+ } else if (compositionKind === "oneOf") {
1250
+ const synthetic = detectConstPropertyDiscriminator(nonNullVariants);
1251
+ if (synthetic) resolvedDiscriminator = synthetic;
1252
+ else {
1253
+ const namingDisc = deriveConstNamingDiscriminator(nonNullVariants);
1254
+ if (namingDisc) {
1255
+ const inlineMapping = deriveInlineVariantMapping(nonNullVariants, namingDisc.property, contextName, parentModelName);
1256
+ if (inlineMapping) resolvedDiscriminator = {
1257
+ property: namingDisc.property,
1258
+ mapping: inlineMapping
1259
+ };
1260
+ }
1261
+ }
1262
+ }
1263
+ const variants = [];
1264
+ for (const v of rawVariants) {
1265
+ if (v === nullVariant) continue;
1266
+ const inlineMappingName = resolvedDiscriminator && !v.$ref && v.properties && (v.type === "object" || !v.type) ? mapVariantToDiscriminatorEntry(v, resolvedDiscriminator) : null;
1267
+ if (inlineMappingName) variants.push({
1268
+ kind: "model",
1269
+ name: inlineMappingName
1270
+ });
1271
+ else variants.push(schemaToTypeRef(v, contextName, parentModelName));
1272
+ }
1070
1273
  const hasNull = !!nullVariant;
1071
1274
  const union = {
1072
1275
  kind: "union",
1073
1276
  variants,
1074
1277
  compositionKind,
1075
- ...schema.discriminator ? { discriminator: {
1076
- property: schema.discriminator.propertyName,
1077
- mapping: Object.fromEntries(Object.entries(schema.discriminator.mapping ?? {}).map(([k, v]) => [k, v.replace(/^#\/components\/schemas\//, "")]))
1078
- } } : syntheticDiscriminator ? { discriminator: syntheticDiscriminator } : {}
1278
+ ...resolvedDiscriminator ? { discriminator: resolvedDiscriminator } : {}
1079
1279
  };
1080
1280
  return hasNull ? {
1081
1281
  kind: "nullable",