@workos/oagen-emitters 0.14.0 → 0.14.2

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.
@@ -2286,6 +2286,24 @@ dumper.dump;
2286
2286
  //#endregion
2287
2287
  //#region src/shared/model-utils.ts
2288
2288
  /**
2289
+ * Collect model names referenced as the return type of any non-paginated
2290
+ * operation. The list-wrapper skip rule below assumes a wrapper is always
2291
+ * replaced by the SDK's pagination machinery — but a few endpoints
2292
+ * (e.g. `GET /vault/v1/kv/{id}/versions`) have a list-envelope response
2293
+ * shape with no pagination params, so the parser leaves them as a plain
2294
+ * model reference. We must still emit those wrappers as regular models;
2295
+ * otherwise the generated resource code references an undefined name.
2296
+ */
2297
+ function collectNonPaginatedResponseModelNames(services) {
2298
+ const names = /* @__PURE__ */ new Set();
2299
+ for (const service of services) for (const op of service.operations) {
2300
+ if (op.pagination) continue;
2301
+ walkTypeRef(op.response, { model: (r) => names.add(r.name) });
2302
+ for (const sr of op.successResponses ?? []) walkTypeRef(sr.type, { model: (r) => names.add(r.name) });
2303
+ }
2304
+ return names;
2305
+ }
2306
+ /**
2289
2307
  * Detect whether a model is a list wrapper -- the standard paginated
2290
2308
  * list envelope with `data` (array), `list_metadata`, and optionally `object: 'list'`.
2291
2309
  *
@@ -2316,6 +2334,35 @@ function isListMetadataModel(model) {
2316
2334
  if (!before || !after) return false;
2317
2335
  return isNullableString(before) && isNullableString(after);
2318
2336
  }
2337
+ /**
2338
+ * Compute the `ListMetadata`-shape model names that must still be emitted
2339
+ * because some surviving wrapper references them.
2340
+ *
2341
+ * Each language emitter blanket-skips `isListMetadataModel` models on the
2342
+ * assumption that the SDK's shared pagination wrapper subsumes them. That
2343
+ * is correct for paginated list envelopes (the iterator unwraps the
2344
+ * envelope and `list_metadata` is handled at runtime), but a *non-paginated*
2345
+ * wrapper like vault's `VersionListResponse` still has a
2346
+ * `list_metadata: ListMetadata` field — and skipping emission of
2347
+ * `ListMetadata` leaves the wrapper's interface importing from a file that
2348
+ * was never written.
2349
+ *
2350
+ * Pass the same `nonPaginatedRefs` set the emitter uses for its own
2351
+ * wrapper-survival decision so the two answers stay in sync.
2352
+ */
2353
+ function collectReferencedListMetadataModels(models, nonPaginatedRefs) {
2354
+ const listMetadataNames = /* @__PURE__ */ new Set();
2355
+ for (const m of models) if (isListMetadataModel(m)) listMetadataNames.add(m.name);
2356
+ if (listMetadataNames.size === 0) return /* @__PURE__ */ new Set();
2357
+ const referenced = /* @__PURE__ */ new Set();
2358
+ for (const model of models) {
2359
+ if (isListMetadataModel(model)) continue;
2360
+ if (isListWrapperModel(model) && !nonPaginatedRefs.has(model.name)) continue;
2361
+ const deps = collectFieldDependencies(model);
2362
+ for (const dep of deps.models) if (listMetadataNames.has(dep)) referenced.add(dep);
2363
+ }
2364
+ return referenced;
2365
+ }
2319
2366
  /** Check if a field type is nullable string (nullable<string> or just string). */
2320
2367
  function isNullableString(field) {
2321
2368
  if (field.type.kind === "primitive" && field.type.type === "string") return true;
@@ -2636,7 +2683,7 @@ function detectDiscriminators(models) {
2636
2683
  * Returns a new array of enriched models (original models are not mutated).
2637
2684
  * Synthetic enums are stored internally; retrieve them via `getSyntheticEnums()`.
2638
2685
  */
2639
- function enrichModelsFromSpec(models) {
2686
+ function enrichModelsFromSpec(models, enums = []) {
2640
2687
  if (!loadRawSpec()) {
2641
2688
  _lastSyntheticEnums = [];
2642
2689
  return models;
@@ -2646,6 +2693,10 @@ function enrichModelsFromSpec(models) {
2646
2693
  collector.usedNames.add(m.name);
2647
2694
  collector.usedNames.add(toSnakeCase(m.name));
2648
2695
  }
2696
+ for (const e of enums) {
2697
+ collector.usedNames.add(e.name);
2698
+ collector.usedNames.add(toSnakeCase(e.name));
2699
+ }
2649
2700
  const enriched2 = models.map((model) => {
2650
2701
  const rawSchema = lookupRawSchema(model.name);
2651
2702
  if (!rawSchema) return model;
@@ -2804,6 +2855,22 @@ function isAdoptedModelName(name) {
2804
2855
  return adoptedModelNames.has(name);
2805
2856
  }
2806
2857
  /**
2858
+ * Domain names that `resolveInterfaceName` reached via a structural rename
2859
+ * — the resolved name differs from the IR model's own name. `wireInterfaceName`
2860
+ * consults this set to decide whether to fire the "single-form wire" case:
2861
+ * that case is *only* meant for structurally-renamed models, where the
2862
+ * baseline owns a `*Response` interface representing the wire shape with no
2863
+ * separate `*Wire` companion. Without this signal, a freshly-emitted model
2864
+ * whose IR name already ends in `Response` (e.g. `CreateDataKeyResponse`)
2865
+ * would land in the same case as soon as a prior buggy regen wrote the
2866
+ * baseline — producing two `export interface CreateDataKeyResponse { ... }`
2867
+ * declarations in the same file.
2868
+ */
2869
+ let structurallyRenamedDomainNames = /* @__PURE__ */ new Set();
2870
+ function setStructurallyRenamedDomainNames(names) {
2871
+ structurallyRenamedDomainNames = names;
2872
+ }
2873
+ /**
2807
2874
  * Wire/response interface name.
2808
2875
  *
2809
2876
  * Resolution order:
@@ -2826,7 +2893,7 @@ function wireInterfaceName(domainName) {
2826
2893
  if (domainName.endsWith("Response")) {
2827
2894
  const wireForm = `${domainName}Wire`;
2828
2895
  if (baselineInterfaceNames.has(wireForm)) return wireForm;
2829
- if (baselineInterfaceNames.has(domainName)) return domainName;
2896
+ if (structurallyRenamedDomainNames.has(domainName) && baselineInterfaceNames.has(domainName)) return domainName;
2830
2897
  return wireForm;
2831
2898
  }
2832
2899
  return `${domainName}Response`;
@@ -4870,8 +4937,11 @@ function generateResourceClass(service, ctx) {
4870
4937
  importedTypeNames.add(resolved);
4871
4938
  const modelServiceDir = resolveDir(modelToService.get(name));
4872
4939
  const relPath = modelServiceDir === serviceDir ? `./interfaces/${fileName$3(name)}.interface` : `../${modelServiceDir}/interfaces/${fileName$3(name)}.interface`;
4873
- if (usedWireTypes.has(resolved)) lines.push(`import type { ${resolved}, ${wireInterfaceName(resolved)} } from '${relPath}';`);
4874
- else lines.push(`import type { ${resolved} } from '${relPath}';`);
4940
+ if (usedWireTypes.has(resolved)) {
4941
+ const wireName = wireInterfaceName(resolved);
4942
+ if (wireName === resolved) lines.push(`import type { ${resolved} } from '${relPath}';`);
4943
+ else lines.push(`import type { ${resolved}, ${wireName} } from '${relPath}';`);
4944
+ } else lines.push(`import type { ${resolved} } from '${relPath}';`);
4875
4945
  }
4876
4946
  for (const name of requestModels) {
4877
4947
  if (allModels.has(name)) continue;
@@ -5799,11 +5869,13 @@ function generateFixtures$5(spec, ctx) {
5799
5869
  walk(field.type);
5800
5870
  }
5801
5871
  }
5872
+ const nonPaginatedRefs = collectNonPaginatedResponseModelNames(spec.services);
5873
+ const listMetadataNeeded = collectReferencedListMetadataModels(spec.models, nonPaginatedRefs);
5802
5874
  const seenFixturePaths = /* @__PURE__ */ new Set();
5803
5875
  for (const model of spec.models) {
5804
5876
  if (!fixtureReachable.has(model.name)) continue;
5805
- if (isListMetadataModel(model)) continue;
5806
- if (isListWrapperModel(model)) continue;
5877
+ if (isListMetadataModel(model) && !listMetadataNeeded.has(model.name)) continue;
5878
+ if (isListWrapperModel(model) && !nonPaginatedRefs.has(model.name)) continue;
5807
5879
  const fixturePath = `src/${resolveDir(modelToService.get(model.name))}/fixtures/${fileName$3(model.name)}.json`;
5808
5880
  if (seenFixturePaths.has(fixturePath)) continue;
5809
5881
  seenFixturePaths.add(fixturePath);
@@ -5991,12 +6063,14 @@ function generateModels$7(models, ctx, shared) {
5991
6063
  }
5992
6064
  }
5993
6065
  const discriminatedSkip = ctx._discriminatedModelNames;
6066
+ const nonPaginatedRefs = collectNonPaginatedResponseModelNames(ctx.spec.services);
6067
+ const listMetadataNeeded = collectReferencedListMetadataModels(models, nonPaginatedRefs);
5994
6068
  for (const originalModel of models) {
5995
6069
  const model = projectedByName.get(originalModel.name) ?? originalModel;
5996
6070
  if (!reachableModels.has(model.name)) continue;
5997
6071
  if (interfaceEligibleModels && !interfaceEligibleModels.has(model.name)) continue;
5998
- if (isListMetadataModel(model)) continue;
5999
- if (isListWrapperModel(model)) continue;
6072
+ if (isListMetadataModel(model) && !listMetadataNeeded.has(model.name)) continue;
6073
+ if (isListWrapperModel(model) && !nonPaginatedRefs.has(model.name)) continue;
6000
6074
  if (discriminatedSkip?.has(model.name)) continue;
6001
6075
  const service = modelToService.get(model.name);
6002
6076
  const isOwnedModel = isNodeOwnedService(ctx, service);
@@ -6185,25 +6259,27 @@ function generateModels$7(models, ctx, shared) {
6185
6259
  lines.push("}");
6186
6260
  }
6187
6261
  lines.push("");
6188
- const seenWireFields = /* @__PURE__ */ new Set();
6189
- if (model.fields.length === 0) lines.push(`export type ${responseName}${typeParams} = object;`);
6190
- else {
6191
- lines.push(`export interface ${responseName}${typeParams} {`);
6192
- for (const field of model.fields) {
6193
- const wireField = wireFieldName(field.name);
6194
- if (seenWireFields.has(wireField)) continue;
6195
- seenWireFields.add(wireField);
6196
- const baselineField = baselineResponse?.fields?.[wireField];
6197
- if (baselineField && baselineTypeResolvable(baselineField.type, importableNames) && baselineFieldCompatible(baselineField, field)) {
6198
- const opt = baselineField.optional ? "?" : "";
6199
- lines.push(` ${wireField}${opt}: ${baselineField.type};`);
6200
- } else {
6201
- const isNewFieldOnExistingModel = baselineResponse && !baselineField;
6202
- const opt = baselineField?.optional === true || !field.required || isNewFieldOnExistingModel ? "?" : "";
6203
- lines.push(` ${wireField}${opt}: ${mapWireTypeRef(field.type, modelWireTypeRefOpts)};`);
6262
+ if (responseName !== domainName) {
6263
+ const seenWireFields = /* @__PURE__ */ new Set();
6264
+ if (model.fields.length === 0) lines.push(`export type ${responseName}${typeParams} = object;`);
6265
+ else {
6266
+ lines.push(`export interface ${responseName}${typeParams} {`);
6267
+ for (const field of model.fields) {
6268
+ const wireField = wireFieldName(field.name);
6269
+ if (seenWireFields.has(wireField)) continue;
6270
+ seenWireFields.add(wireField);
6271
+ const baselineField = baselineResponse?.fields?.[wireField];
6272
+ if (baselineField && baselineTypeResolvable(baselineField.type, importableNames) && baselineFieldCompatible(baselineField, field)) {
6273
+ const opt = baselineField.optional ? "?" : "";
6274
+ lines.push(` ${wireField}${opt}: ${baselineField.type};`);
6275
+ } else {
6276
+ const isNewFieldOnExistingModel = baselineResponse && !baselineField;
6277
+ const opt = baselineField?.optional === true || !field.required || isNewFieldOnExistingModel ? "?" : "";
6278
+ lines.push(` ${wireField}${opt}: ${mapWireTypeRef(field.type, modelWireTypeRefOpts)};`);
6279
+ }
6204
6280
  }
6281
+ lines.push("}");
6205
6282
  }
6206
- lines.push("}");
6207
6283
  }
6208
6284
  const filePath = `src/${dirName}/interfaces/${fileName$3(model.name)}.interface.ts`;
6209
6285
  if (ctx.apiSurface && ctx.targetDir) {
@@ -6330,13 +6406,15 @@ function generateSerializers(models, ctx, shared) {
6330
6406
  }
6331
6407
  }
6332
6408
  const discriminatedSerializerSkip = ctx._discriminatedModelNames;
6409
+ const serializerNonPaginatedRefs = collectNonPaginatedResponseModelNames(ctx.spec.services);
6410
+ const serializerListMetadataNeeded = collectReferencedListMetadataModels(models, serializerNonPaginatedRefs);
6333
6411
  const eligibleModels = [];
6334
6412
  for (const originalModel of models) {
6335
6413
  const model = projectedByName.get(originalModel.name) ?? originalModel;
6336
6414
  if (!serializerReachable.has(model.name)) continue;
6337
6415
  if (serializerEligibleModels && !serializerEligibleModels.has(model.name)) continue;
6338
- if (isListMetadataModel(model)) continue;
6339
- if (isListWrapperModel(model)) continue;
6416
+ if (isListMetadataModel(model) && !serializerListMetadataNeeded.has(model.name)) continue;
6417
+ if (isListWrapperModel(model) && !serializerNonPaginatedRefs.has(model.name)) continue;
6340
6418
  if (discriminatedSerializerSkip?.has(model.name)) continue;
6341
6419
  if (!isNodeOwnedService(ctx, modelToService.get(model.name)) && !modelHasNewFields(model, ctx) && !forceGenerateSerializer.has(model.name)) continue;
6342
6420
  eligibleModels.push(model);
@@ -7391,7 +7469,11 @@ function buildFieldAssertions(model, accessor, modelMap) {
7391
7469
  const domainField = fieldName$6(field.name);
7392
7470
  const fieldAccessor = isDateTimeFieldType(field.type) ? `${accessor}.${domainField}.toISOString()` : `${accessor}.${domainField}`;
7393
7471
  if (field.example !== void 0) {
7394
- if (typeof field.example === "object" && field.example !== null) assertions.push(`expect(${accessor}.${domainField}).toEqual(${JSON.stringify(field.example)});`);
7472
+ if (field.example === null) {
7473
+ assertions.push(`expect(${accessor}.${domainField}).toBeNull();`);
7474
+ continue;
7475
+ }
7476
+ if (typeof field.example === "object") assertions.push(`expect(${accessor}.${domainField}).toEqual(${JSON.stringify(field.example)});`);
7395
7477
  else {
7396
7478
  const exampleLiteral = typeof field.example === "string" ? `'${field.example}'` : String(field.example);
7397
7479
  assertions.push(`expect(${fieldAccessor}).toBe(${exampleLiteral});`);
@@ -8183,6 +8265,22 @@ function withNodeOperationOverrides(ctx) {
8183
8265
  * one oagen run, so we walk the target SDK once and reuse it.
8184
8266
  */
8185
8267
  const surfaceCache = /* @__PURE__ */ new WeakMap();
8268
+ /**
8269
+ * Paths the node emitter has produced so far in this ctx, accumulated across
8270
+ * `applyLiveSurface` calls. Drives `carryForwardManagedFiles` so files in the
8271
+ * prior manifest that we did not re-emit this run still land in the new
8272
+ * manifest as "still managed" — without that, the orchestrator's prune diff
8273
+ * treats every untouched autogen file as stale.
8274
+ */
8275
+ const emittedPathsCache = /* @__PURE__ */ new WeakMap();
8276
+ function getEmittedPaths(ctx) {
8277
+ let set = emittedPathsCache.get(ctx);
8278
+ if (!set) {
8279
+ set = /* @__PURE__ */ new Set();
8280
+ emittedPathsCache.set(ctx, set);
8281
+ }
8282
+ return set;
8283
+ }
8186
8284
  function getSurface(ctx) {
8187
8285
  let surface = surfaceCache.get(ctx);
8188
8286
  if (surface) return surface;
@@ -8205,6 +8303,12 @@ function getSurface(ctx) {
8205
8303
  setBaselineInterfaceNames(allInterfaces);
8206
8304
  setInlineEnumUnions(/* @__PURE__ */ new Map());
8207
8305
  setAdoptedModelNames(computeAdoptedModelNames(ctx, surface));
8306
+ const renamed = /* @__PURE__ */ new Set();
8307
+ for (const model of ctx.spec.models) {
8308
+ const resolved = resolveInterfaceName(model.name, ctx);
8309
+ if (resolved !== model.name) renamed.add(resolved);
8310
+ }
8311
+ setStructurallyRenamedDomainNames(renamed);
8208
8312
  return surface;
8209
8313
  }
8210
8314
  /**
@@ -8229,8 +8333,10 @@ function getSurface(ctx) {
8229
8333
  * `integrateTarget: false` files (smoke-manifest.json etc.) are also dropped:
8230
8334
  * with no `--target` step they would otherwise land as untracked cruft.
8231
8335
  *
8232
- * Note: pairing this with `--no-prune` is required for stable behavior — see
8233
- * `scripts/sdk-generate.sh` in the spec repo, which enables it for `--lang node`.
8336
+ * Note: the carry-forward step in `generateTests` re-declares prior-manifest
8337
+ * paths we didn't touch this run, so the orchestrator's prune diff stays
8338
+ * accurate without needing `--no-prune` at the call site. See
8339
+ * `carryForwardManagedFiles` below.
8234
8340
  */
8235
8341
  /**
8236
8342
  * `*.spec.ts`, `*.test.ts`, and JSON fixtures under `fixtures/` are owned by
@@ -8380,6 +8486,52 @@ function applyLiveSurface(files, ctx, surface) {
8380
8486
  if (f.content && !f.content.endsWith("\n")) f.content += "\n";
8381
8487
  out.push(f);
8382
8488
  }
8489
+ const emitted = getEmittedPaths(ctx);
8490
+ for (const f of out) emitted.add(f.path);
8491
+ return out;
8492
+ }
8493
+ /**
8494
+ * Re-declare prior-manifest paths that we did not emit this run so manifest
8495
+ * pruning can tell "intentionally removed" from "untouched but still managed."
8496
+ *
8497
+ * The node emitter only outputs files it actually wants to write each run —
8498
+ * untouched-but-up-to-date autogen files don't come back through any
8499
+ * `generateXxx` method. Without this carry-forward, the orchestrator's
8500
+ * `prevManifest.files − currentEmission` diff treats every such file as stale
8501
+ * and prunes the whole tree on a regen. That's why `scripts/sdk-generate.sh`
8502
+ * historically paired the node emitter with `--no-prune` — at the cost of
8503
+ * never pruning legitimately-removed files (e.g. an enum file orphaned by a
8504
+ * `schemaNameTransform` rename like `RadarAction` → `RadarListAction`).
8505
+ *
8506
+ * The carry-forward entry uses `skipIfExists: true`, so writer.ts skips the
8507
+ * write and only ensures the header is present (no-op for files that already
8508
+ * have it). The path still lands in `outputEmittedPaths` and therefore in the
8509
+ * new manifest, which restores correct prune semantics.
8510
+ *
8511
+ * Files dropped from the carry-forward set:
8512
+ * - Not on disk anymore (file was hand-deleted — let prune confirm absence).
8513
+ * - `@oagen-ignore-file` protected (user has explicitly taken ownership).
8514
+ * - `.ts` files that no longer carry the auto-gen header (user has taken
8515
+ * ownership in-place; the next prune cycle will clear the manifest entry).
8516
+ */
8517
+ function carryForwardManagedFiles(ctx, surface) {
8518
+ const priorPaths = ctx.priorTargetManifestPaths;
8519
+ if (!priorPaths || priorPaths.size === 0) return [];
8520
+ const emitted = getEmittedPaths(ctx);
8521
+ const out = [];
8522
+ for (const relPath of priorPaths) {
8523
+ if (emitted.has(relPath)) continue;
8524
+ if (!surface.files.has(relPath)) continue;
8525
+ if (surface.protectedFiles.has(relPath)) continue;
8526
+ if (relPath.endsWith(".ts") && !surface.autogenFiles.has(relPath)) continue;
8527
+ out.push({
8528
+ path: relPath,
8529
+ content: "",
8530
+ skipIfExists: true,
8531
+ headerPlacement: "skip"
8532
+ });
8533
+ emitted.add(relPath);
8534
+ }
8383
8535
  return out;
8384
8536
  }
8385
8537
  /**
@@ -8446,9 +8598,8 @@ const nodeEmitter = {
8446
8598
  },
8447
8599
  generateTests(spec, ctx) {
8448
8600
  const nodeCtx = withNodeOperationOverrides(ctx);
8449
- if (!nodeOptions(nodeCtx).regenerateOwnedTests) return [];
8450
8601
  const surface = getSurface(nodeCtx);
8451
- return applyLiveSurface(generateTests$7(spec, nodeCtx), nodeCtx, surface);
8602
+ return [...nodeOptions(nodeCtx).regenerateOwnedTests ? applyLiveSurface(generateTests$7(spec, nodeCtx), nodeCtx, surface) : [], ...carryForwardManagedFiles(nodeCtx, surface)];
8452
8603
  },
8453
8604
  buildOperationsMap() {
8454
8605
  return {};
@@ -9225,9 +9376,11 @@ function generateModels$6(models, ctx) {
9225
9376
  const symbolToFile = /* @__PURE__ */ new Map();
9226
9377
  const symbolToOriginalService = /* @__PURE__ */ new Map();
9227
9378
  const emittedFilePaths = /* @__PURE__ */ new Set();
9379
+ const nonPaginatedRefs = collectNonPaginatedResponseModelNames(ctx.spec.services);
9380
+ const listMetadataNeeded = collectReferencedListMetadataModels(models, nonPaginatedRefs);
9228
9381
  for (const model of models) {
9229
- if (isListWrapperModel(model)) continue;
9230
- if (isListMetadataModel(model)) continue;
9382
+ if (isListWrapperModel(model) && !nonPaginatedRefs.has(model.name)) continue;
9383
+ if (isListMetadataModel(model) && !listMetadataNeeded.has(model.name)) continue;
9231
9384
  const dirName = resolveDir(modelToService.get(model.name));
9232
9385
  const modelClassName = className$5(model.name);
9233
9386
  const modelFilePath = `src/${ctx.namespace}/${dirName}/models/${fileName$2(model.name)}.py`;
@@ -10640,7 +10793,9 @@ function generateResources$6(services, ctx) {
10640
10793
  }
10641
10794
  for (const op of allOperations) {
10642
10795
  const plan = planOperation(op);
10643
- if (plan.responseModelName && !listWrapperNames.has(plan.responseModelName)) modelImports.add(plan.responseModelName);
10796
+ if (plan.responseModelName) {
10797
+ if (!listWrapperNames.has(plan.responseModelName) || !plan.isPaginated) modelImports.add(plan.responseModelName);
10798
+ }
10644
10799
  if (op.requestBody?.kind === "model") {
10645
10800
  const requestBodyRef = op.requestBody;
10646
10801
  modelImports.add(requestBodyRef.name);
@@ -11248,9 +11403,11 @@ function generateFixtures$4(spec) {
11248
11403
  const modelMap = new Map(spec.models.map((m) => [m.name, m]));
11249
11404
  const enumMap = new Map(spec.enums.map((e) => [e.name, e]));
11250
11405
  const files = [];
11406
+ const nonPaginatedRefs = collectNonPaginatedResponseModelNames(spec.services);
11407
+ const listMetadataNeeded = collectReferencedListMetadataModels(spec.models, nonPaginatedRefs);
11251
11408
  for (const model of spec.models) {
11252
- if (isListMetadataModel(model)) continue;
11253
- if (isListWrapperModel(model)) continue;
11409
+ if (isListMetadataModel(model) && !listMetadataNeeded.has(model.name)) continue;
11410
+ if (isListWrapperModel(model) && !nonPaginatedRefs.has(model.name)) continue;
11254
11411
  if (model.fields.length === 0) continue;
11255
11412
  const fixture = generateModelFixture$4(model, modelMap, enumMap);
11256
11413
  files.push({
@@ -12328,7 +12485,9 @@ function generateModelRoundTripTests(spec, ctx) {
12328
12485
  }
12329
12486
  }
12330
12487
  for (const name of responseModelNames) requestOnlyModelNames.delete(name);
12331
- const models = spec.models.filter((m) => !isListWrapperModel(m) && !isListMetadataModel(m) && !requestOnlyModelNames.has(m.name));
12488
+ const nonPaginatedRefs = collectNonPaginatedResponseModelNames(spec.services);
12489
+ const listMetadataNeeded = collectReferencedListMetadataModels(spec.models, nonPaginatedRefs);
12490
+ const models = spec.models.filter((m) => !(isListWrapperModel(m) && !nonPaginatedRefs.has(m.name)) && !(isListMetadataModel(m) && !listMetadataNeeded.has(m.name)) && !requestOnlyModelNames.has(m.name));
12332
12491
  if (models.length === 0) return null;
12333
12492
  const modelToService = computeSchemaPlacement(spec, ctx).originalModelToService;
12334
12493
  const roundTripDirMap = buildMountDirMap$1(ctx);
@@ -12769,9 +12928,10 @@ function generateModels$5(models, ctx) {
12769
12928
  ].join("\n"),
12770
12929
  overwriteExisting: true
12771
12930
  });
12931
+ const nonPaginatedRefs = collectNonPaginatedResponseModelNames(ctx.spec.services);
12772
12932
  for (const model of models) {
12773
12933
  if (isListMetadataModel(model)) continue;
12774
- if (isListWrapperModel(model)) continue;
12934
+ if (isListWrapperModel(model) && !nonPaginatedRefs.has(model.name)) continue;
12775
12935
  const name = className$4(model.name);
12776
12936
  const lines = [];
12777
12937
  lines.push(`namespace ${ctx.namespacePascal}\\Resource;`);
@@ -14671,10 +14831,14 @@ function generateModels$4(models, ctx) {
14671
14831
  lines.push(`package ${ctx.namespace}`);
14672
14832
  lines.push("");
14673
14833
  const requestBodyOnly = collectRequestBodyOnlyModelNames$1(ctx.spec.services, models);
14834
+ const nonPaginatedRefs = collectNonPaginatedResponseModelNames(ctx.spec.services);
14835
+ const skipAsListWrapper = (m) => isListWrapperModel(m) && !nonPaginatedRefs.has(m.name);
14836
+ const listMetadataNeeded = collectReferencedListMetadataModels(models, nonPaginatedRefs);
14837
+ const skipAsListMetadata = (m) => isListMetadataModel(m) && !listMetadataNeeded.has(m.name);
14674
14838
  const modelHashMap = /* @__PURE__ */ new Map();
14675
14839
  const hashGroups = /* @__PURE__ */ new Map();
14676
14840
  for (const model of models) {
14677
- if (isListWrapperModel(model) || isListMetadataModel(model)) continue;
14841
+ if (skipAsListWrapper(model) || skipAsListMetadata(model)) continue;
14678
14842
  if (requestBodyOnly.has(model.name)) continue;
14679
14843
  const hash = structuralHash$2(model);
14680
14844
  modelHashMap.set(model.name, hash);
@@ -14691,7 +14855,7 @@ function generateModels$4(models, ctx) {
14691
14855
  }
14692
14856
  const batchedAliases = /* @__PURE__ */ new Set();
14693
14857
  for (const model of models) {
14694
- if (isListWrapperModel(model) || isListMetadataModel(model)) continue;
14858
+ if (skipAsListWrapper(model) || skipAsListMetadata(model)) continue;
14695
14859
  if (requestBodyOnly.has(model.name)) continue;
14696
14860
  const structName = className$3(model.name);
14697
14861
  const canonicalName = aliasOf.get(model.name);
@@ -16233,9 +16397,11 @@ function generateFixtures$2(spec) {
16233
16397
  const modelMap = new Map(spec.models.map((m) => [m.name, m]));
16234
16398
  const enumMap = new Map(spec.enums.map((e) => [e.name, e]));
16235
16399
  const files = [];
16400
+ const nonPaginatedRefs = collectNonPaginatedResponseModelNames(spec.services);
16401
+ const listMetadataNeeded = collectReferencedListMetadataModels(spec.models, nonPaginatedRefs);
16236
16402
  for (const model of spec.models) {
16237
- if (isListMetadataModel(model)) continue;
16238
- if (isListWrapperModel(model)) continue;
16403
+ if (isListMetadataModel(model) && !listMetadataNeeded.has(model.name)) continue;
16404
+ if (isListWrapperModel(model) && !nonPaginatedRefs.has(model.name)) continue;
16239
16405
  const fixture = model.fields.length === 0 ? {} : generateModelFixture$2(model, modelMap, enumMap);
16240
16406
  files.push({
16241
16407
  path: `testdata/${fileName$1(model.name)}.json`,
@@ -17324,7 +17490,15 @@ function isEnumRef(ref) {
17324
17490
  * omission so the API returns a clear `missing required field` error instead
17325
17491
  * of a confusing 422.
17326
17492
  */
17327
- function emitJsonPropertyAttributes(_wireName, options = {}) {
17493
+ function emitJsonPropertyAttributes(wireName, options = {}) {
17494
+ if (options.explicitWireName) {
17495
+ if (options.isRequiredEnum) return [
17496
+ ` [JsonProperty("${wireName}", DefaultValueHandling = DefaultValueHandling.Ignore)]`,
17497
+ ` [STJS.JsonIgnore(Condition = STJS.JsonIgnoreCondition.WhenWritingDefault)]`,
17498
+ ` [STJS.JsonPropertyName("${wireName}")]`
17499
+ ];
17500
+ return [` [JsonProperty("${wireName}")]`, ` [STJS.JsonPropertyName("${wireName}")]`];
17501
+ }
17328
17502
  if (options.isRequiredEnum) return [` [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]`, ` [STJS.JsonIgnore(Condition = STJS.JsonIgnoreCondition.WhenWritingDefault)]`];
17329
17503
  return [];
17330
17504
  }
@@ -17377,16 +17551,23 @@ function generateModels$3(models, ctx, discCtx) {
17377
17551
  const files = [];
17378
17552
  primeModelAliases(models);
17379
17553
  const requestBodyOnlyNames = collectRequestBodyOnlyModelNames(ctx.spec.services, models);
17554
+ const nonPaginatedRefs = collectNonPaginatedResponseModelNames(ctx.spec.services);
17555
+ const skipAsListWrapper = (m) => isListWrapperModel(m) && !nonPaginatedRefs.has(m.name);
17380
17556
  const baseFieldLookup = /* @__PURE__ */ new Map();
17381
17557
  if (discCtx) {
17382
17558
  for (const model of models) if (discCtx.discriminatorBases.has(model.name)) {
17559
+ const baseClassName = modelClassName(model.name);
17383
17560
  const fieldMap = /* @__PURE__ */ new Map();
17384
- for (const field of model.fields) fieldMap.set(fieldName$2(field.name), mapTypeRef$3(field.type));
17561
+ for (const field of model.fields) {
17562
+ let csName = fieldName$2(field.name);
17563
+ if (csName === baseClassName) csName = `${csName}Value`;
17564
+ fieldMap.set(csName, mapTypeRef$3(field.type));
17565
+ }
17385
17566
  baseFieldLookup.set(model.name, fieldMap);
17386
17567
  }
17387
17568
  }
17388
17569
  for (const model of models) {
17389
- if (isListWrapperModel(model) || isListMetadataModel(model)) continue;
17570
+ if (skipAsListWrapper(model) || isListMetadataModel(model)) continue;
17390
17571
  if (requestBodyOnlyNames.has(model.name)) continue;
17391
17572
  const csClassName = modelClassName(model.name);
17392
17573
  if (isModelAlias(model.name)) continue;
@@ -17394,7 +17575,7 @@ function generateModels$3(models, ctx, discCtx) {
17394
17575
  const fieldTypes = model.fields.map((f) => mapTypeRef$3(f.type));
17395
17576
  const needsCollections = fieldTypes.some((t) => t.startsWith("List<") || t.startsWith("Dictionary<"));
17396
17577
  const needsSystem = fieldTypes.some((t) => t.includes("DateTimeOffset"));
17397
- const needsJsonAttrs = model.fields.some((f) => f.required && isEnumRef(f.type));
17578
+ const needsJsonAttrs = model.fields.some((f) => fieldName$2(f.name) === csClassName) || model.fields.some((f) => f.required && isEnumRef(f.type));
17398
17579
  lines.push(`namespace ${ctx.namespacePascal}`);
17399
17580
  lines.push("{");
17400
17581
  if (needsSystem) lines.push(" using System;");
@@ -17420,7 +17601,9 @@ function generateModels$3(models, ctx, discCtx) {
17420
17601
  const dictObjectFields = [];
17421
17602
  const seenFieldNames = /* @__PURE__ */ new Set();
17422
17603
  for (const field of model.fields) {
17423
- const csFieldName = fieldName$2(field.name);
17604
+ let csFieldName = fieldName$2(field.name);
17605
+ const collidesWithClassName = csFieldName === csClassName;
17606
+ if (collidesWithClassName) csFieldName = `${csFieldName}Value`;
17424
17607
  if (seenFieldNames.has(csFieldName)) continue;
17425
17608
  seenFieldNames.add(csFieldName);
17426
17609
  let useNewModifier = false;
@@ -17465,7 +17648,10 @@ function generateModels$3(models, ctx, discCtx) {
17465
17648
  lines.push(` [System.Obsolete("${msg}")]`);
17466
17649
  }
17467
17650
  const isRequiredEnum = field.required && isEnumRef(field.type) && constInit === null;
17468
- lines.push(...emitJsonPropertyAttributes(field.name, { isRequiredEnum }));
17651
+ lines.push(...emitJsonPropertyAttributes(field.name, {
17652
+ isRequiredEnum,
17653
+ explicitWireName: collidesWithClassName
17654
+ }));
17469
17655
  const discriminatedUnionConverter = discriminatedUnionConverterName(field.type);
17470
17656
  if (discriminatedUnionConverter) lines.push(` [Newtonsoft.Json.JsonConverter(typeof(${discriminatedUnionConverter}))]`);
17471
17657
  const newMod = useNewModifier ? "new " : "";
@@ -20050,14 +20236,18 @@ function promoteFieldType$1(f) {
20050
20236
  * List wrappers (`{ data, list_metadata }`) and the shared `ListMetadata`
20051
20237
  * model are skipped — the hand-maintained runtime provides [Page]/[ListMetadata].
20052
20238
  */
20053
- function generateModels$2(models, _ctx) {
20239
+ function generateModels$2(models, ctx) {
20054
20240
  if (models.length === 0) return [];
20055
20241
  for (const model of models) for (const field of model.fields) mapTypeRef$2(field.type);
20056
20242
  const files = [];
20243
+ const nonPaginatedRefs = collectNonPaginatedResponseModelNames(ctx.spec.services);
20244
+ const skipAsListWrapper = (m) => isListWrapperModel(m) && !nonPaginatedRefs.has(m.name);
20245
+ const listMetadataNeeded = collectReferencedListMetadataModels(models, nonPaginatedRefs);
20246
+ const skipAsListMetadata = (m) => isListMetadataModel(m) && !listMetadataNeeded.has(m.name);
20057
20247
  modelAliasMap = null;
20058
20248
  const hashGroupsPass1 = /* @__PURE__ */ new Map();
20059
20249
  for (const model of models) {
20060
- if (isListWrapperModel(model) || isListMetadataModel(model)) continue;
20250
+ if (skipAsListWrapper(model) || skipAsListMetadata(model)) continue;
20061
20251
  if (model.fields.length === 0 && discriminatedUnions.has(className$1(model.name))) continue;
20062
20252
  const hash = structuralHash(model);
20063
20253
  if (!hashGroupsPass1.has(hash)) hashGroupsPass1.set(hash, []);
@@ -20076,7 +20266,7 @@ function generateModels$2(models, _ctx) {
20076
20266
  modelAliasMap = aliasOf;
20077
20267
  const hashGroupsPass2 = /* @__PURE__ */ new Map();
20078
20268
  for (const model of models) {
20079
- if (isListWrapperModel(model) || isListMetadataModel(model)) continue;
20269
+ if (skipAsListWrapper(model) || skipAsListMetadata(model)) continue;
20080
20270
  if (model.fields.length === 0 && discriminatedUnions.has(className$1(model.name))) continue;
20081
20271
  if (aliasOf.has(model.name)) continue;
20082
20272
  const hash = structuralHash(model);
@@ -20094,7 +20284,7 @@ function generateModels$2(models, _ctx) {
20094
20284
  }
20095
20285
  }
20096
20286
  for (const model of models) {
20097
- if (isListWrapperModel(model) || isListMetadataModel(model)) continue;
20287
+ if (skipAsListWrapper(model) || skipAsListMetadata(model)) continue;
20098
20288
  const typeName = className$1(model.name);
20099
20289
  if (model.fields.length === 0 && discriminatedUnions.has(typeName)) {
20100
20290
  files.push(emitSealedUnion(typeName, discriminatedUnions.get(typeName)));
@@ -20122,7 +20312,7 @@ function generateModels$2(models, _ctx) {
20122
20312
  }
20123
20313
  const eventMapping = [];
20124
20314
  for (const model of models) {
20125
- if (isListWrapperModel(model) || isListMetadataModel(model)) continue;
20315
+ if (skipAsListWrapper(model) || skipAsListMetadata(model)) continue;
20126
20316
  if (aliasOf.has(model.name)) continue;
20127
20317
  if (!isEventEnvelopeModel(model)) continue;
20128
20318
  const eventField = model.fields.find((f) => f.name === "event");
@@ -21433,6 +21623,8 @@ function resolveBodyModel$2(op, ctx) {
21433
21623
  function registerTypeImports(ref, imports, ctx) {
21434
21624
  const mapped = mapTypeRef$2(ref);
21435
21625
  for (const imp of implicitImportsFor(mapped)) imports.add(imp);
21626
+ const nonPaginatedRefs = collectNonPaginatedResponseModelNames(ctx.spec.services);
21627
+ const listMetadataNeeded = collectReferencedListMetadataModels(ctx.spec.models, nonPaginatedRefs);
21436
21628
  walk(ref, (r) => {
21437
21629
  if (r.kind === "enum") {
21438
21630
  const canonicalName = enumCanonicalMap.get(r.name) ?? r.name;
@@ -21440,7 +21632,11 @@ function registerTypeImports(ref, imports, ctx) {
21440
21632
  }
21441
21633
  if (r.kind === "model") {
21442
21634
  const referenced = ctx.spec.models.find((m) => m.name === r.name);
21443
- if (referenced && (isListWrapperModel(referenced) || isListMetadataModel(referenced))) return;
21635
+ if (referenced) {
21636
+ const skipWrapper = isListWrapperModel(referenced) && !nonPaginatedRefs.has(referenced.name);
21637
+ const skipMetadata = isListMetadataModel(referenced) && !listMetadataNeeded.has(referenced.name);
21638
+ if (skipWrapper || skipMetadata) return;
21639
+ }
21444
21640
  imports.add(`com.workos.models.${className$1(r.name)}`);
21445
21641
  }
21446
21642
  });
@@ -22839,10 +23035,12 @@ function generateModels$1(models, ctx) {
22839
23035
  return mountDirMap.get(service) ?? classifyUnassignedModel(modelName);
22840
23036
  };
22841
23037
  const files = [];
23038
+ const nonPaginatedRefs = collectNonPaginatedResponseModelNames(ctx.spec.services);
23039
+ const skipAsListWrapper = (m) => isListWrapperModel(m) && !nonPaginatedRefs.has(m.name);
22842
23040
  const recursiveHashes = buildRecursiveHashMap(models, enumNames);
22843
23041
  const hashGroups = /* @__PURE__ */ new Map();
22844
23042
  for (const m of models) {
22845
- if (isListWrapperModel(m) || isListMetadataModel(m)) continue;
23043
+ if (skipAsListWrapper(m) || isListMetadataModel(m)) continue;
22846
23044
  const h = recursiveHashes.get(m.name) ?? "";
22847
23045
  if (!hashGroups.has(h)) hashGroups.set(h, []);
22848
23046
  hashGroups.get(h).push(m.name);
@@ -22855,7 +23053,7 @@ function generateModels$1(models, ctx) {
22855
23053
  for (let i = 1; i < sorted.length; i++) aliasOf.set(sorted[i], canonical);
22856
23054
  }
22857
23055
  for (const model of models) {
22858
- if (isListWrapperModel(model) || isListMetadataModel(model)) continue;
23056
+ if (skipAsListWrapper(model) || isListMetadataModel(model)) continue;
22859
23057
  const cls = className(model.name);
22860
23058
  const file = fileName(model.name);
22861
23059
  const canonical = aliasOf.get(model.name);
@@ -23328,7 +23526,10 @@ function mapSorbetType$1(ref) {
23328
23526
  case "array": return `T::Array[${mapSorbetType$1(ref.items)}]`;
23329
23527
  case "model": return `WorkOS::${className(ref.name)}`;
23330
23528
  case "enum": return "String";
23331
- case "nullable": return `T.nilable(${mapSorbetType$1(ref.inner)})`;
23529
+ case "nullable": {
23530
+ const inner = mapSorbetType$1(ref.inner);
23531
+ return inner === "T.untyped" ? inner : `T.nilable(${inner})`;
23532
+ }
23332
23533
  case "literal":
23333
23534
  if (typeof ref.value === "string") return "String";
23334
23535
  if (ref.value === null) return "NilClass";
@@ -24932,6 +25133,7 @@ function generateRbiFiles(spec, ctx) {
24932
25133
  }
24933
25134
  /** Unwrap T.nilable(...) if already wrapped, to avoid double-wrapping. */
24934
25135
  function unwrapNilable(type) {
25136
+ if (type === "T.untyped") return type;
24935
25137
  const match = type.match(/^T\.nilable\((.+)\)$/);
24936
25138
  return match ? match[1] : type;
24937
25139
  }
@@ -24986,8 +25188,8 @@ function ensureTrailingNewlines$1(files) {
24986
25188
  * has its original fields restored — otherwise `ConnectApplication`-style
24987
25189
  * bases would silently lose every variant field they had previously.
24988
25190
  */
24989
- function enrichModelsForRuby(models) {
24990
- const enriched = enrichModelsFromSpec(models);
25191
+ function enrichModelsForRuby(models, enums) {
25192
+ const enriched = enrichModelsFromSpec(models, enums);
24991
25193
  const originalByName = new Map(models.map((m) => [m.name, m]));
24992
25194
  return enriched.map((m) => {
24993
25195
  if (m.discriminator && m.fields.length === 0) {
@@ -25003,7 +25205,7 @@ function enrichModelsForRuby(models) {
25003
25205
  const rubyEmitter = {
25004
25206
  language: "ruby",
25005
25207
  generateModels(models, ctx) {
25006
- return ensureTrailingNewlines$1(generateModels$1(enrichModelsForRuby(models), ctx));
25208
+ return ensureTrailingNewlines$1(generateModels$1(enrichModelsForRuby(models, ctx.spec.enums), ctx));
25007
25209
  },
25008
25210
  generateEnums(enums, ctx) {
25009
25211
  const syntheticEnums = getSyntheticEnums();
@@ -25469,7 +25671,7 @@ function renderField(field, rustField, modelName, registry) {
25469
25671
  function renderModelsBarrel(modules) {
25470
25672
  const sorted = [...new Set(modules)].sort();
25471
25673
  const lines = [];
25472
- for (const m of sorted) lines.push(`pub mod ${m};`);
25674
+ for (const m of sorted) lines.push(`mod ${m};`);
25473
25675
  lines.push("");
25474
25676
  for (const m of sorted) lines.push(`pub use ${m}::*;`);
25475
25677
  return lines.join("\n") + "\n";
@@ -26552,7 +26754,7 @@ function renderResourcesBarrel(exports) {
26552
26754
  }
26553
26755
  unique.sort((a, b) => a.module.localeCompare(b.module));
26554
26756
  const lines = [];
26555
- for (const { module } of unique) lines.push(`pub mod ${module};`);
26757
+ for (const { module } of unique) lines.push(`mod ${module};`);
26556
26758
  lines.push("");
26557
26759
  for (const { module, struct } of unique) lines.push(`pub use ${module}::${struct};`);
26558
26760
  return lines.join("\n") + "\n";
@@ -27402,4 +27604,4 @@ const workosEmittersPlugin = {
27402
27604
  //#endregion
27403
27605
  export { pythonEmitter as _, rustExtractor as a, pythonExtractor as c, rustEmitter as d, rubyEmitter as f, phpEmitter as g, goEmitter as h, kotlinExtractor as i, rubyExtractor as l, dotnetEmitter as m, elixirExtractor as n, goExtractor as o, kotlinEmitter as p, dotnetExtractor as r, phpExtractor as s, workosEmittersPlugin as t, nodeExtractor as u, nodeEmitter as v };
27404
27606
 
27405
- //# sourceMappingURL=plugin-BxVeu2v9.mjs.map
27607
+ //# sourceMappingURL=plugin-BbSmT2kj.mjs.map