@workos/oagen 0.6.0 → 0.7.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.
@@ -600,7 +600,7 @@ function findSchemaByName(pascalName, schemas) {
600
600
  for (const [k, v] of Object.entries(schemas)) if (resolveSchemaName(k) === pascalName) return v;
601
601
  return null;
602
602
  }
603
- /** Walk into a schema to find a field's sub-schema, resolving allOf. */
603
+ /** Walk into a schema to find a field's sub-schema, resolving allOf and oneOf/anyOf. */
604
604
  function findNestedFieldSchema(fieldName, parentSchema, schemas) {
605
605
  if (parentSchema.properties?.[fieldName]) return parentSchema.properties[fieldName];
606
606
  if (parentSchema.allOf) for (const sub of parentSchema.allOf) {
@@ -612,6 +612,15 @@ function findNestedFieldSchema(fieldName, parentSchema, schemas) {
612
612
  }
613
613
  if (resolved.properties?.[fieldName]) return resolved.properties[fieldName];
614
614
  }
615
+ for (const variant of [...parentSchema.oneOf ?? [], ...parentSchema.anyOf ?? []]) {
616
+ let resolved = variant;
617
+ if (variant.$ref) {
618
+ const segments = variant.$ref.split("/");
619
+ const refName = segments[segments.length - 1];
620
+ if (refName && schemas[refName]) resolved = schemas[refName];
621
+ }
622
+ if (resolved.properties?.[fieldName]) return resolved.properties[fieldName];
623
+ }
615
624
  return null;
616
625
  }
617
626
  /** Extract a model (and nested models) from an inline field schema. */
@@ -777,6 +786,19 @@ function extractModel(name, schema, schemas) {
777
786
  if (!fieldSchema) continue;
778
787
  fields.push(buildFieldFromSchema(fieldName, fieldSchema, name, requiredSet));
779
788
  }
789
+ if (fields.length === 0 && (schema.oneOf || schema.anyOf)) {
790
+ const variants = schema.oneOf ?? schema.anyOf ?? [];
791
+ const emptyRequired = /* @__PURE__ */ new Set();
792
+ const seenFieldNames = /* @__PURE__ */ new Set();
793
+ for (const variant of variants) {
794
+ if (!variant.properties) continue;
795
+ for (const [fieldName, fieldSchema] of Object.entries(variant.properties)) {
796
+ if (!fieldSchema || seenFieldNames.has(fieldName)) continue;
797
+ seenFieldNames.add(fieldName);
798
+ fields.push(buildFieldFromSchema(fieldName, fieldSchema, name, emptyRequired));
799
+ }
800
+ }
801
+ }
780
802
  if (schema.additionalProperties && typeof schema.additionalProperties === "object" && schema.properties) {
781
803
  if (Object.keys(schema.additionalProperties).length > 0) {
782
804
  const valueType = schemaToTypeRef(schema.additionalProperties, "additionalProperties", name);
@@ -816,6 +838,18 @@ function extractAllOfModel(name, schema, schemas) {
816
838
  });
817
839
  if (f.required) requiredSet.add(f.name);
818
840
  }
841
+ } else if (resolved.oneOf || resolved.anyOf) {
842
+ const variants = resolved.oneOf ?? resolved.anyOf ?? [];
843
+ const emptyRequired = /* @__PURE__ */ new Set();
844
+ const seenFieldNames = new Set(fields.map((f) => f.name));
845
+ for (const variant of variants) {
846
+ if (!variant.properties) continue;
847
+ for (const [fieldName, fieldSchema] of Object.entries(variant.properties)) {
848
+ if (!fieldSchema || seenFieldNames.has(fieldName)) continue;
849
+ seenFieldNames.add(fieldName);
850
+ fields.push(buildFieldFromSchema(fieldName, fieldSchema, name, emptyRequired));
851
+ }
852
+ }
819
853
  } else {
820
854
  if (resolved.required) for (const r of resolved.required) requiredSet.add(r);
821
855
  if (resolved.properties) {
@@ -1672,6 +1706,94 @@ function inferDeeperContext(path) {
1672
1706
  if (nonParams.length >= 3) return toPascalCase(nonParams.slice(1, -1).join("_"));
1673
1707
  return null;
1674
1708
  }
1709
+ /**
1710
+ * Extract mutually-exclusive parameter groups from the `x-mutually-exclusive-parameter-groups`
1711
+ * operation extension. Cross-references grouped parameter names against the already-extracted
1712
+ * IR parameter arrays so emitters get object-identity references.
1713
+ */
1714
+ function extractParameterGroups(op, allIRParams, operationContext) {
1715
+ const raw = op["x-mutually-exclusive-parameter-groups"];
1716
+ if (!raw || typeof raw !== "object") return void 0;
1717
+ const paramByName = /* @__PURE__ */ new Map();
1718
+ for (const p of allIRParams) paramByName.set(p.name, p);
1719
+ const groups = [];
1720
+ for (const [groupName, groupDef] of Object.entries(raw)) {
1721
+ if (!groupDef || typeof groupDef !== "object") throw new Error(`Malformed x-mutually-exclusive-parameter-groups.${groupName} in ${operationContext}: expected an object with "optional" and "variants".`);
1722
+ if (typeof groupDef.optional !== "boolean") throw new Error(`Malformed x-mutually-exclusive-parameter-groups.${groupName}.optional in ${operationContext}: expected a boolean.`);
1723
+ if (!groupDef.variants || typeof groupDef.variants !== "object") throw new Error(`Malformed x-mutually-exclusive-parameter-groups.${groupName}.variants in ${operationContext}: expected an object mapping variant names to parameter name arrays.`);
1724
+ if (Object.keys(groupDef.variants).length === 0) throw new Error(`Malformed x-mutually-exclusive-parameter-groups.${groupName} in ${operationContext}: group has zero variants.`);
1725
+ const variants = Object.entries(groupDef.variants).map(([variantName, paramNames]) => {
1726
+ if (!Array.isArray(paramNames) || paramNames.length === 0) throw new Error(`Malformed x-mutually-exclusive-parameter-groups.${groupName}.variants.${variantName} in ${operationContext}: expected a non-empty array of parameter names.`);
1727
+ return {
1728
+ name: variantName,
1729
+ parameters: paramNames.map((pName) => {
1730
+ const irParam = paramByName.get(pName);
1731
+ if (!irParam) throw new Error(`x-mutually-exclusive-parameter-groups.${groupName}.variants.${variantName} references parameter "${pName}" which does not exist in the operation's parameters[] (${operationContext}).`);
1732
+ return irParam;
1733
+ })
1734
+ };
1735
+ });
1736
+ groups.push({
1737
+ name: groupName,
1738
+ optional: groupDef.optional,
1739
+ variants
1740
+ });
1741
+ }
1742
+ return groups.length > 0 ? groups : void 0;
1743
+ }
1744
+ /**
1745
+ * Extract mutually-exclusive body parameter groups from the
1746
+ * `x-mutually-exclusive-body-groups` operation extension. Unlike query
1747
+ * parameter groups (whose parameters live in operation.queryParams), body
1748
+ * group parameters are synthetic — built from the oneOf variant schemas in
1749
+ * the request body. Emitters use the group's presence to generate sum-type
1750
+ * interfaces and custom JSON marshalling instead of flat optional fields.
1751
+ */
1752
+ function extractBodyParameterGroups(op, operationContext) {
1753
+ const raw = op["x-mutually-exclusive-body-groups"];
1754
+ if (!raw || typeof raw !== "object") return void 0;
1755
+ const bodySchema = op.requestBody?.content?.["application/json"]?.schema;
1756
+ const variantFieldSchemas = /* @__PURE__ */ new Map();
1757
+ if (bodySchema) {
1758
+ for (const sub of bodySchema.allOf ?? []) for (const variant of sub.oneOf ?? []) if (variant.properties) {
1759
+ for (const [name, fieldSchema] of Object.entries(variant.properties)) if (fieldSchema) variantFieldSchemas.set(name, fieldSchema);
1760
+ }
1761
+ for (const variant of bodySchema.oneOf ?? []) if (variant.properties) {
1762
+ for (const [name, fieldSchema] of Object.entries(variant.properties)) if (fieldSchema) variantFieldSchemas.set(name, fieldSchema);
1763
+ }
1764
+ }
1765
+ const groups = [];
1766
+ for (const [groupName, groupDef] of Object.entries(raw)) {
1767
+ if (!groupDef || typeof groupDef !== "object") continue;
1768
+ if (typeof groupDef.optional !== "boolean") continue;
1769
+ if (!groupDef.variants || typeof groupDef.variants !== "object") continue;
1770
+ if (Object.keys(groupDef.variants).length === 0) continue;
1771
+ const variants = Object.entries(groupDef.variants).map(([variantName, paramNames]) => {
1772
+ if (!Array.isArray(paramNames) || paramNames.length === 0) throw new Error(`Malformed x-mutually-exclusive-body-groups.${groupName}.variants.${variantName} in ${operationContext}: expected a non-empty array of parameter names.`);
1773
+ return {
1774
+ name: variantName,
1775
+ parameters: paramNames.map((pName) => {
1776
+ const fieldSchema = variantFieldSchemas.get(pName);
1777
+ return {
1778
+ name: pName,
1779
+ type: fieldSchema ? schemaToTypeRef(fieldSchema, toPascalCase(pName)) : {
1780
+ kind: "primitive",
1781
+ type: "string"
1782
+ },
1783
+ required: false,
1784
+ description: fieldSchema?.description
1785
+ };
1786
+ })
1787
+ };
1788
+ });
1789
+ groups.push({
1790
+ name: groupName,
1791
+ optional: groupDef.optional,
1792
+ variants
1793
+ });
1794
+ }
1795
+ return groups.length > 0 ? groups : void 0;
1796
+ }
1675
1797
  function buildOperation(method, path, op, pathLevelParams, operationIdTransform, serviceName) {
1676
1798
  const allParams = [...pathLevelParams, ...op.parameters ?? []];
1677
1799
  const hasIdempotencyHeader = allParams.some((p) => p.in === "header" && p.name.toLowerCase() === "idempotency-key");
@@ -1697,6 +1819,16 @@ function buildOperation(method, path, op, pathLevelParams, operationIdTransform,
1697
1819
  };
1698
1820
  } else if (paginationFromParams) pagination = paginationFromParams;
1699
1821
  const security = extractOperationSecurity(op.security);
1822
+ const allIRParams = [
1823
+ ...pathParams,
1824
+ ...queryParams,
1825
+ ...headerParams,
1826
+ ...cookieParams
1827
+ ];
1828
+ const opLabel = op.operationId ?? `${method.toUpperCase()} ${path}`;
1829
+ const queryParamGroups = extractParameterGroups(op, allIRParams, opLabel);
1830
+ const bodyParamGroups = extractBodyParameterGroups(op, opLabel);
1831
+ const parameterGroups = queryParamGroups || bodyParamGroups ? [...queryParamGroups ?? [], ...bodyParamGroups ?? []] : void 0;
1700
1832
  return {
1701
1833
  operation: {
1702
1834
  name: inferOperationName(method, path, op.operationId, operationIdTransform),
@@ -1716,7 +1848,8 @@ function buildOperation(method, path, op, pathLevelParams, operationIdTransform,
1716
1848
  injectIdempotencyKey: hasIdempotencyHeader,
1717
1849
  deprecated: op.deprecated || void 0,
1718
1850
  async: op["x-oagen-async"],
1719
- security
1851
+ security,
1852
+ parameterGroups
1720
1853
  },
1721
1854
  inlineModels
1722
1855
  };
@@ -2971,6 +3104,7 @@ const nodeMergeAdapter = {
2971
3104
  const newText = newMembers.map((m) => m.text).join("\n");
2972
3105
  for (const match of newText.matchAll(/this\.(\w+)/g)) {
2973
3106
  const propName = match[1];
3107
+ if (propName === "workos") continue;
2974
3108
  if (propName && !existingMemberKeys.has(propName)) return true;
2975
3109
  }
2976
3110
  return false;
@@ -3627,6 +3761,40 @@ function buildIgnoredSymbolNames(docstrings, regions) {
3627
3761
  for (const [name, info] of docstrings) if (regions.some((r) => info.declStartIndex >= r.startIndex && info.declStartIndex <= r.endIndex)) ignored.add(name);
3628
3762
  return ignored;
3629
3763
  }
3764
+ function clampIndex(value, contentLength) {
3765
+ if (!Number.isFinite(value)) return 0;
3766
+ if (value < 0) return 0;
3767
+ if (value > contentLength) return contentLength;
3768
+ return value;
3769
+ }
3770
+ /**
3771
+ * Resolve the declaration line start for insertion using the current content,
3772
+ * instead of relying on parser column arithmetic. This avoids misplaced inserts
3773
+ * when parser columns/offsets drift.
3774
+ */
3775
+ function declarationLineStart(content, declStartIndex) {
3776
+ const idx = clampIndex(declStartIndex, content.length);
3777
+ const prevNewline = content.lastIndexOf("\n", Math.max(0, idx - 1));
3778
+ return prevNewline === -1 ? 0 : prevNewline + 1;
3779
+ }
3780
+ /**
3781
+ * Resolve a docstring range by searching for the nearest matching text before
3782
+ * the declaration start. Falls back to parser-provided offsets when search fails.
3783
+ */
3784
+ function resolveDocRange(content, docText, declStartIndex, fallbackStart, fallbackEnd) {
3785
+ const safeDecl = clampIndex(declStartIndex, content.length);
3786
+ const safeFallbackStart = clampIndex(fallbackStart, content.length);
3787
+ const safeFallbackEnd = clampIndex(fallbackEnd, content.length);
3788
+ const found = content.lastIndexOf(docText, safeDecl);
3789
+ if (found !== -1) return {
3790
+ start: found,
3791
+ end: found + docText.length
3792
+ };
3793
+ return {
3794
+ start: safeFallbackStart,
3795
+ end: safeFallbackEnd
3796
+ };
3797
+ }
3630
3798
  /**
3631
3799
  * Check if a tree-sitter grammar is configured for the given language.
3632
3800
  */
@@ -3675,15 +3843,17 @@ async function extractClassEndLines(source, language) {
3675
3843
  if (source[body ? body.endIndex - 1 : node.endIndex - 1] === "}") map.set(name, { bodyEndLine: endRow });
3676
3844
  else map.set(name, { bodyEndLine: endRow + 1 });
3677
3845
  }
3678
- for (const child of tree.rootNode.children) {
3679
- if (child.type === "class_definition" || child.type === "class_declaration") processClassNode(child);
3680
- if (child.type === "decorated_definition") {
3681
- for (const inner of child.namedChildren) if (inner.type === "class_definition") processClassNode(inner);
3846
+ function walk(node) {
3847
+ if (node.type === "class" || node.type === "class_definition" || node.type === "class_declaration") processClassNode(node);
3848
+ if (node.type === "decorated_definition") {
3849
+ for (const inner of node.namedChildren) if (inner.type === "class_definition") processClassNode(inner);
3682
3850
  }
3683
- if (child.type === "export_statement") {
3684
- for (const inner of child.namedChildren) if (inner.type === "class_declaration") processClassNode(inner);
3851
+ if (node.type === "export_statement") {
3852
+ for (const inner of node.namedChildren) if (inner.type === "class_declaration") processClassNode(inner);
3685
3853
  }
3854
+ for (const child of node.namedChildren) walk(child);
3686
3855
  }
3856
+ for (const child of tree.rootNode.children) walk(child);
3687
3857
  return map;
3688
3858
  }
3689
3859
  /**
@@ -3903,15 +4073,16 @@ async function mergeIntoExisting(existingContent, generatedContent, language, he
3903
4073
  if (genDoc) if (existInfo.docstring) {
3904
4074
  if (!existInfo.docstring.text.includes("@oagen-ignore") && existInfo.docstring.text !== genDoc.text) {
3905
4075
  const newText = preserveDeprecatedTags(existInfo.docstring.text, genDoc.text);
4076
+ const range = resolveDocRange(result, existInfo.docstring.text, existInfo.declStartIndex, existInfo.docstring.startIndex, existInfo.docstring.endIndex);
3906
4077
  edits.push({
3907
- start: existInfo.docstring.startIndex,
3908
- end: existInfo.docstring.endIndex,
4078
+ start: range.start,
4079
+ end: range.end,
3909
4080
  newText
3910
4081
  });
3911
4082
  docstringUpdates++;
3912
4083
  }
3913
4084
  } else {
3914
- const lineStart = existInfo.declStartIndex - existInfo.declColumn;
4085
+ const lineStart = declarationLineStart(result, existInfo.declStartIndex);
3915
4086
  const indent = " ".repeat(existInfo.declColumn);
3916
4087
  edits.push({
3917
4088
  start: lineStart,
@@ -3929,15 +4100,16 @@ async function mergeIntoExisting(existingContent, generatedContent, language, he
3929
4100
  if (existMember.docstring) {
3930
4101
  if (!existMember.docstring.text.includes("@oagen-ignore") && existMember.docstring.text !== genMember.docstring.text) {
3931
4102
  const newText = preserveDeprecatedTags(existMember.docstring.text, genMember.docstring.text);
4103
+ const range = resolveDocRange(result, existMember.docstring.text, existMember.declStartIndex, existMember.docstring.startIndex, existMember.docstring.endIndex);
3932
4104
  edits.push({
3933
- start: existMember.docstring.startIndex,
3934
- end: existMember.docstring.endIndex,
4105
+ start: range.start,
4106
+ end: range.end,
3935
4107
  newText
3936
4108
  });
3937
4109
  docstringUpdates++;
3938
4110
  }
3939
4111
  } else {
3940
- const lineStart = existMember.declStartIndex - existMember.declColumn;
4112
+ const lineStart = declarationLineStart(result, existMember.declStartIndex);
3941
4113
  const indent = " ".repeat(existMember.declColumn);
3942
4114
  edits.push({
3943
4115
  start: lineStart,
@@ -3982,15 +4154,16 @@ async function mergeIntoExisting(existingContent, generatedContent, language, he
3982
4154
  if (existMember.docstring.text.includes("@oagen-ignore")) continue;
3983
4155
  if (existMember.docstring.text !== genMember.docstring.text) {
3984
4156
  const newText = preserveDeprecatedTags(existMember.docstring.text, genMember.docstring.text);
4157
+ const range = resolveDocRange(result, existMember.docstring.text, existMember.declStartIndex, existMember.docstring.startIndex, existMember.docstring.endIndex);
3985
4158
  edits.push({
3986
- start: existMember.docstring.startIndex,
3987
- end: existMember.docstring.endIndex,
4159
+ start: range.start,
4160
+ end: range.end,
3988
4161
  newText
3989
4162
  });
3990
4163
  docstringUpdates++;
3991
4164
  }
3992
4165
  } else {
3993
- const lineStart = existMember.declStartIndex - existMember.declColumn;
4166
+ const lineStart = declarationLineStart(result, existMember.declStartIndex);
3994
4167
  const indent = " ".repeat(existMember.declColumn);
3995
4168
  edits.push({
3996
4169
  start: lineStart,
@@ -4163,7 +4336,7 @@ async function overwriteWithPreservedRegions(existingContent, generatedContent,
4163
4336
  });
4164
4337
  }
4165
4338
  if (blocks.length === 0) return generatedContent;
4166
- const result = [...generatedLines];
4339
+ const result = stripIgnoreRegions(generatedLines);
4167
4340
  spliceExtraImports(existingLines, result);
4168
4341
  const classEndLines = await extractClassEndLines(result.join("\n"), language);
4169
4342
  const blocksByClass = /* @__PURE__ */ new Map();
@@ -4230,10 +4403,33 @@ async function overwriteWithPreservedRegions(existingContent, generatedContent,
4230
4403
  for (const ins of insertions) result.splice(ins.line, 0, ...ins.content);
4231
4404
  return result.join("\n");
4232
4405
  }
4406
+ /**
4407
+ * Remove every `@oagen-ignore-start`..`@oagen-ignore-end` region from the
4408
+ * supplied lines, collapsing stretches of blank lines that get left behind.
4409
+ * Used to drop generator-emitted seed blocks before splicing the on-disk
4410
+ * versions back in — otherwise every regen doubles up.
4411
+ */
4412
+ function stripIgnoreRegions(lines) {
4413
+ const out = [];
4414
+ let skipping = false;
4415
+ for (const line of lines) {
4416
+ if (!skipping && line.includes("@oagen-ignore-start")) {
4417
+ skipping = true;
4418
+ while (out.length > 0 && out[out.length - 1].trim() === "") out.pop();
4419
+ continue;
4420
+ }
4421
+ if (skipping) {
4422
+ if (line.includes("@oagen-ignore-end")) skipping = false;
4423
+ continue;
4424
+ }
4425
+ out.push(line);
4426
+ }
4427
+ return out;
4428
+ }
4233
4429
  /** Scan backwards from `lineIdx` to find the nearest class declaration name. */
4234
4430
  function findContainingClass(lines, lineIdx) {
4235
4431
  for (let j = lineIdx - 1; j >= 0; j--) {
4236
- const m = lines[j].match(/^(?:export\s+)?class\s+(\w+)/);
4432
+ const m = lines[j].match(/^\s*(?:export\s+)?class\s+(\w+)/);
4237
4433
  if (m) return m[1];
4238
4434
  }
4239
4435
  return "";
@@ -4243,7 +4439,7 @@ function findContainingClass(lines, lineIdx) {
4243
4439
  * by scanning backwards from `beforeLine`.
4244
4440
  */
4245
4441
  function findClassDeclarationStart(lines, className, beforeLine) {
4246
- const classPattern = new RegExp(`^(?:export\\s+)?class\\s+${className}\\b`);
4442
+ const classPattern = new RegExp(`^\\s*(?:export\\s+)?class\\s+${className}\\b`);
4247
4443
  for (let i = beforeLine - 1; i >= 0; i--) {
4248
4444
  if (!classPattern.test(lines[i])) continue;
4249
4445
  let start = i;
@@ -4712,7 +4908,8 @@ async function resolveFileToDirectoryConflicts(files, targetDir) {
4712
4908
  };
4713
4909
  }
4714
4910
  async function integrateGeneratedFiles(opts) {
4715
- const { files: resolved, removals } = await resolveFileToDirectoryConflicts(await treeShakeFiles(mapFilesForTargetIntegration(opts.files, opts.language), opts.language), opts.targetDir);
4911
+ const mapped = mapFilesForTargetIntegration(opts.files, opts.language);
4912
+ const { files: resolved, removals } = await resolveFileToDirectoryConflicts(await treeShakeFiles(mapped, opts.language), opts.targetDir);
4716
4913
  const result = await writeFiles(resolved, opts.targetDir, {
4717
4914
  language: opts.language,
4718
4915
  header: opts.header
@@ -4722,7 +4919,7 @@ async function integrateGeneratedFiles(opts) {
4722
4919
  } catch {}
4723
4920
  return {
4724
4921
  ...result,
4725
- emittedPaths: resolved.map((f) => f.path)
4922
+ emittedPaths: mapped.map((f) => f.path)
4726
4923
  };
4727
4924
  }
4728
4925
  //#endregion
@@ -4746,6 +4943,7 @@ async function formatTargetFiles(emitter, targetDir, filePaths) {
4746
4943
  if (!formatCmd) return;
4747
4944
  const absolutePaths = filePaths.map((f) => path$1.resolve(targetDir, f));
4748
4945
  const batchSize = formatCmd.batchSize ?? 100;
4946
+ process.stderr.write(`[oagen] formatting ${absolutePaths.length} file(s) with ${formatCmd.cmd} (batch ${batchSize})\n`);
4749
4947
  for (let i = 0; i < absolutePaths.length; i += batchSize) {
4750
4948
  const batch = absolutePaths.slice(i, i + batchSize);
4751
4949
  try {
@@ -4754,7 +4952,10 @@ async function formatTargetFiles(emitter, targetDir, filePaths) {
4754
4952
  maxBuffer: 10 * 1024 * 1024,
4755
4953
  timeout: 12e4
4756
4954
  });
4757
- } catch {}
4955
+ } catch (err) {
4956
+ const msg = err instanceof Error ? err.message : String(err);
4957
+ process.stderr.write(`[oagen] formatter batch failed: ${msg}\n`);
4958
+ }
4758
4959
  }
4759
4960
  }
4760
4961
  //#endregion