@workos/oagen-emitters 0.0.1 → 0.2.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.
Files changed (41) hide show
  1. package/.github/workflows/release-please.yml +9 -1
  2. package/.husky/commit-msg +0 -0
  3. package/.husky/pre-commit +1 -0
  4. package/.husky/pre-push +1 -0
  5. package/.prettierignore +1 -0
  6. package/.release-please-manifest.json +3 -0
  7. package/.vscode/settings.json +3 -0
  8. package/CHANGELOG.md +54 -0
  9. package/README.md +2 -2
  10. package/dist/index.d.mts +7 -0
  11. package/dist/index.d.mts.map +1 -0
  12. package/dist/index.mjs +3522 -0
  13. package/dist/index.mjs.map +1 -0
  14. package/package.json +14 -18
  15. package/release-please-config.json +11 -0
  16. package/src/node/client.ts +437 -204
  17. package/src/node/common.ts +74 -4
  18. package/src/node/config.ts +1 -0
  19. package/src/node/enums.ts +50 -6
  20. package/src/node/errors.ts +78 -3
  21. package/src/node/fixtures.ts +84 -15
  22. package/src/node/index.ts +2 -2
  23. package/src/node/manifest.ts +4 -2
  24. package/src/node/models.ts +195 -79
  25. package/src/node/naming.ts +16 -1
  26. package/src/node/resources.ts +721 -106
  27. package/src/node/serializers.ts +510 -52
  28. package/src/node/tests.ts +621 -105
  29. package/src/node/type-map.ts +89 -11
  30. package/src/node/utils.ts +377 -114
  31. package/test/node/client.test.ts +979 -15
  32. package/test/node/enums.test.ts +0 -1
  33. package/test/node/errors.test.ts +4 -21
  34. package/test/node/models.test.ts +409 -2
  35. package/test/node/naming.test.ts +0 -3
  36. package/test/node/resources.test.ts +964 -7
  37. package/test/node/serializers.test.ts +212 -3
  38. package/tsconfig.json +2 -3
  39. package/{tsup.config.ts → tsdown.config.ts} +1 -1
  40. package/dist/index.d.ts +0 -5
  41. package/dist/index.js +0 -2158
package/dist/index.mjs ADDED
@@ -0,0 +1,3522 @@
1
+ import { assignModelsToServices, assignModelsToServices as assignModelsToServices$1, collectFieldDependencies, mapTypeRef, planOperation, toCamelCase, toKebabCase, toPascalCase, toSnakeCase, walkTypeRef } from "@workos/oagen";
2
+ //#region src/node/naming.ts
3
+ /** kebab-case file name (without extension). */
4
+ function fileName(name) {
5
+ return toKebabCase(name);
6
+ }
7
+ /** camelCase field name for domain interfaces. */
8
+ function fieldName(name) {
9
+ return toCamelCase(name);
10
+ }
11
+ /** snake_case field name for wire/response interfaces. */
12
+ function wireFieldName(name) {
13
+ return toSnakeCase(name);
14
+ }
15
+ /**
16
+ * Wire/response interface name. Uses "Wire" suffix when the domain name
17
+ * already ends in "Response" to avoid stuttering (e.g., FooResponseResponse).
18
+ */
19
+ function wireInterfaceName(domainName) {
20
+ return domainName.endsWith("Response") ? `${domainName}Wire` : `${domainName}Response`;
21
+ }
22
+ /** kebab-case service directory name. */
23
+ function serviceDirName(name) {
24
+ return toKebabCase(name);
25
+ }
26
+ /** camelCase property name for service accessors on the client. */
27
+ function servicePropertyName(name) {
28
+ return toCamelCase(name);
29
+ }
30
+ /**
31
+ * Resolve the effective service name, using the overlay-resolved class name
32
+ * when available. This ensures directory names, file names, and property names
33
+ * all derive from the same resolved name (e.g., "Mfa" instead of "MultiFactorAuth").
34
+ */
35
+ function resolveServiceName(service, ctx) {
36
+ return resolveClassName(service, ctx);
37
+ }
38
+ /**
39
+ * Build a map from IR service name → resolved service name.
40
+ * Used to translate modelToService/enumToService map values to overlay-resolved
41
+ * directory names when the code only has the IR service name string.
42
+ */
43
+ function buildServiceNameMap(services, ctx) {
44
+ const map = /* @__PURE__ */ new Map();
45
+ for (const service of services) map.set(service.name, resolveServiceName(service, ctx));
46
+ return map;
47
+ }
48
+ /** Resolve the SDK method name for an operation, checking overlay first. */
49
+ function resolveMethodName(op, _service, ctx) {
50
+ const httpKey = `${op.httpMethod.toUpperCase()} ${op.path}`;
51
+ const existing = ctx.overlayLookup?.methodByOperation?.get(httpKey);
52
+ if (existing) {
53
+ if (/\/\{[^}]+\}$/.test(op.path) && existing.methodName.endsWith("s") && !existing.methodName.endsWith("ss")) {
54
+ const singular = existing.methodName.slice(0, -1);
55
+ const specDerived = toCamelCase(op.name);
56
+ if (specDerived === singular || specDerived.endsWith(singular.slice(singular.length - 4))) return singular;
57
+ }
58
+ return existing.methodName;
59
+ }
60
+ return toCamelCase(op.name);
61
+ }
62
+ /** Resolve the SDK class name for a service, checking overlay for existing names. */
63
+ function resolveClassName(service, ctx) {
64
+ if (ctx.overlayLookup?.methodByOperation) for (const op of service.operations) {
65
+ const httpKey = `${op.httpMethod.toUpperCase()} ${op.path}`;
66
+ const existing = ctx.overlayLookup.methodByOperation.get(httpKey);
67
+ if (existing) return existing.className;
68
+ }
69
+ return toPascalCase(service.name);
70
+ }
71
+ /** Resolve the interface name for a model, checking overlay first. */
72
+ function resolveInterfaceName(name, ctx) {
73
+ const existing = ctx.overlayLookup?.interfaceByName?.get(name);
74
+ if (existing) return existing;
75
+ return toPascalCase(name);
76
+ }
77
+ //#endregion
78
+ //#region src/node/type-map.ts
79
+ /**
80
+ * Map an IR TypeRef to a TypeScript domain type string.
81
+ * Domain types use PascalCase model names (e.g., `Organization`).
82
+ *
83
+ * @param opts.stringDates - When true, map `date-time` to `string` instead of `Date`.
84
+ * Use this when integrating into an existing SDK that represents timestamps as
85
+ * ISO 8601 strings rather than Date objects.
86
+ * @param opts.genericDefaults - When present, appends default type args to generic model refs.
87
+ */
88
+ function mapTypeRef$1(ref, opts) {
89
+ const primMapper = opts?.stringDates ? mapPrimitiveStringDates : mapPrimitive;
90
+ const genericDefaults = opts?.genericDefaults;
91
+ return mapTypeRef(ref, {
92
+ primitive: primMapper,
93
+ array: (_r, items) => `${parenthesizeUnion(items)}[]`,
94
+ model: (r) => r.name + (genericDefaults?.get(r.name) ?? ""),
95
+ enum: (r) => r.name,
96
+ union: (r, variants) => joinUnionVariants(r, variants),
97
+ nullable: (_r, inner) => `${inner} | null`,
98
+ literal: (r) => typeof r.value === "string" ? `'${r.value}'` : String(r.value),
99
+ map: (_r, value) => `Record<string, ${value}>`
100
+ });
101
+ }
102
+ /**
103
+ * Map an IR TypeRef to a TypeScript wire/response type string.
104
+ * Model references get the `Response` suffix (e.g., `OrganizationResponse`).
105
+ * Wire types use JSON-native types (string for date-time, number/string for int64).
106
+ */
107
+ function mapWireTypeRef(ref, opts) {
108
+ const genericDefaults = opts?.genericDefaults;
109
+ return mapTypeRef(ref, {
110
+ primitive: mapWirePrimitive,
111
+ array: (_r, items) => `${parenthesizeUnion(items)}[]`,
112
+ model: (r) => wireInterfaceName(r.name) + (genericDefaults?.get(r.name) ?? ""),
113
+ enum: (r) => r.name,
114
+ union: (r, variants) => joinUnionVariants(r, variants),
115
+ nullable: (_r, inner) => `${inner} | null`,
116
+ literal: (r) => typeof r.value === "string" ? `'${r.value}'` : String(r.value),
117
+ map: (_r, value) => `Record<string, ${value}>`
118
+ });
119
+ }
120
+ function mapPrimitive(ref) {
121
+ if (ref.format) switch (ref.format) {
122
+ case "date-time": return "Date";
123
+ case "int64": return "bigint";
124
+ }
125
+ switch (ref.type) {
126
+ case "string": return "string";
127
+ case "integer":
128
+ case "number": return "number";
129
+ case "boolean": return "boolean";
130
+ case "unknown": return "any";
131
+ }
132
+ }
133
+ /**
134
+ * Map a primitive type using string representation for dates.
135
+ * Used when the existing SDK represents timestamps as ISO 8601 strings.
136
+ */
137
+ function mapPrimitiveStringDates(ref) {
138
+ if (ref.format) switch (ref.format) {
139
+ case "int64": return "bigint";
140
+ }
141
+ switch (ref.type) {
142
+ case "string": return "string";
143
+ case "integer":
144
+ case "number": return "number";
145
+ case "boolean": return "boolean";
146
+ case "unknown": return "any";
147
+ }
148
+ }
149
+ /**
150
+ * Map an IR PrimitiveType to a TypeScript wire/JSON type string.
151
+ * Wire types match JSON encoding: date-time stays string, int64 stays string/number.
152
+ */
153
+ function mapWirePrimitive(ref) {
154
+ switch (ref.type) {
155
+ case "string": return "string";
156
+ case "integer":
157
+ case "number": return "number";
158
+ case "boolean": return "boolean";
159
+ case "unknown": return "any";
160
+ }
161
+ }
162
+ /**
163
+ * Join union variant type strings using the appropriate operator.
164
+ * allOf unions use `&` (intersection), oneOf/anyOf/unspecified use `|` (union).
165
+ */
166
+ function joinUnionVariants(ref, variants) {
167
+ if (ref.compositionKind === "allOf") return variants.join(" & ");
168
+ return variants.join(" | ");
169
+ }
170
+ /** Wrap union/intersection types in parentheses when used as array item type. */
171
+ function parenthesizeUnion(type) {
172
+ return type.includes(" | ") || type.includes(" & ") ? `(${type})` : type;
173
+ }
174
+ //#endregion
175
+ //#region src/node/utils.ts
176
+ /**
177
+ * Compute a relative import path between two files within the generated SDK.
178
+ * Strips .ts extension from the result.
179
+ */
180
+ function relativeImport(fromFile, toFile) {
181
+ const fromDir = fromFile.split("/").slice(0, -1);
182
+ const toFileParts = toFile.split("/");
183
+ const toDir = toFileParts.slice(0, -1);
184
+ const toFileName = toFileParts[toFileParts.length - 1];
185
+ let common = 0;
186
+ while (common < fromDir.length && common < toDir.length && fromDir[common] === toDir[common]) common++;
187
+ const ups = fromDir.length - common;
188
+ const downs = toDir.slice(common);
189
+ let result = [
190
+ ...Array(ups).fill(".."),
191
+ ...downs,
192
+ toFileName
193
+ ].join("/");
194
+ result = result.replace(/\.ts$/, "");
195
+ if (!result.startsWith(".")) result = "./" + result;
196
+ return result;
197
+ }
198
+ /**
199
+ * Render a JSDoc comment block from a description string.
200
+ * Handles multiline descriptions by prefixing each line with ` * `.
201
+ * Returns the lines with the given indent (default 0 spaces).
202
+ */
203
+ function docComment(description, indent = 0) {
204
+ const pad = " ".repeat(indent);
205
+ const descLines = description.split("\n");
206
+ if (descLines.length === 1) return [`${pad}/** ${descLines[0]} */`];
207
+ const lines = [`${pad}/**`];
208
+ for (const line of descLines) lines.push(line === "" ? `${pad} *` : `${pad} * ${line}`);
209
+ lines.push(`${pad} */`);
210
+ return lines;
211
+ }
212
+ /**
213
+ * Build a map from model name → default type args string for generic models.
214
+ * E.g., Profile<CustomAttributesType = Record<string, unknown>>
215
+ * → Map { 'Profile' → '<Record<string, unknown>>' }
216
+ *
217
+ * Non-generic models are not included in the map.
218
+ */
219
+ function buildGenericModelDefaults(models) {
220
+ const result = /* @__PURE__ */ new Map();
221
+ for (const model of models) {
222
+ if (!model.typeParams?.length) continue;
223
+ const defaults = model.typeParams.map((tp) => tp.default ? mapTypeRef$1(tp.default) : "unknown");
224
+ result.set(model.name, `<${defaults.join(", ")}>`);
225
+ }
226
+ return result;
227
+ }
228
+ /**
229
+ * Remove unused imports from generated source code.
230
+ * Scans the non-import body for each imported identifier and drops
231
+ * individual names that are never referenced. Removes entire import
232
+ * statements when no names are used.
233
+ */
234
+ function pruneUnusedImports(lines) {
235
+ const importLines = [];
236
+ const bodyLines = [];
237
+ let inBody = false;
238
+ for (const line of lines) if (!inBody && (line.startsWith("import ") || line === "")) importLines.push(line);
239
+ else {
240
+ inBody = true;
241
+ bodyLines.push(line);
242
+ }
243
+ const body = bodyLines.join("\n");
244
+ const kept = [];
245
+ for (const line of importLines) {
246
+ if (line === "") {
247
+ kept.push(line);
248
+ continue;
249
+ }
250
+ const match = line.match(/\{([^}]+)\}/);
251
+ if (!match) {
252
+ kept.push(line);
253
+ continue;
254
+ }
255
+ const names = match[1].split(",").map((n) => n.trim()).filter(Boolean);
256
+ const usedNames = names.filter((name) => {
257
+ return new RegExp(`\\b${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`).test(body);
258
+ });
259
+ if (usedNames.length === 0) continue;
260
+ if (usedNames.length === names.length) kept.push(line);
261
+ else {
262
+ const isTypeImport = line.startsWith("import type");
263
+ const fromMatch = line.match(/from\s+['"]([^'"]+)['"]/);
264
+ if (fromMatch) {
265
+ const prefix = isTypeImport ? "import type" : "import";
266
+ kept.push(`${prefix} { ${usedNames.join(", ")} } from '${fromMatch[1]}';`);
267
+ } else kept.push(line);
268
+ }
269
+ }
270
+ return [...kept, ...bodyLines];
271
+ }
272
+ /** Built-in TypeScript types that are always available (no import needed). */
273
+ const TS_BUILTINS = new Set([
274
+ "Record",
275
+ "Promise",
276
+ "Array",
277
+ "Map",
278
+ "Set",
279
+ "Date",
280
+ "string",
281
+ "number",
282
+ "boolean",
283
+ "void",
284
+ "null",
285
+ "undefined",
286
+ "any",
287
+ "never",
288
+ "unknown",
289
+ "true",
290
+ "false"
291
+ ]);
292
+ /**
293
+ * Detect whether the existing SDK uses string (ISO 8601) representation for
294
+ * date-time fields. Checks if any baseline interface has a date-time IR field
295
+ * typed as plain `string` (not `Date`).
296
+ */
297
+ function detectStringDateConvention(models, ctx) {
298
+ if (!ctx.apiSurface?.interfaces) return false;
299
+ for (const model of models) {
300
+ const domainName = resolveInterfaceName(model.name, ctx);
301
+ const baseline = ctx.apiSurface.interfaces[domainName];
302
+ if (!baseline?.fields) continue;
303
+ for (const field of model.fields) {
304
+ if (field.type.kind !== "primitive" || field.type.format !== "date-time") continue;
305
+ const baselineField = baseline.fields[fieldName(field.name)];
306
+ if (baselineField && !baselineField.type.includes("Date")) return true;
307
+ }
308
+ }
309
+ return false;
310
+ }
311
+ /**
312
+ * Build a comprehensive set of all known type names from the IR and baseline.
313
+ * Used to identify type parameters by elimination — any PascalCase name not in
314
+ * this set is likely a generic type parameter.
315
+ */
316
+ function buildKnownTypeNames(models, ctx) {
317
+ const knownNames = /* @__PURE__ */ new Set();
318
+ for (const m of models) knownNames.add(resolveInterfaceName(m.name, ctx));
319
+ for (const e of ctx.spec.enums) knownNames.add(e.name);
320
+ if (ctx.apiSurface?.interfaces) for (const name of Object.keys(ctx.apiSurface.interfaces)) knownNames.add(name);
321
+ if (ctx.apiSurface?.typeAliases) for (const name of Object.keys(ctx.apiSurface.typeAliases)) knownNames.add(name);
322
+ if (ctx.apiSurface?.enums) for (const name of Object.keys(ctx.apiSurface.enums)) knownNames.add(name);
323
+ return knownNames;
324
+ }
325
+ /**
326
+ * Create a service directory resolver bundle.
327
+ * Encapsulates the common pattern of mapping models to services and resolving
328
+ * the output directory for a given IR service name.
329
+ */
330
+ function createServiceDirResolver(models, services, ctx) {
331
+ const modelToService = assignModelsToServices(models, services);
332
+ const serviceNameMap = buildServiceNameMap(services, ctx);
333
+ const resolveDir = (irService) => irService ? serviceDirName(serviceNameMap.get(irService) ?? irService) : "common";
334
+ return {
335
+ modelToService,
336
+ serviceNameMap,
337
+ resolveDir
338
+ };
339
+ }
340
+ /**
341
+ * Check if a set of baseline interface fields appears to contain generic type
342
+ * parameters — PascalCase names that aren't known models, enums, or builtins.
343
+ */
344
+ function isBaselineGeneric(fields, knownNames) {
345
+ for (const [, bf] of Object.entries(fields)) {
346
+ const typeNames = bf.type.match(/\b[A-Z][a-zA-Z0-9]*\b/g);
347
+ if (!typeNames) continue;
348
+ for (const tn of typeNames) {
349
+ if (TS_BUILTINS.has(tn)) continue;
350
+ if (knownNames.has(tn)) continue;
351
+ return true;
352
+ }
353
+ }
354
+ return false;
355
+ }
356
+ /**
357
+ * Detect whether a model matches the standard list-metadata shape:
358
+ * exactly 2 fields named `before` and `after`, both nullable string.
359
+ *
360
+ * These models are redundant because the SDK already has a shared
361
+ * `ListMetadata` type in `src/common/utils/pagination.ts`.
362
+ */
363
+ function isListMetadataModel(model) {
364
+ if (model.fields.length !== 2) return false;
365
+ const fieldsByName = new Map(model.fields.map((f) => [f.name, f]));
366
+ const before = fieldsByName.get("before");
367
+ const after = fieldsByName.get("after");
368
+ if (!before || !after) return false;
369
+ return isNullableString(before) && isNullableString(after);
370
+ }
371
+ /**
372
+ * Detect whether a model is a list wrapper — the standard paginated
373
+ * list envelope with `data` (array), `list_metadata`, and `object: 'list'`.
374
+ *
375
+ * These models are redundant because the SDK already has `List<T>` and
376
+ * `ListResponse<T>` in `src/common/utils/pagination.ts`, and the shared
377
+ * `deserializeList` handles deserialization.
378
+ */
379
+ function isListWrapperModel(model) {
380
+ const fieldsByName = new Map(model.fields.map((f) => [f.name, f]));
381
+ const dataField = fieldsByName.get("data");
382
+ if (!dataField) return false;
383
+ if (dataField.type.kind !== "array") return false;
384
+ if (!fieldsByName.get("list_metadata")) return false;
385
+ const objectField = fieldsByName.get("object");
386
+ if (objectField) {
387
+ if (objectField.type.kind !== "literal" || objectField.type.value !== "list") return false;
388
+ }
389
+ return true;
390
+ }
391
+ /** Check if a field type is nullable string (nullable<string> or just string). */
392
+ function isNullableString(field) {
393
+ const { type } = field;
394
+ if (type.kind === "nullable") return type.inner.kind === "primitive" && type.inner.type === "string";
395
+ if (type.kind === "primitive") return type.type === "string";
396
+ return false;
397
+ }
398
+ /**
399
+ * Compute a structural fingerprint for a model based on its fields.
400
+ * Two models with identical fingerprints are structurally equivalent.
401
+ */
402
+ function modelFingerprint(model) {
403
+ return model.fields.map((f) => `${f.name}:${JSON.stringify(f.type)}:${f.required}`).sort().join("|");
404
+ }
405
+ /**
406
+ * Find structurally identical models and build a deduplication map.
407
+ * Also deduplicates models that resolve to the same interface name across
408
+ * services — when a `$ref` schema is used by multiple tags, the IR may
409
+ * produce per-tag copies that diverge slightly. The version with the most
410
+ * fields is chosen as canonical.
411
+ *
412
+ * Returns a Map from duplicate model name → canonical model name.
413
+ */
414
+ function buildDeduplicationMap(models, ctx) {
415
+ const dedup = /* @__PURE__ */ new Map();
416
+ const fingerprints = /* @__PURE__ */ new Map();
417
+ for (const model of models) {
418
+ if (model.fields.length === 0) continue;
419
+ const fp = modelFingerprint(model);
420
+ const existing = fingerprints.get(fp);
421
+ if (existing) dedup.set(model.name, existing);
422
+ else fingerprints.set(fp, model.name);
423
+ }
424
+ if (ctx) {
425
+ const byDomainName = /* @__PURE__ */ new Map();
426
+ for (const model of models) {
427
+ if (model.fields.length === 0) continue;
428
+ if (dedup.has(model.name)) continue;
429
+ const domainName = resolveInterfaceName(model.name, ctx);
430
+ const group = byDomainName.get(domainName);
431
+ if (group) group.push(model);
432
+ else byDomainName.set(domainName, [model]);
433
+ }
434
+ for (const [, group] of byDomainName) {
435
+ if (group.length < 2) continue;
436
+ group.sort((a, b) => b.fields.length - a.fields.length || a.name.localeCompare(b.name));
437
+ const canonical = group[0];
438
+ for (let i = 1; i < group.length; i++) dedup.set(group[i].name, canonical.name);
439
+ }
440
+ }
441
+ return dedup;
442
+ }
443
+ /**
444
+ * Check whether a service's endpoints are already fully covered by existing
445
+ * hand-written service classes.
446
+ *
447
+ * A service is considered "covered" when:
448
+ * 1. **Every** operation in it appears in `overlayLookup.methodByOperation`
449
+ * 2. The overlay maps those operations to a class that exists in the baseline
450
+ * `apiSurface` (confirming the hand-written class is actually present)
451
+ *
452
+ * Services with zero operations are never considered covered (nothing to
453
+ * deduplicate). When no `apiSurface` is available, the overlay alone is
454
+ * used as the coverage signal (the overlay is only built from existing code).
455
+ *
456
+ * This prevents the emitter from generating resource classes like `Connections`
457
+ * that would duplicate hand-written modules like `SSO` for the same API
458
+ * endpoints (e.g., `GET /connections`).
459
+ */
460
+ function isServiceCoveredByExisting(service, ctx) {
461
+ const overlay = ctx.overlayLookup?.methodByOperation;
462
+ if (!overlay || overlay.size === 0) return false;
463
+ if (service.operations.length === 0) return false;
464
+ const baselineClasses = ctx.apiSurface?.classes;
465
+ if (!baselineClasses) return false;
466
+ const existingClassNames = new Set(Object.keys(baselineClasses));
467
+ return service.operations.every((op) => {
468
+ const httpKey = `${op.httpMethod.toUpperCase()} ${op.path}`;
469
+ const match = overlay.get(httpKey);
470
+ if (!match) return false;
471
+ return existingClassNames.has(match.className);
472
+ });
473
+ }
474
+ /**
475
+ * Return operations in a service that are NOT covered by existing hand-written
476
+ * service classes. For fully uncovered services, returns all operations.
477
+ * For partially covered services, returns only the uncovered operations.
478
+ */
479
+ function uncoveredOperations(service, ctx) {
480
+ const overlay = ctx.overlayLookup?.methodByOperation;
481
+ if (!overlay || overlay.size === 0) return service.operations;
482
+ const baselineClasses = ctx.apiSurface?.classes;
483
+ if (!baselineClasses) return service.operations;
484
+ const existingClassNames = new Set(Object.keys(baselineClasses));
485
+ return service.operations.filter((op) => {
486
+ const httpKey = `${op.httpMethod.toUpperCase()} ${op.path}`;
487
+ const match = overlay.get(httpKey);
488
+ if (!match) return true;
489
+ return !existingClassNames.has(match.className);
490
+ });
491
+ }
492
+ //#endregion
493
+ //#region src/node/enums.ts
494
+ function generateEnums(enums, ctx) {
495
+ if (enums.length === 0) return [];
496
+ const enumToService = assignEnumsToServices(enums, ctx.spec.services);
497
+ const serviceNameMap = buildServiceNameMap(ctx.spec.services, ctx);
498
+ const resolveDir = (irService) => irService ? serviceDirName(serviceNameMap.get(irService) ?? irService) : "common";
499
+ const files = [];
500
+ for (const enumDef of enums) {
501
+ const dirName = resolveDir(enumToService.get(enumDef.name));
502
+ const baselineEnum = ctx.apiSurface?.enums?.[enumDef.name];
503
+ const baselineAlias = ctx.apiSurface?.typeAliases?.[enumDef.name];
504
+ const lines = [];
505
+ let hasNewValues = false;
506
+ if (baselineEnum?.members) {
507
+ const existingValues = new Set(Object.values(baselineEnum.members).map(String));
508
+ const missingValues = enumDef.values.map((v) => String(v.value)).filter((v) => !existingValues.has(v));
509
+ hasNewValues = missingValues.length > 0;
510
+ lines.push(`export enum ${enumDef.name} {`);
511
+ for (const [memberName, memberValue] of Object.entries(baselineEnum.members)) {
512
+ const valueStr = typeof memberValue === "string" ? `'${memberValue}'` : String(memberValue);
513
+ lines.push(` ${memberName} = ${valueStr},`);
514
+ }
515
+ for (const val of missingValues) {
516
+ const memberName = val.replace(/[^a-zA-Z0-9]+/g, "");
517
+ lines.push(` ${memberName} = '${val}',`);
518
+ }
519
+ lines.push("}");
520
+ } else if (baselineAlias?.value) {
521
+ const baselineValues = extractLiteralUnionValues(baselineAlias.value);
522
+ const missing = enumDef.values.map((v) => String(v.value)).filter((v) => !baselineValues.has(v));
523
+ hasNewValues = missing.length > 0;
524
+ if (missing.length > 0) {
525
+ const parts = [...baselineValues, ...missing].map((v) => `'${v}'`);
526
+ lines.push(`export type ${enumDef.name} = ${parts.join(" | ")};`);
527
+ } else lines.push(`export type ${enumDef.name} = ${baselineAlias.value};`);
528
+ } else {
529
+ const values = enumDef.values;
530
+ lines.push(`export type ${enumDef.name} =`);
531
+ for (let i = 0; i < values.length; i++) {
532
+ const v = values[i];
533
+ const valueStr = typeof v.value === "string" ? `'${v.value}'` : String(v.value);
534
+ if (v.description || v.deprecated) {
535
+ const parts = [];
536
+ if (v.description) parts.push(v.description);
537
+ if (v.deprecated) parts.push("@deprecated");
538
+ lines.push(...docComment(parts.join("\n"), 2));
539
+ }
540
+ const suffix = i === values.length - 1 ? ";" : "";
541
+ lines.push(` | ${valueStr}${suffix}`);
542
+ }
543
+ }
544
+ files.push({
545
+ path: `src/${dirName}/interfaces/${fileName(enumDef.name)}.interface.ts`,
546
+ content: lines.join("\n"),
547
+ skipIfExists: !hasNewValues
548
+ });
549
+ }
550
+ return files;
551
+ }
552
+ /**
553
+ * Parse a TypeScript string literal union type alias value (e.g., "'a' | 'b' | 'c'")
554
+ * into a set of its string values.
555
+ */
556
+ function extractLiteralUnionValues(aliasValue) {
557
+ const values = /* @__PURE__ */ new Set();
558
+ const regex = /'([^']+)'/g;
559
+ let match;
560
+ while ((match = regex.exec(aliasValue)) !== null) values.add(match[1]);
561
+ return values;
562
+ }
563
+ function assignEnumsToServices(enums, services) {
564
+ const enumToService = /* @__PURE__ */ new Map();
565
+ const enumNames = new Set(enums.map((e) => e.name));
566
+ for (const service of services) for (const op of service.operations) {
567
+ const refs = /* @__PURE__ */ new Set();
568
+ const collect = (ref) => {
569
+ walkTypeRef(ref, { enum: (r) => refs.add(r.name) });
570
+ };
571
+ if (op.requestBody) collect(op.requestBody);
572
+ collect(op.response);
573
+ for (const p of [
574
+ ...op.pathParams,
575
+ ...op.queryParams,
576
+ ...op.headerParams,
577
+ ...op.cookieParams ?? []
578
+ ]) collect(p.type);
579
+ for (const name of refs) if (enumNames.has(name) && !enumToService.has(name)) enumToService.set(name, service.name);
580
+ }
581
+ return enumToService;
582
+ }
583
+ //#endregion
584
+ //#region src/node/models.ts
585
+ /**
586
+ * Detect baseline interfaces that are generic (have type parameters) even though
587
+ * the IR model has no typeParams (OpenAPI doesn't support generics).
588
+ *
589
+ * Heuristic: if any field type in the baseline interface contains a PascalCase
590
+ * name that isn't a known model, enum, or builtin, it's likely a type parameter
591
+ * (e.g., `CustomAttributesType`), indicating the interface is generic.
592
+ *
593
+ * When detected, adds a default generic type arg so references like `Profile`
594
+ * become `Profile<Record<string, unknown>>`.
595
+ */
596
+ function enrichGenericDefaultsFromBaseline(genericDefaults, models, ctx, resolveDir, modelToService) {
597
+ if (!ctx.apiSurface?.interfaces) return;
598
+ const knownNames = buildKnownTypeNames(models, ctx);
599
+ for (const model of models) {
600
+ if (genericDefaults.has(model.name)) continue;
601
+ const domainName = resolveInterfaceName(model.name, ctx);
602
+ const baseline = ctx.apiSurface.interfaces[domainName];
603
+ if (!baseline?.fields) continue;
604
+ const generatedPath = `src/${resolveDir(modelToService.get(model.name))}/interfaces/${fileName(model.name)}.interface.ts`;
605
+ const baselineSourceFile = baseline.sourceFile;
606
+ if (baselineSourceFile && baselineSourceFile !== generatedPath) continue;
607
+ if (isBaselineGeneric(baseline.fields, knownNames)) genericDefaults.set(model.name, "<Record<string, unknown>>");
608
+ }
609
+ }
610
+ function generateModels(models, ctx) {
611
+ if (models.length === 0) return [];
612
+ const { modelToService, resolveDir } = createServiceDirResolver(models, ctx.spec.services, ctx);
613
+ const useStringDates = detectStringDateConvention(models, ctx);
614
+ const genericDefaults = buildGenericModelDefaults(ctx.spec.models);
615
+ enrichGenericDefaultsFromBaseline(genericDefaults, models, ctx, resolveDir, modelToService);
616
+ const typeRefOpts = useStringDates ? {
617
+ stringDates: true,
618
+ genericDefaults
619
+ } : { genericDefaults };
620
+ const wireTypeRefOpts = { genericDefaults };
621
+ const files = [];
622
+ const dedup = buildDeduplicationMap(models, ctx);
623
+ for (const model of models) {
624
+ if (isListMetadataModel(model)) continue;
625
+ if (isListWrapperModel(model)) continue;
626
+ const canonicalName = dedup.get(model.name);
627
+ if (canonicalName) {
628
+ const domainName = resolveInterfaceName(model.name, ctx);
629
+ const responseName = wireInterfaceName(domainName);
630
+ const canonDomainName = resolveInterfaceName(canonicalName, ctx);
631
+ const canonResponseName = wireInterfaceName(canonDomainName);
632
+ const dirName = resolveDir(modelToService.get(model.name));
633
+ const canonDir = resolveDir(modelToService.get(canonicalName));
634
+ const aliasLines = [
635
+ `import type { ${canonDomainName}, ${canonResponseName} } from '${canonDir === dirName ? `./${fileName(canonicalName)}.interface` : `../../${canonDir}/interfaces/${fileName(canonicalName)}.interface`}';`,
636
+ "",
637
+ `export type ${domainName} = ${canonDomainName};`,
638
+ `export type ${responseName} = ${canonResponseName};`
639
+ ];
640
+ files.push({
641
+ path: `src/${dirName}/interfaces/${fileName(model.name)}.interface.ts`,
642
+ content: aliasLines.join("\n"),
643
+ skipIfExists: true
644
+ });
645
+ continue;
646
+ }
647
+ const dirName = resolveDir(modelToService.get(model.name));
648
+ const domainName = resolveInterfaceName(model.name, ctx);
649
+ const responseName = wireInterfaceName(domainName);
650
+ const deps = collectFieldDependencies(model);
651
+ const lines = [];
652
+ let modelTypeRefOpts = typeRefOpts;
653
+ let modelWireTypeRefOpts = wireTypeRefOpts;
654
+ if (genericDefaults.has(model.name)) {
655
+ const filteredDefaults = new Map(genericDefaults);
656
+ filteredDefaults.delete(model.name);
657
+ modelTypeRefOpts = {
658
+ ...typeRefOpts,
659
+ genericDefaults: filteredDefaults
660
+ };
661
+ modelWireTypeRefOpts = { genericDefaults: filteredDefaults };
662
+ }
663
+ const baselineDomain = ctx.apiSurface?.interfaces?.[domainName];
664
+ const baselineResponse = ctx.apiSurface?.interfaces?.[responseName];
665
+ const importableNames = /* @__PURE__ */ new Set();
666
+ importableNames.add(domainName);
667
+ importableNames.add(responseName);
668
+ for (const dep of deps.models) {
669
+ const depName = resolveInterfaceName(dep, ctx);
670
+ importableNames.add(depName);
671
+ importableNames.add(wireInterfaceName(depName));
672
+ }
673
+ for (const dep of deps.enums) importableNames.add(dep);
674
+ const typeDecls = /* @__PURE__ */ new Map();
675
+ const crossServiceImports = /* @__PURE__ */ new Map();
676
+ const unresolvableNames = /* @__PURE__ */ new Set();
677
+ const enumToService = assignEnumsToServices(ctx.spec.enums, ctx.spec.services);
678
+ const resolvedEnumNames = /* @__PURE__ */ new Map();
679
+ for (const e of ctx.spec.enums) resolvedEnumNames.set(resolveInterfaceName(e.name, ctx), e.name);
680
+ for (const field of model.fields) {
681
+ const baselineFields = [baselineDomain?.fields?.[fieldName(field.name)], baselineResponse?.fields?.[wireFieldName(field.name)]].filter(Boolean);
682
+ for (const bf of baselineFields) {
683
+ const names = bf.type.match(/\b[A-Z][a-zA-Z0-9]*\b/g);
684
+ if (!names) continue;
685
+ for (const name of names) {
686
+ if (TS_BUILTINS.has(name)) continue;
687
+ if (importableNames.has(name)) continue;
688
+ if (typeDecls.has(name)) continue;
689
+ if (crossServiceImports.has(name)) continue;
690
+ if (unresolvableNames.has(name)) continue;
691
+ const irEnumName = resolvedEnumNames.get(name);
692
+ if (irEnumName && !deps.enums.has(irEnumName)) {
693
+ const eDir = resolveDir(enumToService.get(irEnumName));
694
+ const relPath = eDir === dirName ? `./${fileName(irEnumName)}.interface` : `../../${eDir}/interfaces/${fileName(irEnumName)}.interface`;
695
+ crossServiceImports.set(name, {
696
+ name,
697
+ relPath
698
+ });
699
+ importableNames.add(name);
700
+ continue;
701
+ }
702
+ const candidates = [...importableNames].filter((n) => n.endsWith(name) && n !== name);
703
+ if (candidates.length === 1) {
704
+ typeDecls.set(name, candidates[0]);
705
+ importableNames.add(name);
706
+ } else unresolvableNames.add(name);
707
+ }
708
+ }
709
+ }
710
+ for (const dep of deps.models) {
711
+ const depName = resolveInterfaceName(dep, ctx);
712
+ const depDir = resolveDir(modelToService.get(dep));
713
+ const relPath = depDir === dirName ? `./${fileName(dep)}.interface` : `../../${depDir}/interfaces/${fileName(dep)}.interface`;
714
+ lines.push(`import type { ${depName}, ${wireInterfaceName(depName)} } from '${relPath}';`);
715
+ }
716
+ for (const dep of deps.enums) {
717
+ const depDir = resolveDir(enumToService.get(dep));
718
+ const relPath = depDir === dirName ? `./${fileName(dep)}.interface` : `../../${depDir}/interfaces/${fileName(dep)}.interface`;
719
+ lines.push(`import type { ${dep} } from '${relPath}';`);
720
+ }
721
+ for (const [, imp] of crossServiceImports) lines.push(`import type { ${imp.name} } from '${imp.relPath}';`);
722
+ if (lines.length > 0) lines.push("");
723
+ for (const [alias, typeExpr] of typeDecls) lines.push(`type ${alias} = ${typeExpr};`);
724
+ if (typeDecls.size > 0) lines.push("");
725
+ const typeParams = renderTypeParams(model, genericDefaults);
726
+ const seenDomainFields = /* @__PURE__ */ new Set();
727
+ if (model.description) lines.push(...docComment(model.description));
728
+ lines.push(`export interface ${domainName}${typeParams} {`);
729
+ for (const field of model.fields) {
730
+ const domainFieldName = fieldName(field.name);
731
+ if (seenDomainFields.has(domainFieldName)) continue;
732
+ seenDomainFields.add(domainFieldName);
733
+ if (field.description || field.deprecated || field.readOnly || field.writeOnly || field.default !== void 0) {
734
+ const parts = [];
735
+ if (field.description) parts.push(field.description);
736
+ if (field.readOnly) parts.push("@readonly");
737
+ if (field.writeOnly) parts.push("@writeonly");
738
+ if (field.default !== void 0) parts.push(`@default ${JSON.stringify(field.default)}`);
739
+ if (field.deprecated) parts.push("@deprecated");
740
+ lines.push(...docComment(parts.join("\n"), 2));
741
+ }
742
+ const baselineField = baselineDomain?.fields?.[domainFieldName];
743
+ const domainWireField = wireFieldName(field.name);
744
+ const responseBaselineField = baselineResponse?.fields?.[domainWireField];
745
+ const domainResponseOptionalMismatch = baselineField && !baselineField.optional && responseBaselineField && responseBaselineField.optional;
746
+ const readonlyPrefix = field.readOnly ? "readonly " : "";
747
+ if (baselineField && !domainResponseOptionalMismatch && baselineTypeResolvable(baselineField.type, importableNames) && baselineFieldCompatible(baselineField, field)) {
748
+ const opt = baselineField.optional ? "?" : "";
749
+ lines.push(` ${readonlyPrefix}${domainFieldName}${opt}: ${baselineField.type};`);
750
+ } else {
751
+ const isNewFieldOnExistingModel = baselineDomain && !baselineField;
752
+ const isNewFieldOnExistingResponse = !baselineDomain && baselineResponse && !responseBaselineField;
753
+ const opt = !field.required || isNewFieldOnExistingModel || domainResponseOptionalMismatch || isNewFieldOnExistingResponse ? "?" : "";
754
+ lines.push(` ${readonlyPrefix}${domainFieldName}${opt}: ${mapTypeRef$1(field.type, modelTypeRefOpts)};`);
755
+ }
756
+ }
757
+ lines.push("}");
758
+ lines.push("");
759
+ const seenWireFields = /* @__PURE__ */ new Set();
760
+ lines.push(`export interface ${responseName}${typeParams} {`);
761
+ for (const field of model.fields) {
762
+ const wireField = wireFieldName(field.name);
763
+ if (seenWireFields.has(wireField)) continue;
764
+ seenWireFields.add(wireField);
765
+ const baselineField = baselineResponse?.fields?.[wireField];
766
+ if (baselineField && baselineTypeResolvable(baselineField.type, importableNames) && baselineFieldCompatible(baselineField, field)) {
767
+ const opt = baselineField.optional ? "?" : "";
768
+ lines.push(` ${wireField}${opt}: ${baselineField.type};`);
769
+ } else {
770
+ const isNewFieldOnExistingModel = baselineResponse && !baselineField;
771
+ const opt = !field.required || isNewFieldOnExistingModel ? "?" : "";
772
+ lines.push(` ${wireField}${opt}: ${mapWireTypeRef(field.type, modelWireTypeRefOpts)};`);
773
+ }
774
+ }
775
+ lines.push("}");
776
+ files.push({
777
+ path: `src/${dirName}/interfaces/${fileName(model.name)}.interface.ts`,
778
+ content: pruneUnusedImports(lines).join("\n"),
779
+ skipIfExists: true
780
+ });
781
+ }
782
+ return files;
783
+ }
784
+ /**
785
+ * Check if all PascalCase type references in a baseline type string
786
+ * can be resolved to types that are actually importable in the generated file.
787
+ * A type is importable if it's a builtin, or if it's among the set of names
788
+ * that will be imported (the model's own name/response, or its IR deps).
789
+ * Returns false if any reference is unresolvable (e.g., hand-written types
790
+ * from the live SDK, or spec types from other services not in IR deps).
791
+ */
792
+ function baselineTypeResolvable(typeStr, importableNames) {
793
+ const matches = typeStr.match(/\b[A-Z][a-zA-Z0-9]*\b/g);
794
+ if (!matches) return true;
795
+ for (const name of matches) {
796
+ if (TS_BUILTINS.has(name)) continue;
797
+ if (importableNames.has(name)) continue;
798
+ return false;
799
+ }
800
+ return true;
801
+ }
802
+ /**
803
+ * Check if a baseline field type is compatible with the IR field for use
804
+ * in the generated interface. The serializer generates expressions based on
805
+ * the IR type, so the interface type must be assignable from the serializer output.
806
+ *
807
+ * Rejects baseline types when:
808
+ * - IR field is nullable but baseline type doesn't include `null`
809
+ * - IR field is optional but baseline says required (and vice versa)
810
+ * - IR field is required but baseline says optional
811
+ */
812
+ function baselineFieldCompatible(baselineField, irField) {
813
+ const irNullable = irField.type.kind === "nullable";
814
+ const baselineHasNull = baselineField.type.includes("null");
815
+ if (irNullable && !baselineHasNull && irField.required) return false;
816
+ if (!irField.required && !baselineField.optional && !baselineField.type.includes("undefined")) return false;
817
+ if (baselineField.type === "Record<string, unknown>" && hasSpecificIRType(irField.type)) return false;
818
+ return true;
819
+ }
820
+ /** Check if an IR type is more specific than Record<string, unknown>. */
821
+ function hasSpecificIRType(ref) {
822
+ switch (ref.kind) {
823
+ case "model":
824
+ case "enum": return true;
825
+ case "union": return ref.variants.some((v) => v.kind === "model" || v.kind === "enum");
826
+ case "nullable": return hasSpecificIRType(ref.inner);
827
+ default: return false;
828
+ }
829
+ }
830
+ function renderTypeParams(model, genericDefaults) {
831
+ if (!model.typeParams?.length) {
832
+ if (genericDefaults?.has(model.name)) return "<GenericType extends Record<string, unknown> = Record<string, unknown>>";
833
+ return "";
834
+ }
835
+ return `<${model.typeParams.map((tp) => {
836
+ const def = tp.default ? ` = ${mapTypeRef$1(tp.default)}` : "";
837
+ return `${tp.name}${def}`;
838
+ }).join(", ")}>`;
839
+ }
840
+ //#endregion
841
+ //#region src/node/serializers.ts
842
+ /**
843
+ * Render generic type parameter declarations for a model.
844
+ * E.g., `<CustomAttributesType = Record<string, unknown>>`.
845
+ * Returns empty string for non-generic models.
846
+ */
847
+ function renderSerializerTypeParams(model, ctx) {
848
+ if (model.typeParams?.length) {
849
+ const params = model.typeParams.map((tp) => {
850
+ const def = tp.default ? ` = ${mapTypeRef$1(tp.default)}` : "";
851
+ return `${tp.name}${def}`;
852
+ });
853
+ const names = model.typeParams.map((tp) => tp.name);
854
+ return {
855
+ decl: `<${params.join(", ")}>`,
856
+ usage: `<${names.join(", ")}>`
857
+ };
858
+ }
859
+ if (ctx?.apiSurface?.interfaces) {
860
+ const domainName = resolveInterfaceName(model.name, ctx);
861
+ const baseline = ctx.apiSurface.interfaces[domainName];
862
+ if (baseline?.fields) {
863
+ const baselineSourceFile = baseline.sourceFile;
864
+ const { modelToService, resolveDir } = createServiceDirResolver(ctx.spec.models, ctx.spec.services, ctx);
865
+ const generatedPath = `src/${resolveDir(modelToService.get(model.name))}/interfaces/${fileName(model.name)}.interface.ts`;
866
+ const pathMatches = !baselineSourceFile || baselineSourceFile === generatedPath;
867
+ const knownNames = buildKnownTypeNames(ctx.spec.models, ctx);
868
+ if (pathMatches && isBaselineGeneric(baseline.fields, knownNames)) return {
869
+ decl: "<GenericType extends Record<string, unknown> = Record<string, unknown>>",
870
+ usage: "<GenericType>"
871
+ };
872
+ }
873
+ }
874
+ return {
875
+ decl: "",
876
+ usage: ""
877
+ };
878
+ }
879
+ function generateSerializers(models, ctx) {
880
+ if (models.length === 0) return [];
881
+ const { modelToService, resolveDir } = createServiceDirResolver(models, ctx.spec.services, ctx);
882
+ const useStringDates = detectStringDateConvention(models, ctx);
883
+ const files = [];
884
+ const dedup = buildDeduplicationMap(models, ctx);
885
+ const skippedSerializeModels = /* @__PURE__ */ new Set();
886
+ for (const model of models) {
887
+ if (isListMetadataModel(model)) continue;
888
+ if (isListWrapperModel(model)) continue;
889
+ const canonicalName = dedup.get(model.name);
890
+ if (canonicalName) {
891
+ const domainName = resolveInterfaceName(model.name, ctx);
892
+ const canonDomainName = resolveInterfaceName(canonicalName, ctx);
893
+ const dirName = resolveDir(modelToService.get(model.name));
894
+ const canonDir = resolveDir(modelToService.get(canonicalName));
895
+ const serializerPath = `src/${dirName}/serializers/${fileName(model.name)}.serializer.ts`;
896
+ const aliasLines = [`export { deserialize${canonDomainName} as deserialize${domainName}, serialize${canonDomainName} as serialize${domainName} } from '${relativeImport(serializerPath, `src/${canonDir}/serializers/${fileName(canonicalName)}.serializer.ts`)}';`];
897
+ files.push({
898
+ path: serializerPath,
899
+ content: aliasLines.join("\n")
900
+ });
901
+ continue;
902
+ }
903
+ const dirName = resolveDir(modelToService.get(model.name));
904
+ const domainName = resolveInterfaceName(model.name, ctx);
905
+ const responseName = wireInterfaceName(domainName);
906
+ const serializerPath = `src/${dirName}/serializers/${fileName(model.name)}.serializer.ts`;
907
+ const typeParams = renderSerializerTypeParams(model, ctx);
908
+ const baselineResponse = ctx.apiSurface?.interfaces?.[responseName];
909
+ const skipFormatFields = /* @__PURE__ */ new Set();
910
+ const baselineDomain = ctx.apiSurface?.interfaces?.[domainName];
911
+ let shouldSkipSerialize = serializerHasBaselineIncompatibility(model, baselineResponse, baselineDomain, ctx);
912
+ if (!shouldSkipSerialize) for (const field of model.fields) {
913
+ for (const ref of collectSerializedModelRefs(field.type)) {
914
+ if (skippedSerializeModels.has(ref)) {
915
+ shouldSkipSerialize = true;
916
+ break;
917
+ }
918
+ const canon = dedup.get(ref);
919
+ if (canon && skippedSerializeModels.has(canon)) {
920
+ shouldSkipSerialize = true;
921
+ break;
922
+ }
923
+ }
924
+ if (shouldSkipSerialize) break;
925
+ }
926
+ if (shouldSkipSerialize) skippedSerializeModels.add(model.name);
927
+ if (useStringDates) {
928
+ for (const field of model.fields) if (hasDateTimeConversion(field.type)) skipFormatFields.add(field.name);
929
+ }
930
+ if (baselineDomain) for (const field of model.fields) {
931
+ if (skipFormatFields.has(field.name)) continue;
932
+ const baselineField = baselineDomain.fields?.[fieldName(field.name)];
933
+ if (baselineField && !baselineField.type.includes("Date") && hasFormatConversion(field.type)) skipFormatFields.add(field.name);
934
+ }
935
+ const nestedModelRefs = /* @__PURE__ */ new Set();
936
+ for (const field of model.fields) for (const ref of collectSerializedModelRefs(field.type)) if (ref !== model.name) nestedModelRefs.add(ref);
937
+ const lines = [];
938
+ const interfacePath = `src/${dirName}/interfaces/${fileName(model.name)}.interface.ts`;
939
+ lines.push(`import type { ${domainName}, ${responseName} } from '${relativeImport(serializerPath, interfacePath)}';`);
940
+ for (const dep of nestedModelRefs) {
941
+ const depSerializerPath = `src/${resolveDir(modelToService.get(dep))}/serializers/${fileName(dep)}.serializer.ts`;
942
+ const depName = resolveInterfaceName(dep, ctx);
943
+ const rel = relativeImport(serializerPath, depSerializerPath);
944
+ lines.push(`import { deserialize${depName}, serialize${depName} } from '${rel}';`);
945
+ }
946
+ lines.push("");
947
+ const seenDeserFields = /* @__PURE__ */ new Set();
948
+ const deserParamPrefix = model.fields.length === 0 ? "_" : "";
949
+ lines.push(`export const deserialize${domainName} = ${typeParams.decl}(`);
950
+ lines.push(` ${deserParamPrefix}response: ${responseName}${typeParams.usage},`);
951
+ lines.push(`): ${domainName}${typeParams.usage} => ({`);
952
+ for (const field of model.fields) {
953
+ const domain = fieldName(field.name);
954
+ if (seenDeserFields.has(domain)) continue;
955
+ seenDeserFields.add(domain);
956
+ const wire = wireFieldName(field.name);
957
+ const wireAccess = `response.${wire}`;
958
+ const expr = skipFormatFields.has(field.name) ? wireAccess : deserializeExpression(field.type, wireAccess, ctx);
959
+ const isNewField = baselineDomain && !baselineDomain.fields?.[domain];
960
+ if ((!field.required || isNewField) && expr !== wireAccess && needsNullGuard(field.type)) {
961
+ const fallback = field.type.kind === "nullable" ? "null" : "undefined";
962
+ if (expr.startsWith(`${wireAccess} != null ?`)) lines.push(` ${domain}: ${expr.replace(/: null$/, `: ${fallback}`)},`);
963
+ else lines.push(` ${domain}: ${wireAccess} != null ? ${expr} : ${fallback},`);
964
+ } else if (field.required && expr === wireAccess) {
965
+ const fallback = ((baselineResponse?.fields?.[wire])?.optional ?? false) || isNewField ? defaultForType(field.type) : null;
966
+ if (fallback) lines.push(` ${domain}: ${expr} ?? ${fallback},`);
967
+ else lines.push(` ${domain}: ${expr},`);
968
+ } else lines.push(` ${domain}: ${expr},`);
969
+ }
970
+ lines.push("});");
971
+ if (!shouldSkipSerialize) {
972
+ const serParamPrefix = model.fields.length === 0 ? "_" : "";
973
+ lines.push("");
974
+ lines.push(`export const serialize${domainName} = ${typeParams.decl}(`);
975
+ lines.push(` ${serParamPrefix}model: ${domainName}${typeParams.usage},`);
976
+ lines.push(`): ${responseName}${typeParams.usage} => ({`);
977
+ const seenSerFields = /* @__PURE__ */ new Set();
978
+ for (const field of model.fields) {
979
+ const wire = wireFieldName(field.name);
980
+ if (seenSerFields.has(wire)) continue;
981
+ seenSerFields.add(wire);
982
+ const domain = fieldName(field.name);
983
+ const domainAccess = `model.${domain}`;
984
+ const expr = skipFormatFields.has(field.name) ? domainAccess : serializeExpression(field.type, domainAccess, ctx);
985
+ const isNewSerField = baselineDomain && !baselineDomain.fields?.[domain];
986
+ const effectivelyOptionalSer = !field.required || isNewSerField;
987
+ const baselineWireField = baselineResponse?.fields?.[wire];
988
+ const baselineDomainField = baselineDomain?.fields?.[domain];
989
+ const isNewFieldOnExistingDomain = baselineDomain && !baselineDomainField;
990
+ const domainFieldIsOptional = !field.required || (baselineDomainField?.optional ?? false) || !!isNewFieldOnExistingDomain;
991
+ const wireFieldIsRequired = baselineWireField ? !baselineWireField.optional : field.required;
992
+ const needsUndefinedCoalesce = domainFieldIsOptional && wireFieldIsRequired && expr === domainAccess;
993
+ const shouldGuardSer = effectivelyOptionalSer || field.type.kind === "nullable";
994
+ if (expr !== domainAccess && needsNullGuard(field.type) && shouldGuardSer) {
995
+ const fallback = field.type.kind === "nullable" ? "null" : "undefined";
996
+ if (expr.startsWith(`${domainAccess} != null ?`)) lines.push(` ${wire}: ${expr.replace(/: null$/, `: ${fallback}`)},`);
997
+ else lines.push(` ${wire}: ${domainAccess} != null ? ${expr} : ${fallback},`);
998
+ } else if (needsUndefinedCoalesce) if (baselineWireField?.type?.includes("null") || field.type.kind === "nullable") lines.push(` ${wire}: ${expr} ?? null,`);
999
+ else lines.push(` ${wire}: ${expr}!,`);
1000
+ else if (field.type.kind === "nullable" && expr === domainAccess) {
1001
+ const domainWireField2 = wireFieldName(field.name);
1002
+ const responseBaselineField2 = baselineResponse?.fields?.[domainWireField2];
1003
+ const baselineDomainField2 = baselineDomain?.fields?.[domain];
1004
+ const domainResponseMismatch = baselineDomainField2 && !baselineDomainField2.optional && responseBaselineField2 && responseBaselineField2.optional;
1005
+ if (!field.required || isNewSerField || !!domainResponseMismatch) lines.push(` ${wire}: ${expr} ?? null,`);
1006
+ else lines.push(` ${wire}: ${expr},`);
1007
+ } else lines.push(` ${wire}: ${expr},`);
1008
+ }
1009
+ lines.push("});");
1010
+ }
1011
+ files.push({
1012
+ path: serializerPath,
1013
+ content: pruneUnusedImports(lines).join("\n")
1014
+ });
1015
+ }
1016
+ return files;
1017
+ }
1018
+ /**
1019
+ * Collect model names that will actually be called in serialize/deserialize expressions.
1020
+ * Unlike collectModelRefs (which walks all union variants), this only includes models
1021
+ * that the expression functions will actually invoke a serializer/deserializer for.
1022
+ */
1023
+ function collectSerializedModelRefs(ref) {
1024
+ switch (ref.kind) {
1025
+ case "model": return [ref.name];
1026
+ case "array":
1027
+ if (ref.items.kind === "model") return [ref.items.name];
1028
+ return collectSerializedModelRefs(ref.items);
1029
+ case "nullable": return collectSerializedModelRefs(ref.inner);
1030
+ case "union": {
1031
+ const models = uniqueModelVariants(ref);
1032
+ if (ref.discriminator && models.length > 0) return models;
1033
+ if (ref.compositionKind === "allOf" && models.length > 0) return models;
1034
+ if (models.length === 1) return models;
1035
+ return [];
1036
+ }
1037
+ case "map":
1038
+ case "primitive":
1039
+ case "literal":
1040
+ case "enum": return [];
1041
+ }
1042
+ }
1043
+ function deserializeExpression(ref, wireExpr, ctx) {
1044
+ switch (ref.kind) {
1045
+ case "primitive": return deserializePrimitive(ref, wireExpr);
1046
+ case "literal":
1047
+ case "enum": return wireExpr;
1048
+ case "model": return `deserialize${resolveInterfaceName(ref.name, ctx)}(${wireExpr})`;
1049
+ case "array":
1050
+ if (ref.items.kind === "model") return `${wireExpr}.map(deserialize${resolveInterfaceName(ref.items.name, ctx)})`;
1051
+ return wireExpr;
1052
+ case "nullable": {
1053
+ const innerExpr = deserializeExpression(ref.inner, wireExpr, ctx);
1054
+ if (innerExpr !== wireExpr) return `${wireExpr} != null ? ${innerExpr} : null`;
1055
+ return `${wireExpr} ?? null`;
1056
+ }
1057
+ case "union": {
1058
+ if (ref.discriminator) return renderDiscriminatorSwitch(ref, wireExpr, "deserialize", ctx);
1059
+ if (ref.compositionKind === "allOf") return renderAllOfMerge(ref, wireExpr, "deserialize", ctx);
1060
+ const deserModelVariants = uniqueModelVariants(ref);
1061
+ if (deserModelVariants.length === 1) return `deserialize${resolveInterfaceName(deserModelVariants[0], ctx)}(${wireExpr})`;
1062
+ return wireExpr;
1063
+ }
1064
+ case "map": return wireExpr;
1065
+ }
1066
+ }
1067
+ function serializeExpression(ref, domainExpr, ctx) {
1068
+ switch (ref.kind) {
1069
+ case "primitive": return serializePrimitive(ref, domainExpr);
1070
+ case "literal":
1071
+ case "enum": return domainExpr;
1072
+ case "model": return `serialize${resolveInterfaceName(ref.name, ctx)}(${domainExpr})`;
1073
+ case "array":
1074
+ if (ref.items.kind === "model") return `${domainExpr}.map(serialize${resolveInterfaceName(ref.items.name, ctx)})`;
1075
+ return domainExpr;
1076
+ case "nullable": {
1077
+ const innerExpr = serializeExpression(ref.inner, domainExpr, ctx);
1078
+ if (innerExpr !== domainExpr) return `${domainExpr} != null ? ${innerExpr} : null`;
1079
+ return domainExpr;
1080
+ }
1081
+ case "union": {
1082
+ if (ref.discriminator) return renderDiscriminatorSwitch(ref, domainExpr, "serialize", ctx);
1083
+ if (ref.compositionKind === "allOf") return renderAllOfMerge(ref, domainExpr, "serialize", ctx);
1084
+ const serModelVariants = uniqueModelVariants(ref);
1085
+ if (serModelVariants.length === 1) return `serialize${resolveInterfaceName(serModelVariants[0], ctx)}(${domainExpr})`;
1086
+ return domainExpr;
1087
+ }
1088
+ case "map": return domainExpr;
1089
+ }
1090
+ }
1091
+ /**
1092
+ * Extract unique model names from a union's variants.
1093
+ * Used to determine if a union can be deserialized/serialized as a single model.
1094
+ */
1095
+ function uniqueModelVariants(ref) {
1096
+ const modelNames = /* @__PURE__ */ new Set();
1097
+ for (const v of ref.variants) if (v.kind === "model") modelNames.add(v.name);
1098
+ return [...modelNames];
1099
+ }
1100
+ /**
1101
+ * Check whether a TypeRef involves a model reference or format conversion
1102
+ * that would produce a function call in serialization/deserialization.
1103
+ * Used to determine whether optional fields need a null guard wrapper.
1104
+ */
1105
+ function needsNullGuard(ref) {
1106
+ switch (ref.kind) {
1107
+ case "model": return true;
1108
+ case "primitive": return hasFormatConversion(ref);
1109
+ case "array": return ref.items.kind === "model";
1110
+ case "nullable": return needsNullGuard(ref.inner);
1111
+ case "union":
1112
+ if (ref.discriminator) return true;
1113
+ if (ref.compositionKind === "allOf" && uniqueModelVariants(ref).length > 0) return true;
1114
+ return uniqueModelVariants(ref).length === 1;
1115
+ default: return false;
1116
+ }
1117
+ }
1118
+ /** Check if a type has a format that requires conversion. */
1119
+ function hasFormatConversion(ref) {
1120
+ switch (ref.kind) {
1121
+ case "primitive": return ref.format === "date-time" || ref.format === "int64";
1122
+ case "nullable": return hasFormatConversion(ref.inner);
1123
+ default: return false;
1124
+ }
1125
+ }
1126
+ /** Check if a type specifically has a date-time format conversion. */
1127
+ function hasDateTimeConversion(ref) {
1128
+ switch (ref.kind) {
1129
+ case "primitive": return ref.format === "date-time";
1130
+ case "nullable": return hasDateTimeConversion(ref.inner);
1131
+ default: return false;
1132
+ }
1133
+ }
1134
+ /** Deserialize a primitive value, applying format conversions when needed. */
1135
+ function deserializePrimitive(ref, wireExpr) {
1136
+ if (ref.format === "date-time") return `new Date(${wireExpr})`;
1137
+ if (ref.format === "int64") return `BigInt(${wireExpr})`;
1138
+ return wireExpr;
1139
+ }
1140
+ /** Serialize a primitive value, applying format conversions when needed. */
1141
+ function serializePrimitive(ref, domainExpr) {
1142
+ if (ref.format === "date-time") return `${domainExpr}.toISOString()`;
1143
+ if (ref.format === "int64") return `String(${domainExpr})`;
1144
+ return domainExpr;
1145
+ }
1146
+ /**
1147
+ * Render a discriminated union switch expression.
1148
+ * Produces an IIFE that switches on the discriminator property and calls
1149
+ * the appropriate serializer/deserializer for each mapped model.
1150
+ */
1151
+ function renderDiscriminatorSwitch(ref, expr, direction, ctx) {
1152
+ const disc = ref.discriminator;
1153
+ const cases = [];
1154
+ for (const [value, modelName] of Object.entries(disc.mapping)) {
1155
+ const fn = `${direction}${resolveInterfaceName(modelName, ctx)}`;
1156
+ cases.push(`case '${value}': return ${fn}(${expr} as any)`);
1157
+ }
1158
+ return `(() => { switch ((${expr} as any).${disc.property}) { ${cases.join("; ")}; default: return ${expr} } })()`;
1159
+ }
1160
+ /**
1161
+ * Render an allOf merge expression.
1162
+ * Spreads the serialized/deserialized result of each model variant.
1163
+ */
1164
+ function renderAllOfMerge(ref, expr, direction, ctx) {
1165
+ const models = uniqueModelVariants(ref);
1166
+ if (models.length === 0) return expr;
1167
+ return `({ ${models.map((name) => {
1168
+ return `...${direction}${resolveInterfaceName(name, ctx)}(${expr} as any)`;
1169
+ }).join(", ")} })`;
1170
+ }
1171
+ /**
1172
+ * Return a TypeScript default value expression for a type, used as a null
1173
+ * coalesce fallback when a required domain field may be optional in the
1174
+ * response interface (baseline override mismatch).
1175
+ */
1176
+ function defaultForType(ref) {
1177
+ switch (ref.kind) {
1178
+ case "literal": return typeof ref.value === "string" ? `'${ref.value}'` : String(ref.value);
1179
+ case "enum": return null;
1180
+ case "map": return "{}";
1181
+ case "nullable": return "null";
1182
+ case "primitive": switch (ref.type) {
1183
+ case "boolean": return "false";
1184
+ case "string": return "''";
1185
+ case "integer":
1186
+ case "number": return "0";
1187
+ default: return null;
1188
+ }
1189
+ case "array": return "[]";
1190
+ default: return null;
1191
+ }
1192
+ }
1193
+ /**
1194
+ * Check if the generated serialize function would produce type errors
1195
+ * against the baseline wire (response) interface. Returns true when:
1196
+ * - The baseline response interface has required fields whose wire name
1197
+ * doesn't match any IR model field → TS2741 missing property.
1198
+ * - The baseline domain interface has required fields whose camelCase name
1199
+ * doesn't match any IR model field → the serializer would produce
1200
+ * expressions referencing domain fields that don't exist on the baseline.
1201
+ * - The baseline response has a required array field whose type references
1202
+ * a different module than where the serializer imports its nested serializer.
1203
+ */
1204
+ function serializerHasBaselineIncompatibility(model, baselineResponse, baselineDomain, ctx) {
1205
+ if (!baselineResponse?.fields) return false;
1206
+ const irWireFields = /* @__PURE__ */ new Set();
1207
+ const irDomainFields = /* @__PURE__ */ new Set();
1208
+ for (const field of model.fields) {
1209
+ irWireFields.add(wireFieldName(field.name));
1210
+ irDomainFields.add(fieldName(field.name));
1211
+ }
1212
+ for (const [wireField2, fieldDef] of Object.entries(baselineResponse.fields)) {
1213
+ if (fieldDef.optional) continue;
1214
+ if (!irWireFields.has(wireField2)) return true;
1215
+ }
1216
+ if (baselineDomain?.fields) {
1217
+ const baselineRequiredFields = Object.entries(baselineDomain.fields).filter(([, f]) => !f.optional).map(([name]) => name);
1218
+ const unmatchedCount = baselineRequiredFields.filter((n) => !irDomainFields.has(n)).length;
1219
+ if (unmatchedCount > 0 && baselineRequiredFields.length > 0) {
1220
+ if (unmatchedCount / baselineRequiredFields.length > .3) return true;
1221
+ }
1222
+ }
1223
+ if (ctx?.apiSurface?.interfaces) {
1224
+ const modelSourceFile = baselineResponse?.sourceFile;
1225
+ const responseDir = modelSourceFile ? modelSourceFile.split("/").slice(0, 2).join("/") : null;
1226
+ for (const field of model.fields) {
1227
+ let fieldType = field.type;
1228
+ if (fieldType.kind === "nullable") fieldType = fieldType.inner;
1229
+ if (fieldType.kind !== "array" && fieldType.kind !== "model") continue;
1230
+ const innerType = fieldType.kind === "array" ? fieldType.items : fieldType;
1231
+ if (innerType.kind !== "model") continue;
1232
+ const nestedWireName = wireInterfaceName(resolveInterfaceName(innerType.name, ctx));
1233
+ const wireField3 = wireFieldName(field.name);
1234
+ const baselineWireField2 = baselineResponse.fields[wireField3];
1235
+ if (!baselineWireField2) continue;
1236
+ const baselineTypeNames = baselineWireField2.type.match(/\b[A-Z][a-zA-Z0-9]*Response\b/g) || [];
1237
+ if (baselineTypeNames.length > 0 && !baselineTypeNames.includes(nestedWireName)) return true;
1238
+ if (baselineWireField2.type.includes(nestedWireName) || baselineWireField2.type.match(/\b[A-Z]\w*Response\b/)) {
1239
+ const typeNames = baselineWireField2.type.match(/\b[A-Z][a-zA-Z0-9]*\b/g) || [];
1240
+ for (const typeName of typeNames) {
1241
+ if (typeName === "Record" || typeName === "Array") continue;
1242
+ const nestedIface = ctx.apiSurface.interfaces[typeName];
1243
+ if (!nestedIface) continue;
1244
+ const nestedSrc = nestedIface.sourceFile;
1245
+ if (!nestedSrc || !responseDir) continue;
1246
+ if (nestedSrc.split("/").slice(0, 2).join("/") !== responseDir) return true;
1247
+ }
1248
+ }
1249
+ }
1250
+ }
1251
+ return false;
1252
+ }
1253
+ //#endregion
1254
+ //#region src/node/fixtures.ts
1255
+ /**
1256
+ * Prefix mapping for generating realistic ID fixture values.
1257
+ * When a field named "id" belongs to a model whose name matches a key here,
1258
+ * the generated ID will be prefixed accordingly (e.g. "conn_01234").
1259
+ */
1260
+ const ID_PREFIXES = {
1261
+ Connection: "conn_",
1262
+ Organization: "org_",
1263
+ OrganizationMembership: "om_",
1264
+ User: "user_",
1265
+ Directory: "directory_",
1266
+ DirectoryGroup: "dir_grp_",
1267
+ DirectoryUser: "dir_usr_",
1268
+ Invitation: "inv_",
1269
+ Session: "session_",
1270
+ AuthenticationFactor: "auth_factor_",
1271
+ EmailVerification: "email_verification_",
1272
+ MagicAuth: "magic_auth_",
1273
+ PasswordReset: "password_reset_"
1274
+ };
1275
+ /**
1276
+ * Generate JSON fixture files for test data.
1277
+ * Each model that appears as a response gets a fixture in wire format (snake_case).
1278
+ */
1279
+ function generateFixtures(spec, ctx) {
1280
+ if (spec.models.length === 0) return [];
1281
+ const { modelToService, resolveDir } = ctx ? createServiceDirResolver(spec.models, ctx.spec.services, ctx) : {
1282
+ modelToService: assignModelsToServices$1(spec.models, spec.services),
1283
+ resolveDir: (irService) => irService ? serviceDirName(irService) : "common"
1284
+ };
1285
+ const modelMap = new Map(spec.models.map((m) => [m.name, m]));
1286
+ const enumMap = new Map(spec.enums.map((e) => [e.name, e]));
1287
+ const files = [];
1288
+ for (const model of spec.models) {
1289
+ if (isListMetadataModel(model)) continue;
1290
+ if (isListWrapperModel(model)) continue;
1291
+ const dirName = resolveDir(modelToService.get(model.name));
1292
+ const fixture = generateModelFixture(model, modelMap, enumMap);
1293
+ files.push({
1294
+ path: `src/${dirName}/fixtures/${fileName(model.name)}.fixture.json`,
1295
+ content: JSON.stringify(fixture, null, 2)
1296
+ });
1297
+ }
1298
+ for (const service of spec.services) {
1299
+ const serviceDir = serviceDirName(ctx ? resolveResourceClassName(service, ctx) : service.name);
1300
+ for (const op of service.operations) if (op.pagination) {
1301
+ let itemModel = op.pagination.itemType.kind === "model" ? modelMap.get(op.pagination.itemType.name) : null;
1302
+ if (itemModel) {
1303
+ const unwrapped = unwrapListModel(itemModel, modelMap);
1304
+ if (unwrapped) itemModel = unwrapped;
1305
+ const listFixture = {
1306
+ data: [generateModelFixture(itemModel, modelMap, enumMap)],
1307
+ list_metadata: {
1308
+ before: null,
1309
+ after: null
1310
+ }
1311
+ };
1312
+ files.push({
1313
+ path: `src/${serviceDir}/fixtures/list-${fileName(itemModel.name)}.fixture.json`,
1314
+ content: JSON.stringify(listFixture, null, 2)
1315
+ });
1316
+ }
1317
+ }
1318
+ }
1319
+ return files;
1320
+ }
1321
+ /**
1322
+ * Detect if a model is a list wrapper (has a `data` array field and a `list_metadata` field).
1323
+ * If so, return the inner item model from the `data` array. Otherwise return null.
1324
+ * This prevents double-nesting when the pagination itemType points to a list wrapper
1325
+ * instead of the actual item model.
1326
+ */
1327
+ function unwrapListModel(model, modelMap) {
1328
+ const dataField = model.fields.find((f) => f.name === "data");
1329
+ const hasListMetadata = model.fields.some((f) => f.name === "list_metadata" || f.name === "listMetadata");
1330
+ if (dataField && hasListMetadata && dataField.type.kind === "array") {
1331
+ const itemType = dataField.type.items;
1332
+ if (itemType.kind === "model") return modelMap.get(itemType.name) ?? null;
1333
+ }
1334
+ return null;
1335
+ }
1336
+ function generateModelFixture(model, modelMap, enumMap) {
1337
+ const fixture = {};
1338
+ for (const field of model.fields) {
1339
+ const wireName = wireFieldName(field.name);
1340
+ if (field.example !== void 0) fixture[wireName] = field.example;
1341
+ else fixture[wireName] = generateFieldValue(field.type, field.name, model.name, modelMap, enumMap);
1342
+ }
1343
+ return fixture;
1344
+ }
1345
+ function generateFieldValue(ref, fieldName, modelName, modelMap, enumMap) {
1346
+ switch (ref.kind) {
1347
+ case "primitive": return generatePrimitiveValue(ref.type, ref.format, fieldName, modelName);
1348
+ case "literal": return ref.value;
1349
+ case "enum": return enumMap.get(ref.name)?.values[0]?.value ?? "unknown";
1350
+ case "model": {
1351
+ const nested = modelMap.get(ref.name);
1352
+ if (nested) return generateModelFixture(nested, modelMap, enumMap);
1353
+ return {};
1354
+ }
1355
+ case "array":
1356
+ if (ref.items.kind === "enum") {
1357
+ const e = enumMap.get(ref.items.name);
1358
+ if (e && e.values.length > 0) return e.values.map((v) => v.value);
1359
+ }
1360
+ return [generateFieldValue(ref.items, fieldName, modelName, modelMap, enumMap)];
1361
+ case "nullable": return generateFieldValue(ref.inner, fieldName, modelName, modelMap, enumMap);
1362
+ case "union":
1363
+ if (ref.variants.length > 0) return generateFieldValue(ref.variants[0], fieldName, modelName, modelMap, enumMap);
1364
+ return null;
1365
+ case "map": return { key: generateFieldValue(ref.valueType, "value", modelName, modelMap, enumMap) };
1366
+ }
1367
+ }
1368
+ function generatePrimitiveValue(type, format, name, modelName) {
1369
+ switch (type) {
1370
+ case "string":
1371
+ if (format === "date-time") return "2023-01-01T00:00:00.000Z";
1372
+ if (format === "date") return "2023-01-01";
1373
+ if (format === "uuid") return "00000000-0000-0000-0000-000000000000";
1374
+ if (name === "id") return `${ID_PREFIXES[modelName] ?? ""}01234`;
1375
+ if (name.includes("id")) return `${name}_01234`;
1376
+ if (name.includes("email")) return "test@example.com";
1377
+ if (name.includes("url") || name.includes("uri")) return "https://example.com";
1378
+ if (name.includes("name")) return "Test";
1379
+ return `test_${name}`;
1380
+ case "integer": return 1;
1381
+ case "number": return 1;
1382
+ case "boolean": return true;
1383
+ case "unknown": return {};
1384
+ default: return null;
1385
+ }
1386
+ }
1387
+ //#endregion
1388
+ //#region src/node/resources.ts
1389
+ /**
1390
+ * Check whether the baseline (hand-written) class has a constructor compatible
1391
+ * with the generated pattern `constructor(private readonly workos: WorkOS)`.
1392
+ * Returns true when no baseline exists (fresh generation) or when compatible.
1393
+ */
1394
+ function hasCompatibleConstructor(className, ctx) {
1395
+ const baselineClass = ctx.apiSurface?.classes?.[className];
1396
+ if (!baselineClass) return true;
1397
+ const params = baselineClass.constructorParams;
1398
+ if (!params || params.length === 0) return true;
1399
+ return params.some((p) => p.name === "workos" && p.type.includes("WorkOS"));
1400
+ }
1401
+ /**
1402
+ * Resolve the resource class name for a service, accounting for constructor
1403
+ * compatibility with the baseline class.
1404
+ *
1405
+ * When the overlay-resolved class has an incompatible constructor (e.g., a
1406
+ * hand-written `Webhooks` class that takes `CryptoProvider` instead of `WorkOS`),
1407
+ * falls back to the IR name (`toPascalCase(service.name)`). If the IR name
1408
+ * collides with the overlay name, appends an `Endpoints` suffix.
1409
+ */
1410
+ function resolveResourceClassName(service, ctx) {
1411
+ const overlayName = resolveServiceName(service, ctx);
1412
+ if (hasCompatibleConstructor(overlayName, ctx)) return overlayName;
1413
+ const irName = toPascalCase(service.name);
1414
+ if (irName === overlayName) return irName + "Endpoints";
1415
+ return irName;
1416
+ }
1417
+ /** Standard pagination query params handled by PaginationOptions — not imported individually. */
1418
+ const PAGINATION_PARAM_NAMES = new Set([
1419
+ "limit",
1420
+ "before",
1421
+ "after",
1422
+ "order"
1423
+ ]);
1424
+ /** Map HTTP status codes to their corresponding exception class names for @throws docs. */
1425
+ const STATUS_TO_EXCEPTION_NAME = {
1426
+ 400: "BadRequestException",
1427
+ 401: "UnauthorizedException",
1428
+ 404: "NotFoundException",
1429
+ 409: "ConflictException",
1430
+ 422: "UnprocessableEntityException",
1431
+ 429: "RateLimitExceededException"
1432
+ };
1433
+ /**
1434
+ * Compute the options interface name for a paginated method.
1435
+ * When the method name is simply "list", prefix with the service name to avoid
1436
+ * naming collisions at barrel-export level (e.g. "ConnectionsListOptions"
1437
+ * instead of the generic "ListOptions").
1438
+ */
1439
+ function paginatedOptionsName(method, resolvedServiceName) {
1440
+ if (method === "list") return `${toPascalCase(resolvedServiceName)}ListOptions`;
1441
+ return toPascalCase(method) + "Options";
1442
+ }
1443
+ /** HTTP methods that require a body argument even when the spec has no request body. */
1444
+ function httpMethodNeedsBody(method) {
1445
+ return method === "post" || method === "put" || method === "patch";
1446
+ }
1447
+ function generateResources(services, ctx) {
1448
+ if (services.length === 0) return [];
1449
+ const files = [];
1450
+ for (const service of services) {
1451
+ if (isServiceCoveredByExisting(service, ctx)) continue;
1452
+ const ops = uncoveredOperations(service, ctx);
1453
+ if (ops.length === 0) continue;
1454
+ if (ops.length < service.operations.length) {
1455
+ const file = generateResourceClass({
1456
+ ...service,
1457
+ operations: ops
1458
+ }, ctx);
1459
+ delete file.skipIfExists;
1460
+ files.push(file);
1461
+ } else files.push(generateResourceClass(service, ctx));
1462
+ }
1463
+ return files;
1464
+ }
1465
+ function generateResourceClass(service, ctx) {
1466
+ const resolvedName = resolveResourceClassName(service, ctx);
1467
+ const serviceDir = serviceDirName(resolvedName);
1468
+ const serviceClass = resolvedName;
1469
+ const resourcePath = `src/${serviceDir}/${fileName(resolvedName)}.ts`;
1470
+ const plans = service.operations.map((op) => ({
1471
+ op,
1472
+ plan: planOperation(op),
1473
+ method: resolveMethodName(op, service, ctx)
1474
+ }));
1475
+ if (ctx.overlayLookup?.methodByOperation) {
1476
+ const methodOrder = /* @__PURE__ */ new Map();
1477
+ let pos = 0;
1478
+ for (const [, info] of ctx.overlayLookup.methodByOperation) if (!methodOrder.has(info.methodName)) methodOrder.set(info.methodName, pos++);
1479
+ if (methodOrder.size > 0) plans.sort((a, b) => {
1480
+ return (methodOrder.get(a.method) ?? Number.MAX_SAFE_INTEGER) - (methodOrder.get(b.method) ?? Number.MAX_SAFE_INTEGER);
1481
+ });
1482
+ }
1483
+ const hasPaginated = plans.some((p) => p.plan.isPaginated);
1484
+ const modelMap = new Map(ctx.spec.models.map((m) => [m.name, m]));
1485
+ const responseModels = /* @__PURE__ */ new Set();
1486
+ const requestModels = /* @__PURE__ */ new Set();
1487
+ const paramEnums = /* @__PURE__ */ new Set();
1488
+ const paramModels = /* @__PURE__ */ new Set();
1489
+ for (const { op, plan } of plans) {
1490
+ if (plan.isPaginated && op.pagination?.itemType.kind === "model") {
1491
+ let itemName = op.pagination.itemType.name;
1492
+ const itemModel = modelMap.get(itemName);
1493
+ if (itemModel) {
1494
+ const unwrapped = unwrapListModel(itemModel, modelMap);
1495
+ if (unwrapped) itemName = unwrapped.name;
1496
+ }
1497
+ responseModels.add(itemName);
1498
+ } else if (plan.responseModelName) responseModels.add(plan.responseModelName);
1499
+ const bodyInfo = extractRequestBodyType(op, ctx);
1500
+ if (bodyInfo?.kind === "model") requestModels.add(bodyInfo.name);
1501
+ else if (bodyInfo?.kind === "union") if (bodyInfo.discriminator) for (const name of bodyInfo.modelNames) requestModels.add(name);
1502
+ else for (const name of bodyInfo.modelNames) paramModels.add(name);
1503
+ const queryParams = plan.isPaginated ? op.queryParams.filter((p) => !PAGINATION_PARAM_NAMES.has(p.name)) : op.queryParams;
1504
+ for (const param of [...queryParams, ...op.pathParams]) collectParamTypeRefs(param.type, paramEnums, paramModels);
1505
+ }
1506
+ const allModels = new Set([
1507
+ ...responseModels,
1508
+ ...requestModels,
1509
+ ...paramModels
1510
+ ]);
1511
+ const lines = [];
1512
+ lines.push("import type { WorkOS } from '../workos';");
1513
+ if (hasPaginated) {
1514
+ lines.push("import type { PaginationOptions } from '../common/interfaces/pagination-options.interface';");
1515
+ lines.push("import type { AutoPaginatable } from '../common/utils/pagination';");
1516
+ lines.push("import { createPaginatedList } from '../common/utils/fetch-and-deserialize';");
1517
+ }
1518
+ const hasIdempotentPost = plans.some((p) => p.plan.isIdempotentPost);
1519
+ const hasCustomEncoding = plans.some((p) => p.op.requestBodyEncoding && p.op.requestBodyEncoding !== "json" && p.plan.hasBody);
1520
+ if (hasIdempotentPost || hasCustomEncoding) lines.push("import type { PostOptions } from '../common/interfaces/post-options.interface';");
1521
+ const { modelToService, resolveDir } = createServiceDirResolver(ctx.spec.models, ctx.spec.services, ctx);
1522
+ const usedWireTypes = /* @__PURE__ */ new Set();
1523
+ for (const name of responseModels) usedWireTypes.add(resolveInterfaceName(name, ctx));
1524
+ const importedTypeNames = /* @__PURE__ */ new Set();
1525
+ for (const name of allModels) {
1526
+ const resolved = resolveInterfaceName(name, ctx);
1527
+ if (importedTypeNames.has(resolved)) continue;
1528
+ importedTypeNames.add(resolved);
1529
+ const modelServiceDir = resolveDir(modelToService.get(name));
1530
+ const relPath = modelServiceDir === serviceDir ? `./interfaces/${fileName(name)}.interface` : `../${modelServiceDir}/interfaces/${fileName(name)}.interface`;
1531
+ if (usedWireTypes.has(resolved)) lines.push(`import type { ${resolved}, ${wireInterfaceName(resolved)} } from '${relPath}';`);
1532
+ else lines.push(`import type { ${resolved} } from '${relPath}';`);
1533
+ }
1534
+ const serializerImportsByPath = /* @__PURE__ */ new Map();
1535
+ const importedDeserializers = /* @__PURE__ */ new Set();
1536
+ for (const name of responseModels) {
1537
+ const resolved = resolveInterfaceName(name, ctx);
1538
+ if (importedDeserializers.has(resolved)) continue;
1539
+ importedDeserializers.add(resolved);
1540
+ const modelServiceDir = resolveDir(modelToService.get(name));
1541
+ const relPath = modelServiceDir === serviceDir ? `./serializers/${fileName(name)}.serializer` : `../${modelServiceDir}/serializers/${fileName(name)}.serializer`;
1542
+ const existing = serializerImportsByPath.get(relPath) ?? [];
1543
+ existing.push(`deserialize${resolved}`);
1544
+ serializerImportsByPath.set(relPath, existing);
1545
+ }
1546
+ const importedSerializers = /* @__PURE__ */ new Set();
1547
+ for (const name of requestModels) {
1548
+ const resolved = resolveInterfaceName(name, ctx);
1549
+ if (importedSerializers.has(resolved)) continue;
1550
+ importedSerializers.add(resolved);
1551
+ const modelServiceDir = resolveDir(modelToService.get(name));
1552
+ const relPath = modelServiceDir === serviceDir ? `./serializers/${fileName(name)}.serializer` : `../${modelServiceDir}/serializers/${fileName(name)}.serializer`;
1553
+ const existing = serializerImportsByPath.get(relPath) ?? [];
1554
+ existing.push(`serialize${resolved}`);
1555
+ serializerImportsByPath.set(relPath, existing);
1556
+ }
1557
+ for (const [relPath, specifiers] of serializerImportsByPath) lines.push(`import { ${specifiers.join(", ")} } from '${relPath}';`);
1558
+ const specEnumNames = new Set(ctx.spec.enums.map((e) => e.name));
1559
+ if (paramEnums.size > 0) {
1560
+ const enumToService = assignEnumsToServices(ctx.spec.enums, ctx.spec.services);
1561
+ for (const name of paramEnums) {
1562
+ if (allModels.has(name)) continue;
1563
+ if (!specEnumNames.has(name)) continue;
1564
+ const enumServiceDir = resolveDir(enumToService.get(name));
1565
+ const relPath = enumServiceDir === serviceDir ? `./interfaces/${fileName(name)}.interface` : `../${enumServiceDir}/interfaces/${fileName(name)}.interface`;
1566
+ lines.push(`import type { ${name} } from '${relPath}';`);
1567
+ }
1568
+ }
1569
+ lines.push("");
1570
+ for (const { op, plan, method } of plans) if (plan.isPaginated) {
1571
+ const extraParams = op.queryParams.filter((p) => !PAGINATION_PARAM_NAMES.has(p.name));
1572
+ if (extraParams.length > 0) {
1573
+ const optionsName = paginatedOptionsName(method, resolvedName);
1574
+ lines.push(`export interface ${optionsName} extends PaginationOptions {`);
1575
+ for (const param of extraParams) {
1576
+ const opt = !param.required ? "?" : "";
1577
+ if (param.description || param.deprecated) {
1578
+ const parts = [];
1579
+ if (param.description) parts.push(param.description);
1580
+ if (param.deprecated) parts.push("@deprecated");
1581
+ lines.push(...docComment(parts.join("\n"), 2));
1582
+ }
1583
+ lines.push(` ${fieldName(param.name)}${opt}: ${mapParamType(param.type, specEnumNames)};`);
1584
+ }
1585
+ lines.push("}");
1586
+ lines.push("");
1587
+ }
1588
+ } else if (!plan.isPaginated && !plan.hasBody && !plan.isDelete && op.queryParams.length > 0) {
1589
+ const optionsName = toPascalCase(method) + "Options";
1590
+ lines.push(`export interface ${optionsName} {`);
1591
+ for (const param of op.queryParams) {
1592
+ const opt = !param.required ? "?" : "";
1593
+ if (param.description || param.deprecated) {
1594
+ const parts = [];
1595
+ if (param.description) parts.push(param.description);
1596
+ if (param.deprecated) parts.push("@deprecated");
1597
+ lines.push(...docComment(parts.join("\n"), 2));
1598
+ }
1599
+ lines.push(` ${fieldName(param.name)}${opt}: ${mapParamType(param.type, specEnumNames)};`);
1600
+ }
1601
+ lines.push("}");
1602
+ lines.push("");
1603
+ }
1604
+ if (service.description) lines.push(...docComment(service.description));
1605
+ lines.push(`export class ${serviceClass} {`);
1606
+ lines.push(" constructor(private readonly workos: WorkOS) {}");
1607
+ for (const { op, plan, method } of plans) {
1608
+ lines.push("");
1609
+ lines.push(...renderMethod(op, plan, method, service, ctx, modelMap, specEnumNames));
1610
+ }
1611
+ lines.push("}");
1612
+ return {
1613
+ path: resourcePath,
1614
+ content: lines.join("\n"),
1615
+ skipIfExists: true
1616
+ };
1617
+ }
1618
+ function renderMethod(op, plan, method, service, ctx, modelMap, specEnumNames) {
1619
+ const lines = [];
1620
+ const responseModel = plan.responseModelName ? resolveInterfaceName(plan.responseModelName, ctx) : null;
1621
+ const pathStr = buildPathStr(op);
1622
+ const httpKey = `${op.httpMethod.toUpperCase()} ${op.path}`;
1623
+ const overlayMethod = ctx.overlayLookup?.methodByOperation?.get(httpKey);
1624
+ let validParamNames = null;
1625
+ if (overlayMethod) validParamNames = new Set(overlayMethod.params.map((p) => p.name));
1626
+ else {
1627
+ const actualParams = /* @__PURE__ */ new Set();
1628
+ for (const p of op.pathParams) actualParams.add(fieldName(p.name));
1629
+ if (plan.hasBody) actualParams.add("payload");
1630
+ if (plan.isPaginated) actualParams.add("options");
1631
+ if (!plan.isPaginated && op.queryParams.length > 0 && !plan.isDelete && responseModel) actualParams.add("options");
1632
+ validParamNames = actualParams;
1633
+ }
1634
+ const docParts = [];
1635
+ if (op.description) docParts.push(op.description);
1636
+ for (const param of op.pathParams) {
1637
+ const paramName = fieldName(param.name);
1638
+ if (validParamNames && !validParamNames.has(paramName)) continue;
1639
+ const deprecatedPrefix = param.deprecated ? "(deprecated) " : "";
1640
+ if (param.description) docParts.push(`@param ${paramName} - ${deprecatedPrefix}${param.description}`);
1641
+ else if (param.deprecated) docParts.push(`@param ${paramName} - (deprecated)`);
1642
+ if (param.default !== void 0) docParts.push(`@default ${JSON.stringify(param.default)}`);
1643
+ if (param.example !== void 0) docParts.push(`@example ${JSON.stringify(param.example)}`);
1644
+ }
1645
+ if (!plan.isPaginated) {
1646
+ if (validParamNames && (validParamNames.has("options") || overlayMethod)) for (const param of op.queryParams) {
1647
+ const paramName = `options.${fieldName(param.name)}`;
1648
+ if (validParamNames && !validParamNames.has("options") && !validParamNames.has(fieldName(param.name))) continue;
1649
+ const deprecatedPrefix = param.deprecated ? "(deprecated) " : "";
1650
+ if (param.description) docParts.push(`@param ${paramName} - ${deprecatedPrefix}${param.description}`);
1651
+ else if (param.deprecated) docParts.push(`@param ${paramName} - (deprecated)`);
1652
+ if (param.default !== void 0) docParts.push(`@default ${JSON.stringify(param.default)}`);
1653
+ if (param.example !== void 0) docParts.push(`@example ${JSON.stringify(param.example)}`);
1654
+ }
1655
+ }
1656
+ if (plan.hasBody) {
1657
+ const bodyInfo = extractRequestBodyType(op, ctx);
1658
+ if (bodyInfo?.kind === "model") {
1659
+ const bodyModel = ctx.spec.models.find((m) => m.name === bodyInfo.name);
1660
+ let payloadDesc;
1661
+ if (bodyModel?.description) payloadDesc = `@param payload - ${bodyModel.description}`;
1662
+ else if (bodyModel) {
1663
+ const requiredFieldNames = bodyModel.fields.filter((f) => f.required).map((f) => fieldName(f.name));
1664
+ payloadDesc = requiredFieldNames.length > 0 ? `@param payload - Object containing ${requiredFieldNames.join(", ")}.` : "@param payload - The request body.";
1665
+ } else payloadDesc = "@param payload - The request body.";
1666
+ docParts.push(payloadDesc);
1667
+ } else docParts.push("@param payload - The request body.");
1668
+ }
1669
+ if (plan.isPaginated) docParts.push("@param options - Pagination and filter options.");
1670
+ else if (op.queryParams.length > 0) docParts.push("@param options - Additional query options.");
1671
+ if (plan.isPaginated && op.pagination?.itemType.kind === "model") {
1672
+ let itemRawName = op.pagination.itemType.name;
1673
+ const pModel = modelMap.get(itemRawName);
1674
+ if (pModel) {
1675
+ const unwrapped = unwrapListModel(pModel, modelMap);
1676
+ if (unwrapped) itemRawName = unwrapped.name;
1677
+ }
1678
+ const itemTypeName = resolveInterfaceName(itemRawName, ctx);
1679
+ docParts.push(`@returns {AutoPaginatable<${itemTypeName}>}`);
1680
+ } else if (responseModel) docParts.push(`@returns {${responseModel}}`);
1681
+ else docParts.push("@returns {void}");
1682
+ for (const err of op.errors) {
1683
+ const exceptionName = STATUS_TO_EXCEPTION_NAME[err.statusCode];
1684
+ if (exceptionName) docParts.push(`@throws {${exceptionName}} ${err.statusCode}`);
1685
+ }
1686
+ if (op.deprecated) docParts.push("@deprecated");
1687
+ if (docParts.length > 0) {
1688
+ const allLines = [];
1689
+ for (const part of docParts) for (const line of part.split("\n")) allLines.push(line);
1690
+ if (allLines.length === 1) lines.push(` /** ${allLines[0]} */`);
1691
+ else {
1692
+ lines.push(" /**");
1693
+ for (const line of allLines) lines.push(line === "" ? " *" : ` * ${line}`);
1694
+ lines.push(" */");
1695
+ }
1696
+ }
1697
+ const preDecisionCount = lines.length;
1698
+ if (plan.isPaginated && op.pagination && op.httpMethod === "get") {
1699
+ let paginatedItemRawName = op.pagination.itemType.kind === "model" ? op.pagination.itemType.name : null;
1700
+ if (paginatedItemRawName) {
1701
+ const pModel = modelMap.get(paginatedItemRawName);
1702
+ if (pModel) {
1703
+ const unwrapped = unwrapListModel(pModel, modelMap);
1704
+ if (unwrapped) paginatedItemRawName = unwrapped.name;
1705
+ }
1706
+ }
1707
+ const paginatedItemType = paginatedItemRawName ? resolveInterfaceName(paginatedItemRawName, ctx) : responseModel;
1708
+ if (paginatedItemType) renderPaginatedMethod(lines, op, plan, method, paginatedItemType, pathStr, resolveServiceName(service, ctx), specEnumNames);
1709
+ } else if (plan.isPaginated && plan.hasBody && responseModel) renderBodyMethod(lines, op, plan, method, responseModel, pathStr, ctx, specEnumNames);
1710
+ else if (plan.isDelete && plan.hasBody) renderDeleteWithBodyMethod(lines, op, plan, method, pathStr, ctx, specEnumNames);
1711
+ else if (plan.isDelete) renderDeleteMethod(lines, op, plan, method, pathStr, specEnumNames);
1712
+ else if (plan.hasBody && responseModel) renderBodyMethod(lines, op, plan, method, responseModel, pathStr, ctx, specEnumNames);
1713
+ else if (responseModel) renderGetMethod(lines, op, plan, method, responseModel, pathStr, specEnumNames);
1714
+ else renderVoidMethod(lines, op, plan, method, pathStr, ctx, specEnumNames);
1715
+ if (lines.length === preDecisionCount) {
1716
+ const params = buildPathParams(op, specEnumNames);
1717
+ lines.push(` async ${method}(${params}): Promise<void> {`);
1718
+ lines.push(` await this.workos.${op.httpMethod}(${pathStr}${httpMethodNeedsBody(op.httpMethod) ? ", {}" : ""});`);
1719
+ lines.push(" }");
1720
+ }
1721
+ return lines;
1722
+ }
1723
+ function renderPaginatedMethod(lines, op, plan, method, itemType, pathStr, resolvedServiceName, specEnumNames) {
1724
+ const optionsType = op.queryParams.filter((p) => !PAGINATION_PARAM_NAMES.has(p.name)).length > 0 ? paginatedOptionsName(method, resolvedServiceName) : "PaginationOptions";
1725
+ const pathParams = buildPathParams(op, specEnumNames);
1726
+ const allParams = pathParams ? `${pathParams}, options?: ${optionsType}` : `options?: ${optionsType}`;
1727
+ lines.push(` async ${method}(${allParams}): Promise<AutoPaginatable<${itemType}, ${optionsType}>> {`);
1728
+ lines.push(` return createPaginatedList<${wireInterfaceName(itemType)}, ${itemType}, ${optionsType}>(this.workos, ${pathStr}, deserialize${itemType}, options);`);
1729
+ lines.push(" }");
1730
+ }
1731
+ function renderDeleteMethod(lines, op, plan, method, pathStr, specEnumNames) {
1732
+ const params = buildPathParams(op, specEnumNames);
1733
+ lines.push(` async ${method}(${params}): Promise<void> {`);
1734
+ lines.push(` await this.workos.delete(${pathStr});`);
1735
+ lines.push(" }");
1736
+ }
1737
+ function renderDeleteWithBodyMethod(lines, op, plan, method, pathStr, ctx, specEnumNames) {
1738
+ const bodyInfo = extractRequestBodyType(op, ctx);
1739
+ let requestType;
1740
+ let bodyExpr;
1741
+ if (bodyInfo?.kind === "model") {
1742
+ requestType = resolveInterfaceName(bodyInfo.name, ctx);
1743
+ bodyExpr = `serialize${requestType}(payload)`;
1744
+ } else if (bodyInfo?.kind === "union") {
1745
+ requestType = bodyInfo.typeStr;
1746
+ if (bodyInfo.discriminator) bodyExpr = renderUnionBodySerializer(bodyInfo.discriminator, ctx);
1747
+ else bodyExpr = "payload";
1748
+ } else {
1749
+ requestType = "Record<string, unknown>";
1750
+ bodyExpr = "payload";
1751
+ }
1752
+ const paramParts = [];
1753
+ for (const param of op.pathParams) paramParts.push(`${fieldName(param.name)}: ${specEnumNames ? mapParamType(param.type, specEnumNames) : mapTypeRef$1(param.type)}`);
1754
+ paramParts.push(`payload: ${requestType}`);
1755
+ lines.push(` async ${method}(${paramParts.join(", ")}): Promise<void> {`);
1756
+ lines.push(` await this.workos.deleteWithBody(${pathStr}, ${bodyExpr});`);
1757
+ lines.push(" }");
1758
+ }
1759
+ function renderBodyMethod(lines, op, plan, method, responseModel, pathStr, ctx, specEnumNames) {
1760
+ const bodyInfo = extractRequestBodyType(op, ctx);
1761
+ let requestType;
1762
+ let bodyExpr;
1763
+ if (bodyInfo?.kind === "model") {
1764
+ requestType = resolveInterfaceName(bodyInfo.name, ctx);
1765
+ bodyExpr = `serialize${requestType}(payload)`;
1766
+ } else if (bodyInfo?.kind === "union") {
1767
+ requestType = bodyInfo.typeStr;
1768
+ if (bodyInfo.discriminator) bodyExpr = renderUnionBodySerializer(bodyInfo.discriminator, ctx);
1769
+ else bodyExpr = "payload";
1770
+ } else {
1771
+ requestType = "Record<string, unknown>";
1772
+ bodyExpr = "payload";
1773
+ }
1774
+ const paramParts = [];
1775
+ for (const param of op.pathParams) paramParts.push(`${fieldName(param.name)}: ${specEnumNames ? mapParamType(param.type, specEnumNames) : mapTypeRef$1(param.type)}`);
1776
+ paramParts.push(`payload: ${requestType}`);
1777
+ if (plan.isIdempotentPost) paramParts.push("requestOptions: PostOptions = {}");
1778
+ const paramsStr = paramParts.join(", ");
1779
+ const encoding = op.requestBodyEncoding;
1780
+ const encodingOption = encoding && encoding !== "json" ? `, encoding: '${encoding}' as const` : "";
1781
+ const hasCustomEncoding = encodingOption !== "";
1782
+ lines.push(` async ${method}(${paramsStr}): Promise<${responseModel}> {`);
1783
+ if (plan.isIdempotentPost) if (hasCustomEncoding) {
1784
+ lines.push(` const { data } = await this.workos.${op.httpMethod}<${wireInterfaceName(responseModel)}>(`);
1785
+ lines.push(` ${pathStr},`);
1786
+ lines.push(` ${bodyExpr},`);
1787
+ lines.push(` { ...requestOptions${encodingOption} },`);
1788
+ lines.push(" );");
1789
+ } else {
1790
+ lines.push(` const { data } = await this.workos.${op.httpMethod}<${wireInterfaceName(responseModel)}>(`);
1791
+ lines.push(` ${pathStr},`);
1792
+ lines.push(` ${bodyExpr},`);
1793
+ lines.push(" requestOptions,");
1794
+ lines.push(" );");
1795
+ }
1796
+ else if (hasCustomEncoding) {
1797
+ lines.push(` const { data } = await this.workos.${op.httpMethod}<${wireInterfaceName(responseModel)}>(`);
1798
+ lines.push(` ${pathStr},`);
1799
+ lines.push(` ${bodyExpr},`);
1800
+ lines.push(` { ${encodingOption.slice(2)} },`);
1801
+ lines.push(" );");
1802
+ } else {
1803
+ lines.push(` const { data } = await this.workos.${op.httpMethod}<${wireInterfaceName(responseModel)}>(`);
1804
+ lines.push(` ${pathStr},`);
1805
+ lines.push(` ${bodyExpr},`);
1806
+ lines.push(" );");
1807
+ }
1808
+ lines.push(` return deserialize${responseModel}(data);`);
1809
+ lines.push(" }");
1810
+ }
1811
+ function renderGetMethod(lines, op, plan, method, responseModel, pathStr, specEnumNames) {
1812
+ const params = buildPathParams(op, specEnumNames);
1813
+ const hasQuery = op.queryParams.length > 0 && !plan.isPaginated;
1814
+ const optionsType = hasQuery ? toPascalCase(method) + "Options" : null;
1815
+ const allParams = hasQuery ? params ? `${params}, options?: ${optionsType}` : `options?: ${optionsType}` : params;
1816
+ lines.push(` async ${method}(${allParams}): Promise<${responseModel}> {`);
1817
+ if (hasQuery) {
1818
+ const queryExpr = renderQueryExpr(op.queryParams);
1819
+ lines.push(` const { data } = await this.workos.${op.httpMethod}<${wireInterfaceName(responseModel)}>(${pathStr}, {`);
1820
+ lines.push(` query: ${queryExpr},`);
1821
+ lines.push(" });");
1822
+ } else if (httpMethodNeedsBody(op.httpMethod)) lines.push(` const { data } = await this.workos.${op.httpMethod}<${wireInterfaceName(responseModel)}>(${pathStr}, {});`);
1823
+ else lines.push(` const { data } = await this.workos.${op.httpMethod}<${wireInterfaceName(responseModel)}>(${pathStr});`);
1824
+ lines.push(` return deserialize${responseModel}(data);`);
1825
+ lines.push(" }");
1826
+ }
1827
+ function renderVoidMethod(lines, op, plan, method, pathStr, ctx, specEnumNames) {
1828
+ const params = buildPathParams(op, specEnumNames);
1829
+ const hasQuery = op.queryParams.length > 0 && !plan.hasBody;
1830
+ const optionsType = hasQuery ? toPascalCase(method) + "Options" : null;
1831
+ let bodyParam = "";
1832
+ let bodyExpr = "payload";
1833
+ if (plan.hasBody) {
1834
+ const bodyInfo = extractRequestBodyType(op, ctx);
1835
+ if (bodyInfo?.kind === "model") {
1836
+ const requestType = resolveInterfaceName(bodyInfo.name, ctx);
1837
+ bodyParam = `payload: ${requestType}`;
1838
+ bodyExpr = `serialize${requestType}(payload)`;
1839
+ } else if (bodyInfo?.kind === "union") {
1840
+ bodyParam = `payload: ${bodyInfo.typeStr}`;
1841
+ if (bodyInfo.discriminator) bodyExpr = renderUnionBodySerializer(bodyInfo.discriminator, ctx);
1842
+ else bodyExpr = "payload";
1843
+ } else {
1844
+ bodyParam = "payload: Record<string, unknown>";
1845
+ bodyExpr = "payload";
1846
+ }
1847
+ }
1848
+ const paramParts = [];
1849
+ if (params) paramParts.push(params);
1850
+ if (bodyParam) paramParts.push(bodyParam);
1851
+ if (optionsType) paramParts.push(`options?: ${optionsType}`);
1852
+ const allParams = paramParts.join(", ");
1853
+ lines.push(` async ${method}(${allParams}): Promise<void> {`);
1854
+ if (plan.hasBody) lines.push(` await this.workos.${op.httpMethod}(${pathStr}, ${bodyExpr});`);
1855
+ else if (hasQuery) {
1856
+ const queryExpr = renderQueryExpr(op.queryParams);
1857
+ lines.push(` await this.workos.${op.httpMethod}(${pathStr}, {`);
1858
+ lines.push(` query: ${queryExpr},`);
1859
+ lines.push(" });");
1860
+ } else if (httpMethodNeedsBody(op.httpMethod)) lines.push(` await this.workos.${op.httpMethod}(${pathStr}, {});`);
1861
+ else lines.push(` await this.workos.${op.httpMethod}(${pathStr});`);
1862
+ lines.push(" }");
1863
+ }
1864
+ /**
1865
+ * Generate an inline query serialization expression that maps camelCase option
1866
+ * keys to their snake_case wire equivalents. When all keys already match
1867
+ * (camel === snake), returns 'options' as-is for brevity.
1868
+ */
1869
+ function renderQueryExpr(queryParams) {
1870
+ if (!queryParams.some((p) => fieldName(p.name) !== wireFieldName(p.name))) return "options";
1871
+ const parts = [];
1872
+ for (const param of queryParams) {
1873
+ const camel = fieldName(param.name);
1874
+ const snake = wireFieldName(param.name);
1875
+ if (param.required) parts.push(`${snake}: options.${camel}`);
1876
+ else parts.push(`...(options.${camel} !== undefined && { ${snake}: options.${camel} })`);
1877
+ }
1878
+ return `options ? { ${parts.join(", ")} } : undefined`;
1879
+ }
1880
+ function buildPathStr(op) {
1881
+ const interpolated = op.path.replace(/\{(\w+)\}/g, (_, p) => `\${${fieldName(p)}}`);
1882
+ return interpolated.includes("${") ? `\`${interpolated}\`` : `'${op.path}'`;
1883
+ }
1884
+ function buildPathParams(op, specEnumNames) {
1885
+ const declaredNames = new Set(op.pathParams.map((p) => fieldName(p.name)));
1886
+ const params = op.pathParams.map((p) => {
1887
+ const type = specEnumNames ? mapParamType(p.type, specEnumNames) : mapTypeRef$1(p.type);
1888
+ return `${fieldName(p.name)}: ${type}`;
1889
+ });
1890
+ const templateVars = [...op.path.matchAll(/\{(\w+)\}/g)].map(([, name]) => fieldName(name));
1891
+ for (const varName of templateVars) if (!declaredNames.has(varName)) params.push(`${varName}: string`);
1892
+ return params.join(", ");
1893
+ }
1894
+ /**
1895
+ * Walk a parameter's type tree and collect enum/model names for imports.
1896
+ * Handles arrays and nullable wrappers that may contain nested enums/models.
1897
+ */
1898
+ function collectParamTypeRefs(type, enums, models) {
1899
+ switch (type.kind) {
1900
+ case "enum":
1901
+ enums.add(type.name);
1902
+ break;
1903
+ case "model":
1904
+ models.add(type.name);
1905
+ break;
1906
+ case "array":
1907
+ collectParamTypeRefs(type.items, enums, models);
1908
+ break;
1909
+ case "nullable":
1910
+ collectParamTypeRefs(type.inner, enums, models);
1911
+ break;
1912
+ }
1913
+ }
1914
+ /**
1915
+ * Extract request body type info, supporting both single models and union types.
1916
+ * Returns structured info so callers can handle imports and serialization appropriately.
1917
+ */
1918
+ /**
1919
+ * Generate an IIFE expression that dispatches to the correct serializer for a
1920
+ * discriminated union request body. Switches on the camelCase discriminator
1921
+ * property of the domain object and calls the appropriate serialize function
1922
+ * for each mapped model variant.
1923
+ */
1924
+ function renderUnionBodySerializer(disc, ctx) {
1925
+ const prop = fieldName(disc.property);
1926
+ const cases = [];
1927
+ for (const [value, modelName] of Object.entries(disc.mapping)) {
1928
+ const resolved = resolveInterfaceName(modelName, ctx);
1929
+ cases.push(`case '${value}': return serialize${resolved}(payload as any)`);
1930
+ }
1931
+ return `(() => { switch ((payload as any).${prop}) { ${cases.join("; ")}; default: return payload } })()`;
1932
+ }
1933
+ function extractRequestBodyType(op, ctx) {
1934
+ if (!op.requestBody) return null;
1935
+ if (op.requestBody.kind === "model") return {
1936
+ kind: "model",
1937
+ name: op.requestBody.name
1938
+ };
1939
+ if (op.requestBody.kind === "union") {
1940
+ const modelNames = [];
1941
+ for (const variant of op.requestBody.variants) if (variant.kind === "model") modelNames.push(variant.name);
1942
+ if (modelNames.length > 0) return {
1943
+ kind: "union",
1944
+ typeStr: modelNames.map((n) => resolveInterfaceName(n, ctx)).join(" | "),
1945
+ modelNames,
1946
+ discriminator: op.requestBody.discriminator
1947
+ };
1948
+ }
1949
+ return null;
1950
+ }
1951
+ /**
1952
+ * Map a parameter type to a TypeScript type string, handling inline enums
1953
+ * that don't have corresponding global enum definitions. These would
1954
+ * otherwise emit bare names like `Type` or `Action` that are never imported.
1955
+ *
1956
+ * Recursively handles container types (arrays, nullable) so that inline
1957
+ * enums nested inside e.g. `array<enum>` are also inlined as string literal unions.
1958
+ */
1959
+ function mapParamType(type, specEnumNames) {
1960
+ if (type.kind === "enum" && !specEnumNames.has(type.name)) {
1961
+ if (type.values && type.values.length > 0) return type.values.map((v) => typeof v === "string" ? `'${v}'` : String(v)).join(" | ");
1962
+ return "string";
1963
+ }
1964
+ if (type.kind === "array") {
1965
+ const inner = mapParamType(type.items, specEnumNames);
1966
+ return inner.includes(" | ") ? `(${inner})[]` : `${inner}[]`;
1967
+ }
1968
+ if (type.kind === "nullable") return `${mapParamType(type.inner, specEnumNames)} | null`;
1969
+ return mapTypeRef$1(type);
1970
+ }
1971
+ //#endregion
1972
+ //#region src/node/client.ts
1973
+ function generateClient(spec, ctx) {
1974
+ const files = [];
1975
+ files.push(generateWorkOSClient(spec, ctx));
1976
+ files.push(...generateServiceBarrels(spec, ctx));
1977
+ files.push(generateBarrel(spec, ctx));
1978
+ files.push(generateWorkerBarrel(spec, ctx));
1979
+ files.push(generatePackageJson(ctx));
1980
+ files.push(generateTsConfig());
1981
+ return files;
1982
+ }
1983
+ function generateWorkOSClient(spec, ctx) {
1984
+ const lines = [];
1985
+ const hasExistingWorkOS = !!ctx.apiSurface?.classes?.["WorkOS"];
1986
+ if (!hasExistingWorkOS) lines.push("import { WorkOSBase } from './common/workos-base';");
1987
+ const coveredServices = /* @__PURE__ */ new Set();
1988
+ for (const service of spec.services) if (isServiceCoveredByExisting(service, ctx)) coveredServices.add(service.name);
1989
+ for (const service of spec.services) {
1990
+ if (coveredServices.has(service.name)) continue;
1991
+ const resolvedName = resolveResourceClassName(service, ctx);
1992
+ const serviceDir = serviceDirName(resolvedName);
1993
+ lines.push(`import { ${resolvedName} } from './${serviceDir}/${fileName(resolvedName)}';`);
1994
+ }
1995
+ lines.push("");
1996
+ if (spec.description) lines.push(...docComment(spec.description));
1997
+ const extendsClause = hasExistingWorkOS ? "" : " extends WorkOSBase";
1998
+ lines.push(`export class WorkOS${extendsClause} {`);
1999
+ if (spec.servers && spec.servers.length > 0) {
2000
+ for (const server of spec.servers) {
2001
+ const constName = serverConstName(server.description ?? server.url);
2002
+ if (server.description) lines.push(...docComment(server.description, 2));
2003
+ lines.push(` static readonly ${constName} = '${server.url}';`);
2004
+ }
2005
+ lines.push("");
2006
+ }
2007
+ const existingProps = /* @__PURE__ */ new Set();
2008
+ const baselineWorkOS = ctx.apiSurface?.classes?.["WorkOS"] ?? ctx.apiSurface?.classes?.["WorkOSNode"];
2009
+ if (baselineWorkOS?.properties) for (const name of Object.keys(baselineWorkOS.properties)) existingProps.add(name);
2010
+ for (const service of spec.services) {
2011
+ if (coveredServices.has(service.name)) continue;
2012
+ const resolvedName = resolveResourceClassName(service, ctx);
2013
+ const propName = servicePropertyName(resolvedName);
2014
+ if (existingProps.has(propName)) continue;
2015
+ lines.push(` readonly ${propName} = new ${resolvedName}(this);`);
2016
+ }
2017
+ if (needsAuthOverride(spec.auth)) {
2018
+ lines.push("");
2019
+ lines.push(" protected override setAuthHeaders(headers: Record<string, string>): void {");
2020
+ renderAuthOverride(lines, spec.auth);
2021
+ lines.push(" }");
2022
+ }
2023
+ lines.push("}");
2024
+ return {
2025
+ path: "src/workos.ts",
2026
+ content: lines.join("\n"),
2027
+ skipIfExists: true
2028
+ };
2029
+ }
2030
+ /**
2031
+ * Generate per-service barrel files (interfaces/index.ts) that re-export
2032
+ * all interface and enum files for each service directory. This reduces
2033
+ * the root barrel from ~200+ individual type exports to one wildcard
2034
+ * re-export per service.
2035
+ */
2036
+ function generateServiceBarrels(spec, ctx) {
2037
+ const files = [];
2038
+ const { modelToService, resolveDir } = createServiceDirResolver(spec.models, spec.services, ctx);
2039
+ const dirExports = /* @__PURE__ */ new Map();
2040
+ const dirSymbols = /* @__PURE__ */ new Map();
2041
+ if (ctx.apiSurface?.interfaces) for (const [name, iface] of Object.entries(ctx.apiSurface.interfaces)) {
2042
+ const sourceFile = iface.sourceFile;
2043
+ if (!sourceFile) continue;
2044
+ const match = sourceFile.match(/^src\/([^/]+)\/interfaces\//);
2045
+ if (match) {
2046
+ const dirName = match[1];
2047
+ if (!dirSymbols.has(dirName)) dirSymbols.set(dirName, /* @__PURE__ */ new Set());
2048
+ dirSymbols.get(dirName).add(name);
2049
+ }
2050
+ }
2051
+ if (ctx.apiSurface?.enums) for (const [name, enumDef] of Object.entries(ctx.apiSurface.enums)) {
2052
+ const sourceFile = enumDef.sourceFile;
2053
+ if (!sourceFile) continue;
2054
+ const match = sourceFile.match(/^src\/([^/]+)\/interfaces\//);
2055
+ if (match) {
2056
+ const dirName = match[1];
2057
+ if (!dirSymbols.has(dirName)) dirSymbols.set(dirName, /* @__PURE__ */ new Set());
2058
+ dirSymbols.get(dirName).add(name);
2059
+ }
2060
+ }
2061
+ if (ctx.apiSurface?.typeAliases) for (const [name, alias] of Object.entries(ctx.apiSurface.typeAliases)) {
2062
+ const sourceFile = alias.sourceFile;
2063
+ if (!sourceFile) continue;
2064
+ const match = sourceFile.match(/^src\/([^/]+)\/interfaces\//);
2065
+ if (match) {
2066
+ const dirName = match[1];
2067
+ if (!dirSymbols.has(dirName)) dirSymbols.set(dirName, /* @__PURE__ */ new Set());
2068
+ dirSymbols.get(dirName).add(name);
2069
+ }
2070
+ }
2071
+ const globalExistingSymbols = /* @__PURE__ */ new Set();
2072
+ for (const symbols of dirSymbols.values()) for (const sym of symbols) globalExistingSymbols.add(sym);
2073
+ for (const model of spec.models) {
2074
+ if (isListMetadataModel(model) || isListWrapperModel(model)) continue;
2075
+ const dirName = resolveDir(modelToService.get(model.name));
2076
+ if (!dirExports.has(dirName)) {
2077
+ dirExports.set(dirName, []);
2078
+ if (!dirSymbols.has(dirName)) dirSymbols.set(dirName, /* @__PURE__ */ new Set());
2079
+ }
2080
+ const domainName = resolveInterfaceName(model.name, ctx);
2081
+ const wireName = wireInterfaceName(domainName);
2082
+ const symbols = dirSymbols.get(dirName);
2083
+ if (globalExistingSymbols.has(domainName) || globalExistingSymbols.has(wireName)) continue;
2084
+ symbols.add(domainName);
2085
+ symbols.add(wireName);
2086
+ globalExistingSymbols.add(domainName);
2087
+ globalExistingSymbols.add(wireName);
2088
+ dirExports.get(dirName).push(`export * from './${fileName(model.name)}.interface';`);
2089
+ }
2090
+ for (const enumDef of spec.enums) {
2091
+ const dirName = resolveDir(findEnumService(enumDef.name, spec.services));
2092
+ if (!dirExports.has(dirName)) {
2093
+ dirExports.set(dirName, []);
2094
+ if (!dirSymbols.has(dirName)) dirSymbols.set(dirName, /* @__PURE__ */ new Set());
2095
+ }
2096
+ const symbols = dirSymbols.get(dirName);
2097
+ if (globalExistingSymbols.has(enumDef.name)) continue;
2098
+ symbols.add(enumDef.name);
2099
+ globalExistingSymbols.add(enumDef.name);
2100
+ dirExports.get(dirName).push(`export * from './${fileName(enumDef.name)}.interface';`);
2101
+ }
2102
+ for (const [dirName, exports] of dirExports) {
2103
+ const uniqueExports = [...new Set(exports)];
2104
+ uniqueExports.sort();
2105
+ files.push({
2106
+ path: `src/${dirName}/interfaces/index.ts`,
2107
+ content: uniqueExports.join("\n"),
2108
+ skipIfExists: true
2109
+ });
2110
+ }
2111
+ return files;
2112
+ }
2113
+ function generateBarrel(spec, ctx) {
2114
+ const lines = [];
2115
+ const { modelToService, resolveDir } = createServiceDirResolver(spec.models, spec.services, ctx);
2116
+ const exportedNames = /* @__PURE__ */ new Set();
2117
+ if (ctx.apiSurface?.interfaces) for (const name of Object.keys(ctx.apiSurface.interfaces)) exportedNames.add(name);
2118
+ if (ctx.apiSurface?.classes) for (const name of Object.keys(ctx.apiSurface.classes)) exportedNames.add(name);
2119
+ const existingSdkExports = /* @__PURE__ */ new Set();
2120
+ if (ctx.apiSurface?.exports) for (const names of Object.values(ctx.apiSurface.exports)) for (const name of names) existingSdkExports.add(name);
2121
+ lines.push("export * from './common/exceptions';");
2122
+ lines.push("export { AutoPaginatable } from './common/utils/pagination';");
2123
+ lines.push("export type { List, ListMetadata, ListResponse } from './common/utils/pagination';");
2124
+ lines.push("export type { PaginationOptions } from './common/interfaces/pagination-options.interface';");
2125
+ lines.push("export type { WorkOSOptions } from './common/interfaces/workos-options.interface';");
2126
+ lines.push("export type { PostOptions } from './common/interfaces/post-options.interface';");
2127
+ lines.push("export type { GetOptions } from './common/interfaces/get-options.interface';");
2128
+ lines.push("");
2129
+ for (const name of [
2130
+ "AutoPaginatable",
2131
+ "List",
2132
+ "ListMetadata",
2133
+ "ListResponse",
2134
+ "PaginationOptions",
2135
+ "WorkOSOptions",
2136
+ "PostOptions",
2137
+ "GetOptions"
2138
+ ]) exportedNames.add(name);
2139
+ const coveredServicesBarrel = /* @__PURE__ */ new Set();
2140
+ for (const service of spec.services) if (isServiceCoveredByExisting(service, ctx)) coveredServicesBarrel.add(service.name);
2141
+ const exportedDirs = /* @__PURE__ */ new Set();
2142
+ for (const service of spec.services) {
2143
+ const resolvedName = resolveResourceClassName(service, ctx);
2144
+ const serviceDir = serviceDirName(resolvedName);
2145
+ const interfacesDir = resolveDir(service.name);
2146
+ const serviceModels = spec.models.filter((m) => {
2147
+ if (modelToService.get(m.name) !== service.name) return false;
2148
+ if (isListMetadataModel(m) || isListWrapperModel(m)) return false;
2149
+ return true;
2150
+ });
2151
+ const serviceEnums = spec.enums.filter((e) => {
2152
+ return findEnumService(e.name, spec.services) === service.name;
2153
+ });
2154
+ const hasConflict = serviceModels.some((m) => existingSdkExports.has(resolveInterfaceName(m.name, ctx))) || serviceEnums.some((e) => existingSdkExports.has(e.name));
2155
+ if ((serviceModels.length > 0 || serviceEnums.length > 0) && !exportedDirs.has(interfacesDir) && !hasConflict) {
2156
+ exportedDirs.add(interfacesDir);
2157
+ lines.push(`export * from './${interfacesDir}/interfaces';`);
2158
+ for (const model of serviceModels) {
2159
+ exportedNames.add(resolveInterfaceName(model.name, ctx));
2160
+ exportedNames.add(wireInterfaceName(resolveInterfaceName(model.name, ctx)));
2161
+ }
2162
+ for (const enumDef of serviceEnums) exportedNames.add(enumDef.name);
2163
+ } else if (!hasConflict) for (const model of serviceModels) {
2164
+ const name = resolveInterfaceName(model.name, ctx);
2165
+ const wireName = wireInterfaceName(name);
2166
+ if (exportedNames.has(name) || exportedNames.has(wireName)) continue;
2167
+ if (existingSdkExports.has(name)) continue;
2168
+ exportedNames.add(name);
2169
+ exportedNames.add(wireName);
2170
+ lines.push(`export type { ${name}, ${wireName} } from './${interfacesDir}/interfaces/${fileName(model.name)}.interface';`);
2171
+ }
2172
+ if (coveredServicesBarrel.has(service.name)) lines.push(`// ${resolvedName} is covered by an existing hand-written class — not re-exported.`);
2173
+ else if (!exportedNames.has(resolvedName)) {
2174
+ exportedNames.add(resolvedName);
2175
+ lines.push(`export { ${resolvedName} } from './${serviceDir}/${fileName(resolvedName)}';`);
2176
+ }
2177
+ lines.push("");
2178
+ }
2179
+ const unassignedModels = spec.models.filter((m) => !modelToService.has(m.name));
2180
+ const commonEnums = spec.enums.filter((e) => {
2181
+ return !findEnumService(e.name, spec.services);
2182
+ });
2183
+ const commonHasConflict = unassignedModels.some((m) => existingSdkExports.has(resolveInterfaceName(m.name, ctx))) || commonEnums.some((e) => existingSdkExports.has(e.name));
2184
+ if ((unassignedModels.length > 0 || commonEnums.length > 0) && !exportedDirs.has("common") && !commonHasConflict) {
2185
+ exportedDirs.add("common");
2186
+ lines.push("export * from './common/interfaces';");
2187
+ for (const model of unassignedModels) {
2188
+ exportedNames.add(resolveInterfaceName(model.name, ctx));
2189
+ exportedNames.add(wireInterfaceName(resolveInterfaceName(model.name, ctx)));
2190
+ }
2191
+ for (const enumDef of commonEnums) exportedNames.add(enumDef.name);
2192
+ } else for (const model of unassignedModels) {
2193
+ const name = resolveInterfaceName(model.name, ctx);
2194
+ const wireName = wireInterfaceName(name);
2195
+ if (exportedNames.has(name) || exportedNames.has(wireName)) continue;
2196
+ if (existingSdkExports.has(name)) continue;
2197
+ exportedNames.add(name);
2198
+ exportedNames.add(wireName);
2199
+ lines.push(`export type { ${name}, ${wireName} } from './common/interfaces/${fileName(model.name)}.interface';`);
2200
+ }
2201
+ for (const enumDef of spec.enums) {
2202
+ if (exportedNames.has(enumDef.name)) continue;
2203
+ if (existingSdkExports.has(enumDef.name)) continue;
2204
+ exportedNames.add(enumDef.name);
2205
+ const dir = resolveDir(findEnumService(enumDef.name, spec.services));
2206
+ if (!exportedDirs.has(dir)) {
2207
+ const exportKeyword = (ctx.apiSurface?.enums?.[enumDef.name])?.members ? "export" : "export type";
2208
+ lines.push(`${exportKeyword} { ${enumDef.name} } from './${dir}/interfaces/${fileName(enumDef.name)}.interface';`);
2209
+ }
2210
+ }
2211
+ lines.push("");
2212
+ if (!ctx.apiSurface && !exportedNames.has("WorkOS")) {
2213
+ exportedNames.add("WorkOS");
2214
+ lines.push("export { WorkOS } from './workos';");
2215
+ }
2216
+ return {
2217
+ path: "src/index.ts",
2218
+ content: lines.join("\n"),
2219
+ skipIfExists: true
2220
+ };
2221
+ }
2222
+ /**
2223
+ * Generate a worker-compatible barrel file that re-exports everything from
2224
+ * the main barrel. This keeps type exports in sync automatically.
2225
+ */
2226
+ function generateWorkerBarrel(_spec, _ctx) {
2227
+ const lines = [];
2228
+ lines.push("export * from './index';");
2229
+ return {
2230
+ path: "src/index.worker.ts",
2231
+ content: lines.join("\n"),
2232
+ skipIfExists: true
2233
+ };
2234
+ }
2235
+ function findEnumService(enumName, services) {
2236
+ for (const service of services) for (const op of service.operations) {
2237
+ const refs = [];
2238
+ const collect = (ref) => {
2239
+ if (ref?.kind === "enum" && ref.name === enumName) refs.push(ref.name);
2240
+ if (ref?.items) collect(ref.items);
2241
+ if (ref?.inner) collect(ref.inner);
2242
+ if (ref?.variants) ref.variants.forEach(collect);
2243
+ if (ref?.valueType) collect(ref.valueType);
2244
+ };
2245
+ if (op.requestBody) collect(op.requestBody);
2246
+ collect(op.response);
2247
+ for (const p of [...op.pathParams, ...op.queryParams]) collect(p.type);
2248
+ if (refs.length > 0) return service.name;
2249
+ }
2250
+ }
2251
+ /**
2252
+ * Determine whether the spec's auth scheme requires overriding the
2253
+ * default bearer auth in WorkOSBase.setAuthHeaders().
2254
+ */
2255
+ function needsAuthOverride(auth) {
2256
+ if (!auth || auth.length === 0) return false;
2257
+ return auth[0].kind === "apiKey";
2258
+ }
2259
+ /**
2260
+ * Render the body of a setAuthHeaders override for non-default auth schemes.
2261
+ * Only called when needsAuthOverride() returns true.
2262
+ */
2263
+ function renderAuthOverride(lines, auth) {
2264
+ const scheme = auth[0];
2265
+ if (scheme.kind !== "apiKey") return;
2266
+ switch (scheme.in) {
2267
+ case "header":
2268
+ lines.push(` headers['${scheme.name}'] = this.key;`);
2269
+ break;
2270
+ case "query":
2271
+ lines.push(` // Auth key sent as query parameter '${scheme.name}' (see buildUrl)`);
2272
+ break;
2273
+ case "cookie":
2274
+ lines.push(` headers['Cookie'] = \`${scheme.name}=\${this.key}\`;`);
2275
+ break;
2276
+ }
2277
+ }
2278
+ /**
2279
+ * Convert a server description or URL into a SCREAMING_SNAKE_CASE constant name.
2280
+ */
2281
+ function serverConstName(description) {
2282
+ return "SERVER_" + description.replace(/https?:\/\//g, "").replace(/[^a-zA-Z0-9]+/g, "_").replace(/^_|_$/g, "").toUpperCase();
2283
+ }
2284
+ function generatePackageJson(ctx) {
2285
+ const pkg = {
2286
+ name: `@${ctx.namespace}/sdk`,
2287
+ version: "0.0.0",
2288
+ type: "module",
2289
+ main: "src/index.ts",
2290
+ types: "src/index.ts",
2291
+ exports: { ".": "./src/index.ts" },
2292
+ scripts: {
2293
+ test: "jest",
2294
+ build: "tsc"
2295
+ },
2296
+ devDependencies: {
2297
+ typescript: "^5.0.0",
2298
+ jest: "^29.0.0",
2299
+ "jest-fetch-mock": "^3.0.0",
2300
+ "@types/jest": "^29.0.0",
2301
+ "ts-jest": "^29.0.0"
2302
+ }
2303
+ };
2304
+ return {
2305
+ path: "package.json",
2306
+ content: JSON.stringify(pkg, null, 2),
2307
+ skipIfExists: true,
2308
+ integrateTarget: false
2309
+ };
2310
+ }
2311
+ function generateTsConfig() {
2312
+ return {
2313
+ path: "tsconfig.json",
2314
+ content: JSON.stringify({
2315
+ compilerOptions: {
2316
+ target: "ES2020",
2317
+ module: "CommonJS",
2318
+ lib: ["ES2020"],
2319
+ declaration: true,
2320
+ strict: true,
2321
+ exactOptionalPropertyTypes: true,
2322
+ esModuleInterop: true,
2323
+ skipLibCheck: true,
2324
+ forceConsistentCasingInFileNames: true,
2325
+ resolveJsonModule: true,
2326
+ outDir: "./lib",
2327
+ rootDir: "./src"
2328
+ },
2329
+ include: ["src/**/*"],
2330
+ exclude: [
2331
+ "node_modules",
2332
+ "lib",
2333
+ "**/*.spec.ts"
2334
+ ]
2335
+ }, null, 2),
2336
+ skipIfExists: true,
2337
+ integrateTarget: false
2338
+ };
2339
+ }
2340
+ //#endregion
2341
+ //#region src/node/errors.ts
2342
+ function generateErrors(ctx) {
2343
+ const files = [
2344
+ {
2345
+ path: "src/common/exceptions/bad-request.exception.ts",
2346
+ content: `export class BadRequestException extends Error {
2347
+ readonly status = 400;
2348
+ readonly name = 'BadRequestException';
2349
+ readonly requestID: string;
2350
+ readonly code?: string;
2351
+
2352
+ constructor({
2353
+ code,
2354
+ message,
2355
+ requestID,
2356
+ }: {
2357
+ code?: string;
2358
+ message?: string;
2359
+ requestID: string;
2360
+ }) {
2361
+ super();
2362
+ this.message = message ?? 'Bad request';
2363
+ this.requestID = requestID;
2364
+ if (code) this.code = code;
2365
+ }
2366
+ }`,
2367
+ skipIfExists: true,
2368
+ integrateTarget: false
2369
+ },
2370
+ {
2371
+ path: "src/common/exceptions/unauthorized.exception.ts",
2372
+ content: `export class UnauthorizedException extends Error {
2373
+ readonly status = 401;
2374
+ readonly name = 'UnauthorizedException';
2375
+ readonly requestID: string;
2376
+
2377
+ constructor(requestID: string) {
2378
+ super();
2379
+ this.message = 'Unauthorized';
2380
+ this.requestID = requestID;
2381
+ }
2382
+ }`,
2383
+ skipIfExists: true,
2384
+ integrateTarget: false
2385
+ },
2386
+ {
2387
+ path: "src/common/exceptions/not-found.exception.ts",
2388
+ content: `export class NotFoundException extends Error {
2389
+ readonly status = 404;
2390
+ readonly name = 'NotFoundException';
2391
+ readonly requestID: string;
2392
+ readonly code?: string;
2393
+
2394
+ constructor({
2395
+ code,
2396
+ message,
2397
+ path,
2398
+ requestID,
2399
+ }: {
2400
+ code?: string;
2401
+ message?: string;
2402
+ path: string;
2403
+ requestID: string;
2404
+ }) {
2405
+ super();
2406
+ this.message =
2407
+ message ?? \`The requested path '\${path}' could not be found.\`;
2408
+ this.requestID = requestID;
2409
+ if (code) this.code = code;
2410
+ }
2411
+ }`,
2412
+ skipIfExists: true,
2413
+ integrateTarget: false
2414
+ },
2415
+ {
2416
+ path: "src/common/exceptions/conflict.exception.ts",
2417
+ content: `export class ConflictException extends Error {
2418
+ readonly status = 409;
2419
+ readonly name = 'ConflictException';
2420
+ readonly requestID: string;
2421
+
2422
+ constructor({
2423
+ message,
2424
+ requestID,
2425
+ }: {
2426
+ message?: string;
2427
+ error?: string;
2428
+ requestID: string;
2429
+ }) {
2430
+ super();
2431
+ this.message = message ?? 'Conflict';
2432
+ this.requestID = requestID;
2433
+ }
2434
+ }`,
2435
+ skipIfExists: true,
2436
+ integrateTarget: false
2437
+ },
2438
+ {
2439
+ path: "src/common/exceptions/unprocessable-entity.exception.ts",
2440
+ content: `export interface UnprocessableEntityError {
2441
+ code: string;
2442
+ }
2443
+
2444
+ export class UnprocessableEntityException extends Error {
2445
+ readonly status = 422;
2446
+ readonly name = 'UnprocessableEntityException';
2447
+ readonly requestID: string;
2448
+ readonly code?: string;
2449
+
2450
+ constructor({
2451
+ code,
2452
+ errors,
2453
+ message,
2454
+ requestID,
2455
+ }: {
2456
+ code?: string;
2457
+ errors?: UnprocessableEntityError[];
2458
+ message?: string;
2459
+ requestID: string;
2460
+ }) {
2461
+ super();
2462
+ this.requestID = requestID;
2463
+ this.message = message ?? 'Unprocessable entity';
2464
+ if (code) this.code = code;
2465
+ if (errors) {
2466
+ const requirement =
2467
+ errors.length === 1 ? 'requirement' : 'requirements';
2468
+ this.message = \`The following \${requirement} must be met:\\n\`;
2469
+ for (const { code: errCode } of errors) {
2470
+ this.message = this.message.concat(\`\\t\${errCode}\\n\`);
2471
+ }
2472
+ }
2473
+ }
2474
+ }`,
2475
+ skipIfExists: true,
2476
+ integrateTarget: false
2477
+ },
2478
+ {
2479
+ path: "src/common/exceptions/rate-limit-exceeded.exception.ts",
2480
+ content: `export class RateLimitExceededException extends Error {
2481
+ readonly status = 429;
2482
+ readonly name = 'RateLimitExceededException';
2483
+ readonly requestID: string;
2484
+ readonly retryAfter?: number;
2485
+
2486
+ constructor(message: string, requestID: string, retryAfter?: number) {
2487
+ super();
2488
+ this.message = message ?? 'Too many requests';
2489
+ this.requestID = requestID;
2490
+ this.retryAfter = retryAfter;
2491
+ }
2492
+ }`,
2493
+ skipIfExists: true,
2494
+ integrateTarget: false
2495
+ },
2496
+ {
2497
+ path: "src/common/exceptions/generic-server.exception.ts",
2498
+ content: `export class GenericServerException extends Error {
2499
+ readonly status: number;
2500
+ readonly name = 'GenericServerException';
2501
+ readonly requestID: string;
2502
+
2503
+ constructor(status: number, message: string, requestID: string) {
2504
+ super();
2505
+ this.status = status;
2506
+ this.message = message ?? 'Server error';
2507
+ this.requestID = requestID;
2508
+ }
2509
+ }`,
2510
+ skipIfExists: true,
2511
+ integrateTarget: false
2512
+ },
2513
+ {
2514
+ path: "src/common/exceptions/no-api-key-provided.exception.ts",
2515
+ content: `export class NoApiKeyProvidedException extends Error {
2516
+ readonly name = 'NoApiKeyProvidedException';
2517
+
2518
+ constructor() {
2519
+ super();
2520
+ this.message =
2521
+ 'No API key provided. Pass it to the WorkOS constructor or set the WORKOS_API_KEY environment variable.';
2522
+ }
2523
+ }`,
2524
+ skipIfExists: true,
2525
+ integrateTarget: false
2526
+ },
2527
+ {
2528
+ path: "src/common/exceptions/index.ts",
2529
+ content: `export { BadRequestException } from './bad-request.exception';
2530
+ export { UnauthorizedException } from './unauthorized.exception';
2531
+ export { NotFoundException } from './not-found.exception';
2532
+ export { ConflictException } from './conflict.exception';
2533
+ export {
2534
+ UnprocessableEntityException,
2535
+ type UnprocessableEntityError,
2536
+ } from './unprocessable-entity.exception';
2537
+ export { RateLimitExceededException } from './rate-limit-exceeded.exception';
2538
+ export { GenericServerException } from './generic-server.exception';
2539
+ export { NoApiKeyProvidedException } from './no-api-key-provided.exception';`,
2540
+ skipIfExists: true,
2541
+ integrateTarget: false
2542
+ }
2543
+ ];
2544
+ if (ctx?.spec) {
2545
+ const typedErrors = collectTypedErrors(ctx);
2546
+ for (const { modelName, statusCode, baseException } of typedErrors) {
2547
+ const exceptionClassName = `${modelName}Exception`;
2548
+ const filePath = `src/common/exceptions/${fileName(modelName)}.exception.ts`;
2549
+ const baseImport = baseException ? `import { ${baseException} } from './${fileName(baseException.replace(/Exception$/, "")).replace(/^/, "")}.exception';\n\n` : "";
2550
+ const baseClass = baseException ?? "Error";
2551
+ files.push({
2552
+ path: filePath,
2553
+ content: `${baseImport}export class ${exceptionClassName} extends ${baseClass} {
2554
+ readonly status = ${statusCode};
2555
+ readonly name = '${exceptionClassName}';
2556
+ readonly requestID: string;
2557
+ readonly data?: any;
2558
+
2559
+ constructor({ message, requestID, data }: { message?: string; requestID: string; data?: any }) {
2560
+ ${baseException ? `super(${baseException === "UnauthorizedException" ? "requestID" : `{ message: message ?? '${modelName} error', requestID }`});` : "super();"}
2561
+ this.message = message ?? '${modelName} error';
2562
+ this.requestID = requestID;
2563
+ if (data) this.data = data;
2564
+ }
2565
+ }`,
2566
+ skipIfExists: true,
2567
+ integrateTarget: false
2568
+ });
2569
+ const existingIndex = files.find((f) => f.path === "src/common/exceptions/index.ts");
2570
+ if (existingIndex) existingIndex.content += `\nexport { ${exceptionClassName} } from './${fileName(modelName)}.exception';`;
2571
+ }
2572
+ }
2573
+ return files;
2574
+ }
2575
+ const STATUS_TO_BASE_EXCEPTION = {
2576
+ 400: "BadRequestException",
2577
+ 401: "UnauthorizedException",
2578
+ 404: "NotFoundException",
2579
+ 409: "ConflictException",
2580
+ 422: "UnprocessableEntityException",
2581
+ 429: "RateLimitExceededException"
2582
+ };
2583
+ function collectTypedErrors(ctx) {
2584
+ const seen = /* @__PURE__ */ new Set();
2585
+ const results = [];
2586
+ for (const service of ctx.spec.services) for (const op of service.operations) for (const err of op.errors) if (err.type?.kind === "model" && !seen.has(err.type.name)) {
2587
+ seen.add(err.type.name);
2588
+ results.push({
2589
+ modelName: err.type.name,
2590
+ statusCode: err.statusCode,
2591
+ baseException: STATUS_TO_BASE_EXCEPTION[err.statusCode] ?? (err.statusCode >= 500 ? "GenericServerException" : null)
2592
+ });
2593
+ }
2594
+ return results;
2595
+ }
2596
+ //#endregion
2597
+ //#region src/node/config.ts
2598
+ function generateConfig() {
2599
+ return [
2600
+ {
2601
+ path: "src/common/interfaces/workos-options.interface.ts",
2602
+ content: `export interface WorkOSOptions {
2603
+ apiKey?: string;
2604
+ apiHostname?: string;
2605
+ https?: boolean;
2606
+ port?: number;
2607
+ config?: RequestInit;
2608
+ fetchFn?: typeof fetch;
2609
+ clientId?: string;
2610
+ timeout?: number;
2611
+ }
2612
+
2613
+ export interface AppInfo {
2614
+ name: string;
2615
+ version?: string;
2616
+ }`,
2617
+ skipIfExists: true,
2618
+ integrateTarget: false
2619
+ },
2620
+ {
2621
+ path: "src/common/interfaces/post-options.interface.ts",
2622
+ content: `export interface PostOptions {
2623
+ query?: Record<string, any>;
2624
+ idempotencyKey?: string;
2625
+ warrantToken?: string;
2626
+ skipApiKeyCheck?: boolean;
2627
+ encoding?: 'json' | 'form-data' | 'form-urlencoded' | 'binary' | 'text';
2628
+ }`,
2629
+ skipIfExists: true,
2630
+ integrateTarget: false
2631
+ },
2632
+ {
2633
+ path: "src/common/interfaces/get-options.interface.ts",
2634
+ content: `export interface GetOptions {
2635
+ query?: Record<string, any>;
2636
+ accessToken?: string;
2637
+ warrantToken?: string;
2638
+ skipApiKeyCheck?: boolean;
2639
+ }`,
2640
+ skipIfExists: true,
2641
+ integrateTarget: false
2642
+ },
2643
+ {
2644
+ path: "src/common/interfaces/pagination-options.interface.ts",
2645
+ content: `export interface PaginationOptions {
2646
+ limit?: number;
2647
+ before?: string | null;
2648
+ after?: string | null;
2649
+ order?: 'asc' | 'desc';
2650
+ }`,
2651
+ skipIfExists: true,
2652
+ integrateTarget: false
2653
+ },
2654
+ {
2655
+ path: "src/common/interfaces/request-exception.interface.ts",
2656
+ content: `export interface RequestException {
2657
+ readonly status: number;
2658
+ readonly name: string;
2659
+ readonly requestID: string;
2660
+ readonly code?: string;
2661
+ }`,
2662
+ skipIfExists: true,
2663
+ integrateTarget: false
2664
+ }
2665
+ ];
2666
+ }
2667
+ //#endregion
2668
+ //#region src/node/common.ts
2669
+ function generateCommon() {
2670
+ return [
2671
+ {
2672
+ path: "src/common/utils/pagination.ts",
2673
+ content: paginationContent(),
2674
+ skipIfExists: true,
2675
+ integrateTarget: false
2676
+ },
2677
+ {
2678
+ path: "src/common/utils/fetch-and-deserialize.ts",
2679
+ content: fetchAndDeserializeContent(),
2680
+ skipIfExists: true,
2681
+ integrateTarget: true
2682
+ },
2683
+ {
2684
+ path: "src/common/serializers/list.serializer.ts",
2685
+ content: listSerializerContent(),
2686
+ skipIfExists: true,
2687
+ integrateTarget: false
2688
+ },
2689
+ {
2690
+ path: "src/common/utils/test-utils.ts",
2691
+ content: testUtilsContent(),
2692
+ skipIfExists: true,
2693
+ integrateTarget: true
2694
+ }
2695
+ ];
2696
+ }
2697
+ function paginationContent() {
2698
+ return `import type { PaginationOptions } from '../interfaces/pagination-options.interface';
2699
+
2700
+ export interface ListMetadata {
2701
+ before: string | null;
2702
+ after: string | null;
2703
+ }
2704
+
2705
+ export interface List<T> {
2706
+ object: 'list';
2707
+ data: T[];
2708
+ listMetadata: ListMetadata;
2709
+ }
2710
+
2711
+ export interface ListResponse<T> {
2712
+ object: 'list';
2713
+ data: T[];
2714
+ list_metadata: {
2715
+ before: string | null;
2716
+ after: string | null;
2717
+ };
2718
+ }
2719
+
2720
+ export class AutoPaginatable<
2721
+ ResourceType,
2722
+ ParametersType extends PaginationOptions = PaginationOptions,
2723
+ > {
2724
+ readonly object = 'list' as const;
2725
+ readonly options: ParametersType;
2726
+
2727
+ constructor(
2728
+ protected list: List<ResourceType>,
2729
+ private apiCall: (params: PaginationOptions) => Promise<List<ResourceType>>,
2730
+ options?: ParametersType,
2731
+ ) {
2732
+ this.options = options ?? ({} as ParametersType);
2733
+ }
2734
+
2735
+ get data(): ResourceType[] {
2736
+ return this.list.data;
2737
+ }
2738
+
2739
+ get listMetadata() {
2740
+ return this.list.listMetadata;
2741
+ }
2742
+
2743
+ private async *generatePages(
2744
+ params: PaginationOptions,
2745
+ ): AsyncGenerator<ResourceType[]> {
2746
+ const result = await this.apiCall({
2747
+ ...this.options,
2748
+ limit: 100,
2749
+ after: params.after,
2750
+ });
2751
+ yield result.data;
2752
+ if (result.listMetadata.after) {
2753
+ await new Promise((resolve) => setTimeout(resolve, 350));
2754
+ yield* this.generatePages({ after: result.listMetadata.after });
2755
+ }
2756
+ }
2757
+
2758
+ async autoPagination(): Promise<ResourceType[]> {
2759
+ if (this.options.limit) {
2760
+ return this.data;
2761
+ }
2762
+ const results: ResourceType[] = [];
2763
+ for await (const page of this.generatePages({
2764
+ after: this.options.after,
2765
+ })) {
2766
+ results.push(...page);
2767
+ }
2768
+ return results;
2769
+ }
2770
+ }`;
2771
+ }
2772
+ function fetchAndDeserializeContent() {
2773
+ return `import type { WorkOS } from '../../workos';
2774
+ import type { PaginationOptions } from '../interfaces/pagination-options.interface';
2775
+ import { AutoPaginatable, type List, type ListResponse } from './pagination';
2776
+
2777
+ function setDefaultOptions(
2778
+ options?: PaginationOptions,
2779
+ ): Record<string, any> {
2780
+ return {
2781
+ order: 'desc',
2782
+ ...options,
2783
+ };
2784
+ }
2785
+
2786
+ function deserializeList<T, U>(
2787
+ data: ListResponse<T>,
2788
+ deserializeFn: (item: T) => U,
2789
+ ): List<U> {
2790
+ return {
2791
+ data: data.data.map(deserializeFn),
2792
+ listMetadata: {
2793
+ before: data.list_metadata.before,
2794
+ after: data.list_metadata.after,
2795
+ },
2796
+ };
2797
+ }
2798
+
2799
+ export const fetchAndDeserialize = async <T, U>(
2800
+ workos: WorkOS,
2801
+ endpoint: string,
2802
+ deserializeFn: (data: T) => U,
2803
+ options?: PaginationOptions,
2804
+ ): Promise<List<U>> => {
2805
+ const { data } = await workos.get<ListResponse<T>>(endpoint, {
2806
+ query: setDefaultOptions(options),
2807
+ });
2808
+ return deserializeList(data, deserializeFn);
2809
+ };
2810
+
2811
+ export async function createPaginatedList<TResponse, TModel, TOptions extends PaginationOptions>(
2812
+ workos: WorkOS,
2813
+ endpoint: string,
2814
+ deserializeFn: (r: TResponse) => TModel,
2815
+ options?: TOptions,
2816
+ ): Promise<AutoPaginatable<TModel, TOptions>> {
2817
+ return new AutoPaginatable(
2818
+ await fetchAndDeserialize<TResponse, TModel>(workos, endpoint, deserializeFn, options),
2819
+ (params) => fetchAndDeserialize<TResponse, TModel>(workos, endpoint, deserializeFn, params),
2820
+ options,
2821
+ );
2822
+ }`;
2823
+ }
2824
+ function listSerializerContent() {
2825
+ return `import type { ListMetadata, ListResponse } from '../utils/pagination';
2826
+
2827
+ export function deserializeListMetadata(
2828
+ metadata: ListResponse<any>['list_metadata'],
2829
+ ): ListMetadata {
2830
+ return {
2831
+ before: metadata.before,
2832
+ after: metadata.after,
2833
+ };
2834
+ }`;
2835
+ }
2836
+ function testUtilsContent() {
2837
+ return `import fetch from 'jest-fetch-mock';
2838
+
2839
+ interface MockParams {
2840
+ status?: number;
2841
+ headers?: Record<string, string>;
2842
+ [key: string]: any;
2843
+ }
2844
+
2845
+ export function fetchOnce(
2846
+ response: any = {},
2847
+ { status = 200, headers, ...rest }: MockParams = {},
2848
+ ) {
2849
+ return fetch.once(JSON.stringify(response), {
2850
+ status,
2851
+ headers: {
2852
+ 'content-type': 'application/json;charset=UTF-8',
2853
+ ...headers,
2854
+ },
2855
+ ...rest,
2856
+ });
2857
+ }
2858
+
2859
+ export function fetchURL(): string {
2860
+ return String(fetch.mock.calls[0][0]);
2861
+ }
2862
+
2863
+ export function fetchMethod(): string {
2864
+ return String(fetch.mock.calls[0][1]?.method ?? 'GET');
2865
+ }
2866
+
2867
+ export function fetchSearchParams(): Record<string, string> {
2868
+ return Object.fromEntries(new URL(fetchURL()).searchParams);
2869
+ }
2870
+
2871
+ export function fetchHeaders(): Record<string, string> {
2872
+ const headers = fetch.mock.calls[0][1]?.headers ?? {};
2873
+ return headers as Record<string, string>;
2874
+ }
2875
+
2876
+ export function fetchBody({ raw = false } = {}): any {
2877
+ const body = fetch.mock.calls[0][1]?.body;
2878
+ if (body instanceof URLSearchParams) return body.toString();
2879
+ if (raw) return body;
2880
+ return JSON.parse(String(body));
2881
+ }
2882
+
2883
+ /**
2884
+ * Shared test helper: asserts that the given async function throws when the
2885
+ * server responds with 401 Unauthorized.
2886
+ */
2887
+ export function testUnauthorized(fn: () => Promise<any>) {
2888
+ it('throws on unauthorized', async () => {
2889
+ fetchOnce({ message: 'Unauthorized' }, { status: 401 });
2890
+ await expect(fn()).rejects.toThrow('Unauthorized');
2891
+ });
2892
+ }
2893
+
2894
+ /**
2895
+ * Shared test helper: asserts that a paginated list call returns the expected
2896
+ * shape (data array + listMetadata) and hits the correct endpoint.
2897
+ */
2898
+ export function testPaginatedList(
2899
+ fn: () => Promise<any>,
2900
+ pathContains: string,
2901
+ ) {
2902
+ it('returns paginated results', async () => {
2903
+ // Caller must have called fetchOnce with the list fixture before invoking fn
2904
+ const { data, listMetadata } = await fn();
2905
+ expect(fetchURL()).toContain(pathContains);
2906
+ expect(fetchSearchParams()).toHaveProperty('order');
2907
+ expect(Array.isArray(data)).toBe(true);
2908
+ expect(listMetadata).toBeDefined();
2909
+ });
2910
+ }
2911
+
2912
+ /**
2913
+ * Shared test helper: asserts that a paginated list call returns empty data
2914
+ * when the server responds with an empty list.
2915
+ */
2916
+ export function testEmptyResults(fn: () => Promise<any>) {
2917
+ it('handles empty results', async () => {
2918
+ fetchOnce({ data: [], list_metadata: { before: null, after: null } });
2919
+ const { data } = await fn();
2920
+ expect(data).toEqual([]);
2921
+ });
2922
+ }
2923
+
2924
+ /**
2925
+ * Shared test helper: asserts that pagination params are forwarded correctly.
2926
+ */
2927
+ export function testPaginationParams(fn: (opts: any) => Promise<any>, fixture: any) {
2928
+ it('forwards pagination params', async () => {
2929
+ fetchOnce(fixture);
2930
+ await fn({ limit: 10, after: 'cursor_abc' });
2931
+ expect(fetchSearchParams()['limit']).toBe('10');
2932
+ expect(fetchSearchParams()['after']).toBe('cursor_abc');
2933
+ });
2934
+ }`;
2935
+ }
2936
+ //#endregion
2937
+ //#region src/node/tests.ts
2938
+ function generateTests(spec, ctx) {
2939
+ const files = [];
2940
+ const fixtures = generateFixtures(spec, ctx);
2941
+ for (const f of fixtures) files.push({
2942
+ path: f.path,
2943
+ content: f.content,
2944
+ headerPlacement: "skip"
2945
+ });
2946
+ const modelMap = new Map(spec.models.map((m) => [m.name, m]));
2947
+ for (const service of spec.services) {
2948
+ if (isServiceCoveredByExisting(service, ctx)) continue;
2949
+ const ops = uncoveredOperations(service, ctx);
2950
+ if (ops.length === 0) continue;
2951
+ const testService = ops.length < service.operations.length ? {
2952
+ ...service,
2953
+ operations: ops
2954
+ } : service;
2955
+ files.push(generateServiceTest(testService, spec, ctx, modelMap));
2956
+ }
2957
+ const serializerTests = generateSerializerTests(spec, ctx);
2958
+ for (const f of serializerTests) files.push(f);
2959
+ return files;
2960
+ }
2961
+ function generateServiceTest(service, spec, ctx, modelMap) {
2962
+ const resolvedName = resolveResourceClassName(service, ctx);
2963
+ const serviceDir = serviceDirName(resolvedName);
2964
+ const serviceClass = resolvedName;
2965
+ const serviceProp = servicePropertyName(resolvedName);
2966
+ const testPath = `src/${serviceDir}/${fileName(resolvedName)}.spec.ts`;
2967
+ const plans = service.operations.map((op) => ({
2968
+ op,
2969
+ plan: planOperation(op),
2970
+ method: resolveMethodName(op, service, ctx)
2971
+ }));
2972
+ if (ctx.overlayLookup?.methodByOperation) {
2973
+ const methodOrder = /* @__PURE__ */ new Map();
2974
+ let pos = 0;
2975
+ for (const [, info] of ctx.overlayLookup.methodByOperation) if (!methodOrder.has(info.methodName)) methodOrder.set(info.methodName, pos++);
2976
+ if (methodOrder.size > 0) plans.sort((a, b) => {
2977
+ return (methodOrder.get(a.method) ?? Number.MAX_SAFE_INTEGER) - (methodOrder.get(b.method) ?? Number.MAX_SAFE_INTEGER);
2978
+ });
2979
+ }
2980
+ const { modelToService, resolveDir } = createServiceDirResolver(spec.models, spec.services, ctx);
2981
+ const lines = [];
2982
+ lines.push("import fetch from 'jest-fetch-mock';");
2983
+ const hasPaginated = plans.some((p) => p.plan.isPaginated);
2984
+ const hasBody = plans.some((p) => p.plan.hasBody);
2985
+ const testUtils = [
2986
+ "fetchOnce",
2987
+ "fetchURL",
2988
+ "fetchMethod"
2989
+ ];
2990
+ if (hasPaginated) testUtils.push("fetchSearchParams");
2991
+ if (hasBody) testUtils.push("fetchBody");
2992
+ if (hasPaginated) testUtils.push("testEmptyResults", "testPaginationParams");
2993
+ if (plans.some((p) => p.plan.responseModelName || p.plan.isPaginated)) testUtils.push("testUnauthorized");
2994
+ lines.push("import {");
2995
+ for (const util of testUtils) lines.push(` ${util},`);
2996
+ lines.push("} from '../common/utils/test-utils';");
2997
+ lines.push("import { WorkOS } from '../workos';");
2998
+ lines.push("");
2999
+ const fixtureImports = /* @__PURE__ */ new Set();
3000
+ for (const { op, plan } of plans) if (plan.isPaginated && op.pagination) {
3001
+ let itemModelName = op.pagination.itemType.kind === "model" ? op.pagination.itemType.name : null;
3002
+ if (itemModelName) {
3003
+ const rawModel = modelMap.get(itemModelName);
3004
+ if (rawModel) {
3005
+ const unwrapped = unwrapListModel(rawModel, modelMap);
3006
+ if (unwrapped) itemModelName = unwrapped.name;
3007
+ }
3008
+ const fixturePath = `./fixtures/list-${fileName(itemModelName)}.fixture.json`;
3009
+ fixtureImports.add(`import list${itemModelName}Fixture from '${fixturePath}';`);
3010
+ }
3011
+ } else if (plan.responseModelName) {
3012
+ const respDir = resolveDir(modelToService.get(plan.responseModelName));
3013
+ const fixturePath = respDir === serviceDir ? `./fixtures/${fileName(plan.responseModelName)}.fixture.json` : `../${respDir}/fixtures/${fileName(plan.responseModelName)}.fixture.json`;
3014
+ fixtureImports.add(`import ${toCamelCase(plan.responseModelName)}Fixture from '${fixturePath}';`);
3015
+ }
3016
+ for (const imp of fixtureImports) lines.push(imp);
3017
+ lines.push("");
3018
+ lines.push("const workos = new WorkOS('sk_test_Sz3IQjepeSWaI4cMS4ms4sMuU');");
3019
+ lines.push("");
3020
+ const { lines: helperLines, helpers: entityHelperNames } = generateEntityHelpers(plans, modelMap, ctx);
3021
+ for (const line of helperLines) lines.push(line);
3022
+ lines.push(`describe('${serviceClass}', () => {`);
3023
+ lines.push(" beforeEach(() => fetch.resetMocks());");
3024
+ for (const { op, plan, method } of plans) {
3025
+ lines.push("");
3026
+ lines.push(` describe('${method}', () => {`);
3027
+ if (plan.isPaginated) renderPaginatedTest(lines, op, plan, method, serviceProp, modelMap, ctx, entityHelperNames);
3028
+ else if (plan.isDelete) renderDeleteTest(lines, op, plan, method, serviceProp, modelMap);
3029
+ else if (plan.hasBody && plan.responseModelName) renderBodyTest(lines, op, plan, method, serviceProp, modelMap, ctx, entityHelperNames);
3030
+ else if (plan.responseModelName) renderGetTest(lines, op, plan, method, serviceProp, modelMap, ctx, entityHelperNames);
3031
+ else renderVoidTest(lines, op, plan, method, serviceProp, modelMap);
3032
+ if (plan.responseModelName || plan.isPaginated) renderErrorTest(lines, op, plan, method, serviceProp, modelMap);
3033
+ lines.push(" });");
3034
+ }
3035
+ lines.push("});");
3036
+ return {
3037
+ path: testPath,
3038
+ content: lines.join("\n"),
3039
+ skipIfExists: true
3040
+ };
3041
+ }
3042
+ /** Compute the test value for a single path parameter.
3043
+ * Uses distinct values per param name so multi-param paths don't all get 'test_id'.
3044
+ */
3045
+ function pathParamTestValue(param, paramName) {
3046
+ if (param?.type.kind === "enum" && param.type.values?.length) {
3047
+ const first = param.type.values[0];
3048
+ return typeof first === "string" ? first : String(first);
3049
+ }
3050
+ const name = paramName ?? param?.name;
3051
+ if (name) return `test_${fieldName(name)}`;
3052
+ return "test_id";
3053
+ }
3054
+ /** Build test arguments for all path params (handles multiple path params). */
3055
+ function buildTestPathArgs(op) {
3056
+ const templateVars = [...op.path.matchAll(/\{(\w+)\}/g)].map(([, name]) => fieldName(name));
3057
+ const declaredNames = new Set(op.pathParams.map((p) => fieldName(p.name)));
3058
+ const paramByName = new Map(op.pathParams.map((p) => [fieldName(p.name), p]));
3059
+ const allVars = [];
3060
+ for (const p of op.pathParams) allVars.push(fieldName(p.name));
3061
+ for (const v of templateVars) if (!declaredNames.has(v)) allVars.push(v);
3062
+ return allVars.map((varName) => `'${pathParamTestValue(paramByName.get(varName), varName)}'`).join(", ");
3063
+ }
3064
+ function renderPaginatedTest(lines, op, plan, method, serviceProp, modelMap, ctx, entityHelpers) {
3065
+ let itemModelName = op.pagination?.itemType.kind === "model" ? op.pagination.itemType.name : "Item";
3066
+ const rawModel = itemModelName !== "Item" ? modelMap.get(itemModelName) : null;
3067
+ if (rawModel) {
3068
+ const unwrapped = unwrapListModel(rawModel, modelMap);
3069
+ if (unwrapped) itemModelName = unwrapped.name;
3070
+ }
3071
+ const pathArgs = buildTestPathArgs(op);
3072
+ lines.push(" it('returns paginated results', async () => {");
3073
+ lines.push(` fetchOnce(list${itemModelName}Fixture);`);
3074
+ lines.push("");
3075
+ lines.push(` const { data, listMetadata } = await workos.${serviceProp}.${method}(${pathArgs});`);
3076
+ lines.push("");
3077
+ lines.push(" expect(fetchMethod()).toBe('GET');");
3078
+ const expectedPath = buildExpectedPath(op);
3079
+ lines.push(` expect(new URL(String(fetchURL())).pathname).toBe('${expectedPath}');`);
3080
+ lines.push(" expect(fetchSearchParams()).toHaveProperty('order');");
3081
+ lines.push(" expect(Array.isArray(data)).toBe(true);");
3082
+ lines.push(" expect(listMetadata).toBeDefined();");
3083
+ const paginatedHelperName = ctx ? `expect${resolveInterfaceName(itemModelName, ctx)}` : null;
3084
+ if (paginatedHelperName && entityHelpers?.has(paginatedHelperName)) {
3085
+ lines.push(" expect(data.length).toBeGreaterThan(0);");
3086
+ lines.push(` ${paginatedHelperName}(data[0]);`);
3087
+ } else {
3088
+ const itemModel = modelMap.get(itemModelName);
3089
+ if (itemModel) {
3090
+ const assertions = buildFieldAssertions(itemModel, "data[0]", modelMap);
3091
+ if (assertions.length > 0) {
3092
+ lines.push(" expect(data.length).toBeGreaterThan(0);");
3093
+ for (const assertion of assertions) lines.push(` ${assertion}`);
3094
+ }
3095
+ }
3096
+ }
3097
+ lines.push(" });");
3098
+ lines.push("");
3099
+ lines.push(` testEmptyResults(() => workos.${serviceProp}.${method}(${pathArgs}));`);
3100
+ lines.push("");
3101
+ lines.push(` testPaginationParams(`);
3102
+ lines.push(` (opts) => workos.${serviceProp}.${method}(${pathArgs ? pathArgs + ", " : ""}opts),`);
3103
+ lines.push(` list${itemModelName}Fixture,`);
3104
+ lines.push(" );");
3105
+ }
3106
+ function renderDeleteTest(lines, op, plan, method, serviceProp, modelMap) {
3107
+ const pathArgs = buildTestPathArgs(op);
3108
+ const payload = plan.hasBody ? buildTestPayload(op, modelMap) : null;
3109
+ const bodyArg = plan.hasBody ? payload ? payload.camelCaseObj : fallbackBodyArg(op, modelMap) : "";
3110
+ const args = plan.hasBody ? pathArgs ? `${pathArgs}, ${bodyArg}` : bodyArg : pathArgs;
3111
+ lines.push(" it('sends a DELETE request', async () => {");
3112
+ lines.push(" fetchOnce({}, { status: 204 });");
3113
+ lines.push("");
3114
+ lines.push(` await workos.${serviceProp}.${method}(${args});`);
3115
+ lines.push("");
3116
+ lines.push(" expect(fetchMethod()).toBe('DELETE');");
3117
+ const expectedPathDel = buildExpectedPath(op);
3118
+ lines.push(` expect(new URL(String(fetchURL())).pathname).toBe('${expectedPathDel}');`);
3119
+ if (plan.hasBody) if (payload) lines.push(` expect(fetchBody()).toEqual(expect.objectContaining(${payload.snakeCaseObj}));`);
3120
+ else lines.push(" expect(fetchBody()).toBeDefined();");
3121
+ lines.push(" });");
3122
+ }
3123
+ function renderBodyTest(lines, op, plan, method, serviceProp, modelMap, ctx, entityHelpers) {
3124
+ const responseModelName = plan.responseModelName;
3125
+ const fixture = `${toCamelCase(responseModelName)}Fixture`;
3126
+ const pathArgs = buildTestPathArgs(op);
3127
+ const payload = buildTestPayload(op, modelMap);
3128
+ const payloadArg = payload ? payload.camelCaseObj : fallbackBodyArg(op, modelMap);
3129
+ const allArgs = pathArgs ? `${pathArgs}, ${payloadArg}` : payloadArg;
3130
+ lines.push(" it('sends the correct request and returns result', async () => {");
3131
+ lines.push(` fetchOnce(${fixture});`);
3132
+ lines.push("");
3133
+ lines.push(` const result = await workos.${serviceProp}.${method}(${allArgs});`);
3134
+ lines.push("");
3135
+ lines.push(` expect(fetchMethod()).toBe('${op.httpMethod.toUpperCase()}');`);
3136
+ const expectedPath = buildExpectedPath(op);
3137
+ lines.push(` expect(new URL(String(fetchURL())).pathname).toBe('${expectedPath}');`);
3138
+ if (payload) lines.push(` expect(fetchBody()).toEqual(expect.objectContaining(${payload.snakeCaseObj}));`);
3139
+ else lines.push(" expect(fetchBody()).toBeDefined();");
3140
+ const bodyHelperName = ctx ? `expect${resolveInterfaceName(responseModelName, ctx)}` : null;
3141
+ if (bodyHelperName && entityHelpers?.has(bodyHelperName)) lines.push(` ${bodyHelperName}(result);`);
3142
+ else {
3143
+ const responseModel = modelMap.get(responseModelName);
3144
+ if (responseModel) {
3145
+ const assertions = buildFieldAssertions(responseModel, "result", modelMap);
3146
+ if (assertions.length > 0) for (const assertion of assertions) lines.push(` ${assertion}`);
3147
+ else lines.push(" expect(result).toBeDefined();");
3148
+ } else lines.push(" expect(result).toBeDefined();");
3149
+ }
3150
+ lines.push(" });");
3151
+ }
3152
+ function renderGetTest(lines, op, plan, method, serviceProp, modelMap, ctx, entityHelpers) {
3153
+ const responseModelName = plan.responseModelName;
3154
+ const fixture = `${toCamelCase(responseModelName)}Fixture`;
3155
+ const pathArgs = buildTestPathArgs(op);
3156
+ lines.push(" it('returns the expected result', async () => {");
3157
+ lines.push(` fetchOnce(${fixture});`);
3158
+ lines.push("");
3159
+ lines.push(` const result = await workos.${serviceProp}.${method}(${pathArgs});`);
3160
+ lines.push("");
3161
+ lines.push(` expect(fetchMethod()).toBe('${op.httpMethod.toUpperCase()}');`);
3162
+ const expectedPathGet = buildExpectedPath(op);
3163
+ lines.push(` expect(new URL(String(fetchURL())).pathname).toBe('${expectedPathGet}');`);
3164
+ const helperName = ctx ? `expect${resolveInterfaceName(responseModelName, ctx)}` : null;
3165
+ if (helperName && entityHelpers?.has(helperName)) lines.push(` ${helperName}(result);`);
3166
+ else {
3167
+ const responseModel = modelMap.get(responseModelName);
3168
+ if (responseModel) {
3169
+ const assertions = buildFieldAssertions(responseModel, "result", modelMap);
3170
+ if (assertions.length > 0) for (const assertion of assertions) lines.push(` ${assertion}`);
3171
+ else lines.push(" expect(result).toBeDefined();");
3172
+ } else lines.push(" expect(result).toBeDefined();");
3173
+ }
3174
+ lines.push(" });");
3175
+ }
3176
+ function renderVoidTest(lines, op, plan, method, serviceProp, modelMap) {
3177
+ const pathArgs = buildTestPathArgs(op);
3178
+ const payload = plan.hasBody ? buildTestPayload(op, modelMap) : null;
3179
+ const bodyArg = plan.hasBody ? payload ? payload.camelCaseObj : fallbackBodyArg(op, modelMap) : "";
3180
+ const args = plan.hasBody ? pathArgs ? `${pathArgs}, ${bodyArg}` : bodyArg : pathArgs;
3181
+ lines.push(" it('sends the request', async () => {");
3182
+ lines.push(" fetchOnce({});");
3183
+ lines.push("");
3184
+ lines.push(` await workos.${serviceProp}.${method}(${args});`);
3185
+ lines.push("");
3186
+ lines.push(` expect(fetchMethod()).toBe('${op.httpMethod.toUpperCase()}');`);
3187
+ const expectedPathVoid = buildExpectedPath(op);
3188
+ lines.push(` expect(new URL(String(fetchURL())).pathname).toBe('${expectedPathVoid}');`);
3189
+ if (plan.hasBody && payload) lines.push(` expect(fetchBody()).toEqual(expect.objectContaining(${payload.snakeCaseObj}));`);
3190
+ lines.push(" });");
3191
+ }
3192
+ function renderErrorTest(lines, op, plan, method, serviceProp, modelMap) {
3193
+ const args = buildCallArgs(op, plan, modelMap);
3194
+ lines.push("");
3195
+ lines.push(` testUnauthorized(() => workos.${serviceProp}.${method}(${args}));`);
3196
+ const errorStatuses = new Set(op.errors.map((e) => e.statusCode));
3197
+ if (errorStatuses.has(404) && (method.startsWith("get") || method.startsWith("find"))) {
3198
+ lines.push("");
3199
+ lines.push(" it('throws NotFoundException on 404', async () => {");
3200
+ lines.push(" fetchOnce('', { status: 404 });");
3201
+ lines.push(` await expect(workos.${serviceProp}.${method}(${args})).rejects.toThrow();`);
3202
+ lines.push(" });");
3203
+ }
3204
+ if (errorStatuses.has(422) && (method.startsWith("create") || method.startsWith("update"))) {
3205
+ lines.push("");
3206
+ lines.push(" it('throws UnprocessableEntityException on 422', async () => {");
3207
+ lines.push(" fetchOnce('', { status: 422 });");
3208
+ lines.push(` await expect(workos.${serviceProp}.${method}(${args})).rejects.toThrow();`);
3209
+ lines.push(" });");
3210
+ }
3211
+ }
3212
+ /**
3213
+ * Build the argument string for a method call in tests.
3214
+ * Shared by renderErrorTest and other test renderers.
3215
+ */
3216
+ function buildCallArgs(op, plan, modelMap) {
3217
+ const pathArgs = buildTestPathArgs(op);
3218
+ const isPaginated = plan.isPaginated;
3219
+ const hasBody = plan.hasBody;
3220
+ if (isPaginated) return pathArgs || "";
3221
+ if (hasBody) {
3222
+ const fb = fallbackBodyArg(op, modelMap);
3223
+ return pathArgs ? `${pathArgs}, ${fb}` : fb;
3224
+ }
3225
+ return pathArgs || "";
3226
+ }
3227
+ /**
3228
+ * Generate per-entity assertion helper functions for models used in 2+ tests.
3229
+ * Returns lines like: function expectConnection(result: any) { expect(...) }
3230
+ */
3231
+ /**
3232
+ * Generate per-entity assertion helper functions for models used in 2+ tests.
3233
+ * Returns { lines, helpers } where helpers is a Set of helper function names.
3234
+ */
3235
+ function generateEntityHelpers(plans, modelMap, ctx) {
3236
+ const modelUsage = /* @__PURE__ */ new Map();
3237
+ for (const { op, plan } of plans) {
3238
+ let modelName = null;
3239
+ if (plan.isPaginated && op.pagination?.itemType.kind === "model") {
3240
+ modelName = op.pagination.itemType.name;
3241
+ const rawModel = modelMap.get(modelName);
3242
+ if (rawModel) {
3243
+ const unwrapped = unwrapListModel(rawModel, modelMap);
3244
+ if (unwrapped) modelName = unwrapped.name;
3245
+ }
3246
+ } else if (plan.responseModelName) modelName = plan.responseModelName;
3247
+ if (modelName) modelUsage.set(modelName, (modelUsage.get(modelName) ?? 0) + 1);
3248
+ }
3249
+ const lines = [];
3250
+ const helpers = /* @__PURE__ */ new Set();
3251
+ for (const [modelName, count] of modelUsage) {
3252
+ if (count < 2) continue;
3253
+ const model = modelMap.get(modelName);
3254
+ if (!model) continue;
3255
+ const assertions = buildFieldAssertions(model, "result", modelMap);
3256
+ if (assertions.length === 0) continue;
3257
+ const helperName = `expect${resolveInterfaceName(modelName, ctx)}`;
3258
+ if (helpers.has(helperName)) continue;
3259
+ helpers.add(helperName);
3260
+ lines.push(`function ${helperName}(result: any) {`);
3261
+ for (const assertion of assertions) lines.push(` ${assertion}`);
3262
+ lines.push("}");
3263
+ lines.push("");
3264
+ }
3265
+ return {
3266
+ lines,
3267
+ helpers
3268
+ };
3269
+ }
3270
+ /**
3271
+ * Build field-level assertions for top-level primitive fields of a response model.
3272
+ * Returns lines like: expect(result.fieldName).toBe(fixtureValue);
3273
+ *
3274
+ * When the top level has no assertable primitive fields (e.g. wrapper types
3275
+ * whose only required fields are nested models), recurse one level into those
3276
+ * nested models so we still get meaningful assertions instead of a bare
3277
+ * `toBeDefined()`.
3278
+ */
3279
+ function buildFieldAssertions(model, accessor, modelMap) {
3280
+ const assertions = [];
3281
+ for (const field of model.fields) {
3282
+ if (!field.required) continue;
3283
+ if (field.example !== void 0) {
3284
+ const domainField = fieldName(field.name);
3285
+ if (typeof field.example === "object" && field.example !== null) assertions.push(`expect(${accessor}.${domainField}).toEqual(${JSON.stringify(field.example)});`);
3286
+ else {
3287
+ const exampleLiteral = typeof field.example === "string" ? `'${field.example}'` : String(field.example);
3288
+ assertions.push(`expect(${accessor}.${domainField}).toBe(${exampleLiteral});`);
3289
+ }
3290
+ continue;
3291
+ }
3292
+ const value = fixtureValueForType(field.type, field.name, model.name);
3293
+ if (value === null) continue;
3294
+ const domainField = fieldName(field.name);
3295
+ assertions.push(`expect(${accessor}.${domainField}).toBe(${value});`);
3296
+ }
3297
+ if (assertions.length === 0 && modelMap) for (const field of model.fields) {
3298
+ if (!field.required) continue;
3299
+ if (field.type.kind === "model") {
3300
+ const nestedModel = modelMap.get(field.type.name);
3301
+ if (nestedModel) {
3302
+ const nested = buildFieldAssertions(nestedModel, `${accessor}.${fieldName(field.name)}`);
3303
+ assertions.push(...nested);
3304
+ }
3305
+ }
3306
+ }
3307
+ return assertions;
3308
+ }
3309
+ /**
3310
+ * Return a JS literal string for the expected fixture value of a primitive field.
3311
+ * Returns null for non-primitive or complex types (arrays, models, etc.).
3312
+ */
3313
+ function fixtureValueForType(ref, name, modelName) {
3314
+ switch (ref.kind) {
3315
+ case "primitive": return fixtureValueForPrimitive(ref.type, ref.format, name, modelName);
3316
+ case "literal": return typeof ref.value === "string" ? `'${ref.value}'` : String(ref.value);
3317
+ case "enum":
3318
+ if (ref.values?.length) {
3319
+ const first = ref.values[0];
3320
+ return typeof first === "string" ? `'${first}'` : String(first);
3321
+ }
3322
+ return null;
3323
+ case "array": {
3324
+ const itemValue = fixtureValueForType(ref.items, name, modelName);
3325
+ if (itemValue !== null) return `[${itemValue}]`;
3326
+ return null;
3327
+ }
3328
+ default: return null;
3329
+ }
3330
+ }
3331
+ function fixtureValueForPrimitive(type, format, name, modelName) {
3332
+ switch (type) {
3333
+ case "string":
3334
+ if (format === "date-time") return "'2023-01-01T00:00:00.000Z'";
3335
+ if (format === "date") return "'2023-01-01'";
3336
+ if (format === "uuid") return "'00000000-0000-0000-0000-000000000000'";
3337
+ if (name === "id") return `'${ID_PREFIXES[modelName] ?? ""}01234'`;
3338
+ if (name.includes("id")) return `'${wireFieldName(name)}_01234'`;
3339
+ if (name.includes("email")) return "'test@example.com'";
3340
+ if (name.includes("url") || name.includes("uri")) return "'https://example.com'";
3341
+ if (name.includes("name")) return "'Test'";
3342
+ return `'test_${wireFieldName(name)}'`;
3343
+ case "integer": return "1";
3344
+ case "number": return "1";
3345
+ case "boolean": return "true";
3346
+ default: return null;
3347
+ }
3348
+ }
3349
+ /**
3350
+ * Build the expected full URL path for an operation, substituting path params
3351
+ * with their test values. Returns a string like '/organizations/test_id'.
3352
+ */
3353
+ function buildExpectedPath(op) {
3354
+ let path = op.path;
3355
+ for (const param of op.pathParams) path = path.replace(`{${param.name}}`, pathParamTestValue(param, fieldName(param.name)));
3356
+ return path;
3357
+ }
3358
+ /**
3359
+ * Build a realistic test payload for a request body model.
3360
+ * Returns { camelCaseObj, snakeCaseObj } as inline JS object literal strings,
3361
+ * or null if the request body is not a named model.
3362
+ *
3363
+ * camelCaseObj is what the SDK consumer passes (e.g. { organizationName: 'Test' })
3364
+ * snakeCaseObj is the expected wire format (e.g. { organization_name: 'Test' })
3365
+ */
3366
+ function buildTestPayload(op, modelMap) {
3367
+ if (!op.requestBody || op.requestBody.kind !== "model") return null;
3368
+ const model = modelMap.get(op.requestBody.name);
3369
+ if (!model) return null;
3370
+ const fields = model.fields.filter((f) => f.required);
3371
+ const usableFields = fields.filter((f) => fixtureValueForType(f.type, f.name, model.name) !== null);
3372
+ if (usableFields.length === 0 || usableFields.length < fields.length) return null;
3373
+ const camelEntries = [];
3374
+ const snakeEntries = [];
3375
+ for (const field of usableFields) {
3376
+ const value = fixtureValueForType(field.type, field.name, model.name);
3377
+ const camelKey = fieldName(field.name);
3378
+ const snakeKey = wireFieldName(field.name);
3379
+ camelEntries.push(`${camelKey}: ${value}`);
3380
+ snakeEntries.push(`${snakeKey}: ${value}`);
3381
+ }
3382
+ return {
3383
+ camelCaseObj: `{ ${camelEntries.join(", ")} }`,
3384
+ snakeCaseObj: `{ ${snakeEntries.join(", ")} }`
3385
+ };
3386
+ }
3387
+ /**
3388
+ * Compute a fallback body argument when buildTestPayload returns null.
3389
+ * If the request body model has no required fields (all optional), an empty
3390
+ * object `{}` is a valid value and doesn't need a type assertion. Otherwise,
3391
+ * fall back to `{} as any` to bypass type checking for complex required fields.
3392
+ */
3393
+ function fallbackBodyArg(op, modelMap) {
3394
+ if (!op.requestBody || op.requestBody.kind !== "model") return "{} as any";
3395
+ const model = modelMap.get(op.requestBody.name);
3396
+ if (!model) return "{} as any";
3397
+ return model.fields.some((f) => f.required) ? "{} as any" : "{}";
3398
+ }
3399
+ /**
3400
+ * Determine whether a model should get a round-trip serializer test.
3401
+ * Includes all models with at least one field — every model gets both
3402
+ * serialize and deserialize functions, so all benefit from round-trip testing.
3403
+ */
3404
+ function modelNeedsRoundTripTest(model) {
3405
+ return model.fields.length > 0;
3406
+ }
3407
+ /**
3408
+ * Generate serializer round-trip tests for models that have both serialize and
3409
+ * deserialize functions and have nested types requiring non-trivial serialization.
3410
+ */
3411
+ function generateSerializerTests(spec, ctx) {
3412
+ const files = [];
3413
+ const modelToService = assignModelsToServices$1(spec.models, spec.services);
3414
+ const serviceNameMap = /* @__PURE__ */ new Map();
3415
+ for (const service of spec.services) serviceNameMap.set(service.name, resolveResourceClassName(service, ctx));
3416
+ const resolveDir = (irService) => irService ? serviceDirName(serviceNameMap.get(irService) ?? irService) : "common";
3417
+ const eligibleModels = spec.models.filter((m) => modelNeedsRoundTripTest(m) && !isListMetadataModel(m) && !isListWrapperModel(m));
3418
+ if (eligibleModels.length === 0) return files;
3419
+ const modelsByDir = /* @__PURE__ */ new Map();
3420
+ for (const model of eligibleModels) {
3421
+ const dirName = resolveDir(modelToService.get(model.name));
3422
+ if (!modelsByDir.has(dirName)) modelsByDir.set(dirName, []);
3423
+ modelsByDir.get(dirName).push(model);
3424
+ }
3425
+ for (const [dirName, models] of modelsByDir) {
3426
+ const testPath = `src/${dirName}/serializers.spec.ts`;
3427
+ const lines = [];
3428
+ const serializerImports = [];
3429
+ const fixtureImports = [];
3430
+ for (const model of models) {
3431
+ const domainName = resolveInterfaceName(model.name, ctx);
3432
+ const modelDir = resolveDir(modelToService.get(model.name));
3433
+ const serializerPath = `src/${modelDir}/serializers/${fileName(model.name)}.serializer.ts`;
3434
+ const fixturePath = `src/${modelDir}/fixtures/${fileName(model.name)}.fixture.json`;
3435
+ serializerImports.push(`import { deserialize${domainName}, serialize${domainName} } from '${relativeImport(testPath, serializerPath)}';`);
3436
+ fixtureImports.push(`import ${toCamelCase(model.name)}Fixture from '${relativeImport(testPath, fixturePath)}';`);
3437
+ }
3438
+ for (const imp of serializerImports) lines.push(imp);
3439
+ for (const imp of fixtureImports) lines.push(imp);
3440
+ lines.push("");
3441
+ for (const model of models) {
3442
+ const domainName = resolveInterfaceName(model.name, ctx);
3443
+ const fixtureName = `${toCamelCase(model.name)}Fixture`;
3444
+ lines.push(`describe('${domainName}Serializer', () => {`);
3445
+ lines.push(" it('round-trips through serialize/deserialize', () => {");
3446
+ lines.push(` const fixture = ${fixtureName};`);
3447
+ lines.push(` const deserialized = deserialize${domainName}(fixture);`);
3448
+ lines.push(` const reserialized = serialize${domainName}(deserialized);`);
3449
+ lines.push(" expect(reserialized).toEqual(expect.objectContaining(fixture));");
3450
+ lines.push(" });");
3451
+ lines.push("});");
3452
+ lines.push("");
3453
+ }
3454
+ files.push({
3455
+ path: testPath,
3456
+ content: lines.join("\n"),
3457
+ skipIfExists: true,
3458
+ integrateTarget: false
3459
+ });
3460
+ }
3461
+ return files;
3462
+ }
3463
+ //#endregion
3464
+ //#region src/node/manifest.ts
3465
+ function generateManifest(spec, ctx) {
3466
+ const manifest = {};
3467
+ for (const service of spec.services) {
3468
+ const propName = servicePropertyName(resolveResourceClassName(service, ctx));
3469
+ for (const op of service.operations) {
3470
+ const httpKey = `${op.httpMethod.toUpperCase()} ${op.path}`;
3471
+ manifest[httpKey] = {
3472
+ sdkMethod: resolveMethodName(op, service, ctx),
3473
+ service: propName
3474
+ };
3475
+ }
3476
+ }
3477
+ return [{
3478
+ path: "smoke-manifest.json",
3479
+ content: JSON.stringify(manifest, null, 2),
3480
+ integrateTarget: false,
3481
+ overwriteExisting: true
3482
+ }];
3483
+ }
3484
+ //#endregion
3485
+ //#region src/node/index.ts
3486
+ const nodeEmitter = {
3487
+ language: "node",
3488
+ generateModels(models, ctx) {
3489
+ return [...generateModels(models, ctx), ...generateSerializers(models, ctx)];
3490
+ },
3491
+ generateEnums(enums, ctx) {
3492
+ return generateEnums(enums, ctx);
3493
+ },
3494
+ generateResources(services, ctx) {
3495
+ return generateResources(services, ctx);
3496
+ },
3497
+ generateClient(spec, ctx) {
3498
+ return generateClient(spec, ctx);
3499
+ },
3500
+ generateErrors(ctx) {
3501
+ return generateErrors(ctx);
3502
+ },
3503
+ generateConfig(_ctx) {
3504
+ return [...generateConfig(), ...generateCommon()];
3505
+ },
3506
+ generateTypeSignatures(_spec, _ctx) {
3507
+ return [];
3508
+ },
3509
+ generateTests(spec, ctx) {
3510
+ return generateTests(spec, ctx);
3511
+ },
3512
+ generateManifest(spec, ctx) {
3513
+ return generateManifest(spec, ctx);
3514
+ },
3515
+ fileHeader() {
3516
+ return "// This file is auto-generated by oagen. Do not edit.";
3517
+ }
3518
+ };
3519
+ //#endregion
3520
+ export { nodeEmitter };
3521
+
3522
+ //# sourceMappingURL=index.mjs.map