@workos/oagen-emitters 0.0.1 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) 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/.oxfmtrc.json +8 -1
  6. package/.prettierignore +1 -0
  7. package/.release-please-manifest.json +3 -0
  8. package/.vscode/settings.json +3 -0
  9. package/CHANGELOG.md +61 -0
  10. package/README.md +2 -2
  11. package/dist/index.d.mts +7 -0
  12. package/dist/index.d.mts.map +1 -0
  13. package/dist/index.mjs +4070 -0
  14. package/dist/index.mjs.map +1 -0
  15. package/package.json +14 -18
  16. package/release-please-config.json +11 -0
  17. package/smoke/sdk-dotnet.ts +17 -3
  18. package/smoke/sdk-elixir.ts +17 -3
  19. package/smoke/sdk-go.ts +21 -4
  20. package/smoke/sdk-kotlin.ts +23 -4
  21. package/smoke/sdk-node.ts +15 -3
  22. package/smoke/sdk-ruby.ts +17 -3
  23. package/smoke/sdk-rust.ts +16 -3
  24. package/src/node/client.ts +521 -206
  25. package/src/node/common.ts +74 -4
  26. package/src/node/config.ts +1 -0
  27. package/src/node/enums.ts +53 -9
  28. package/src/node/errors.ts +82 -3
  29. package/src/node/fixtures.ts +87 -16
  30. package/src/node/index.ts +66 -10
  31. package/src/node/manifest.ts +4 -2
  32. package/src/node/models.ts +251 -124
  33. package/src/node/naming.ts +107 -3
  34. package/src/node/resources.ts +1162 -108
  35. package/src/node/serializers.ts +512 -52
  36. package/src/node/tests.ts +650 -110
  37. package/src/node/type-map.ts +89 -11
  38. package/src/node/utils.ts +426 -113
  39. package/test/node/client.test.ts +1083 -20
  40. package/test/node/enums.test.ts +73 -4
  41. package/test/node/errors.test.ts +4 -21
  42. package/test/node/models.test.ts +499 -5
  43. package/test/node/naming.test.ts +14 -7
  44. package/test/node/resources.test.ts +1568 -9
  45. package/test/node/serializers.test.ts +241 -5
  46. package/tsconfig.json +2 -3
  47. package/{tsup.config.ts → tsdown.config.ts} +1 -1
  48. package/dist/index.d.ts +0 -5
  49. package/dist/index.js +0 -2158
package/dist/index.js DELETED
@@ -1,2158 +0,0 @@
1
- // src/node/models.ts
2
- import { walkTypeRef as walkTypeRef2 } from "@workos/oagen";
3
-
4
- // src/node/type-map.ts
5
- import { mapTypeRef as irMapTypeRef } from "@workos/oagen";
6
-
7
- // src/node/naming.ts
8
- import { toPascalCase, toCamelCase, toKebabCase, toSnakeCase } from "@workos/oagen";
9
- function fileName(name) {
10
- return toKebabCase(name);
11
- }
12
- function fieldName(name) {
13
- return toCamelCase(name);
14
- }
15
- function wireFieldName(name) {
16
- return toSnakeCase(name);
17
- }
18
- function wireInterfaceName(domainName) {
19
- return domainName.endsWith("Response") ? `${domainName}Wire` : `${domainName}Response`;
20
- }
21
- function serviceDirName(name) {
22
- return toKebabCase(name);
23
- }
24
- function servicePropertyName(name) {
25
- return toCamelCase(name);
26
- }
27
- function resolveServiceName(service, ctx) {
28
- return resolveClassName(service, ctx);
29
- }
30
- function buildServiceNameMap(services, ctx) {
31
- const map = /* @__PURE__ */ new Map();
32
- for (const service of services) {
33
- map.set(service.name, resolveServiceName(service, ctx));
34
- }
35
- return map;
36
- }
37
- function resolveMethodName(op, _service, ctx) {
38
- const httpKey = `${op.httpMethod.toUpperCase()} ${op.path}`;
39
- const existing = ctx.overlayLookup?.methodByOperation?.get(httpKey);
40
- if (existing) return existing.methodName;
41
- return toCamelCase(op.name);
42
- }
43
- function resolveClassName(service, ctx) {
44
- if (ctx.overlayLookup?.methodByOperation) {
45
- for (const op of service.operations) {
46
- const httpKey = `${op.httpMethod.toUpperCase()} ${op.path}`;
47
- const existing = ctx.overlayLookup.methodByOperation.get(httpKey);
48
- if (existing) return existing.className;
49
- }
50
- }
51
- return toPascalCase(service.name);
52
- }
53
- function resolveInterfaceName(name, ctx) {
54
- const existing = ctx.overlayLookup?.interfaceByName?.get(name);
55
- if (existing) return existing;
56
- return toPascalCase(name);
57
- }
58
-
59
- // src/node/type-map.ts
60
- function mapTypeRef(ref) {
61
- return irMapTypeRef(ref, {
62
- primitive: mapPrimitive,
63
- array: (_r, items) => `${parenthesizeUnion(items)}[]`,
64
- model: (r) => r.name,
65
- enum: (r) => r.name,
66
- union: (_r, variants) => variants.join(" | "),
67
- nullable: (_r, inner) => `${inner} | null`,
68
- literal: (r) => typeof r.value === "string" ? `'${r.value}'` : String(r.value),
69
- map: (_r, value) => `Record<string, ${value}>`
70
- });
71
- }
72
- function mapWireTypeRef(ref) {
73
- return irMapTypeRef(ref, {
74
- primitive: mapPrimitive,
75
- array: (_r, items) => `${parenthesizeUnion(items)}[]`,
76
- model: (r) => wireInterfaceName(r.name),
77
- enum: (r) => r.name,
78
- union: (_r, variants) => variants.join(" | "),
79
- nullable: (_r, inner) => `${inner} | null`,
80
- literal: (r) => typeof r.value === "string" ? `'${r.value}'` : String(r.value),
81
- map: (_r, value) => `Record<string, ${value}>`
82
- });
83
- }
84
- function mapPrimitive(ref) {
85
- switch (ref.type) {
86
- case "string":
87
- return "string";
88
- case "integer":
89
- case "number":
90
- return "number";
91
- case "boolean":
92
- return "boolean";
93
- case "unknown":
94
- return "any";
95
- }
96
- }
97
- function parenthesizeUnion(type) {
98
- return type.includes(" | ") ? `(${type})` : type;
99
- }
100
-
101
- // src/node/utils.ts
102
- import { walkTypeRef } from "@workos/oagen";
103
- function collectModelRefs(ref) {
104
- const names = [];
105
- walkTypeRef(ref, { model: (r) => names.push(r.name) });
106
- return names;
107
- }
108
- function collectEnumRefs(ref) {
109
- const names = [];
110
- walkTypeRef(ref, { enum: (r) => names.push(r.name) });
111
- return names;
112
- }
113
- function assignModelsToServices(models, services) {
114
- const modelToService = /* @__PURE__ */ new Map();
115
- const modelNames = new Set(models.map((m) => m.name));
116
- for (const service of services) {
117
- const referencedModels = /* @__PURE__ */ new Set();
118
- for (const op of service.operations) {
119
- if (op.requestBody) {
120
- for (const name of collectModelRefs(op.requestBody)) {
121
- referencedModels.add(name);
122
- }
123
- }
124
- for (const name of collectModelRefs(op.response)) {
125
- referencedModels.add(name);
126
- }
127
- for (const param of [...op.pathParams, ...op.queryParams, ...op.headerParams]) {
128
- for (const name of collectModelRefs(param.type)) {
129
- referencedModels.add(name);
130
- }
131
- }
132
- if (op.pagination) {
133
- for (const name of collectModelRefs(op.pagination.itemType)) {
134
- referencedModels.add(name);
135
- }
136
- }
137
- }
138
- const toVisit = [...referencedModels];
139
- while (toVisit.length > 0) {
140
- const name = toVisit.pop();
141
- const model = models.find((m) => m.name === name);
142
- if (!model) continue;
143
- for (const field of model.fields) {
144
- for (const ref of collectModelRefs(field.type)) {
145
- if (!referencedModels.has(ref) && modelNames.has(ref)) {
146
- referencedModels.add(ref);
147
- toVisit.push(ref);
148
- }
149
- }
150
- }
151
- }
152
- for (const name of referencedModels) {
153
- if (!modelToService.has(name)) {
154
- modelToService.set(name, service.name);
155
- }
156
- }
157
- }
158
- return modelToService;
159
- }
160
- function collectFieldDependencies(model) {
161
- const models = /* @__PURE__ */ new Set();
162
- const enums = /* @__PURE__ */ new Set();
163
- for (const field of model.fields) {
164
- for (const name of collectModelRefs(field.type)) {
165
- if (name !== model.name) models.add(name);
166
- }
167
- for (const name of collectEnumRefs(field.type)) {
168
- enums.add(name);
169
- }
170
- }
171
- return { models, enums };
172
- }
173
- function relativeImport(fromFile, toFile) {
174
- const fromDir = fromFile.split("/").slice(0, -1);
175
- const toFileParts = toFile.split("/");
176
- const toDir = toFileParts.slice(0, -1);
177
- const toFileName = toFileParts[toFileParts.length - 1];
178
- let common = 0;
179
- while (common < fromDir.length && common < toDir.length && fromDir[common] === toDir[common]) {
180
- common++;
181
- }
182
- const ups = fromDir.length - common;
183
- const downs = toDir.slice(common);
184
- const parts = [...Array(ups).fill(".."), ...downs, toFileName];
185
- let result = parts.join("/");
186
- result = result.replace(/\.ts$/, "");
187
- if (!result.startsWith(".")) result = "./" + result;
188
- return result;
189
- }
190
- function docComment(description, indent = 0) {
191
- const pad = " ".repeat(indent);
192
- const descLines = description.split("\n");
193
- if (descLines.length === 1) {
194
- return [`${pad}/** ${descLines[0]} */`];
195
- }
196
- const lines = [`${pad}/**`];
197
- for (const line of descLines) {
198
- lines.push(line === "" ? `${pad} *` : `${pad} * ${line}`);
199
- }
200
- lines.push(`${pad} */`);
201
- return lines;
202
- }
203
-
204
- // src/node/models.ts
205
- var BUILTINS = /* @__PURE__ */ new Set([
206
- "Record",
207
- "Promise",
208
- "Array",
209
- "Map",
210
- "Set",
211
- "Date",
212
- "string",
213
- "number",
214
- "boolean",
215
- "void",
216
- "null",
217
- "undefined",
218
- "any",
219
- "never",
220
- "unknown",
221
- "true",
222
- "false"
223
- ]);
224
- function generateModels(models, ctx) {
225
- if (models.length === 0) return [];
226
- const modelToService = assignModelsToServices(models, ctx.spec.services);
227
- const serviceNameMap = buildServiceNameMap(ctx.spec.services, ctx);
228
- const resolveDir = (irService) => irService ? serviceDirName(serviceNameMap.get(irService) ?? irService) : "common";
229
- const files = [];
230
- for (const model of models) {
231
- const service = modelToService.get(model.name);
232
- const dirName = resolveDir(service);
233
- const domainName = resolveInterfaceName(model.name, ctx);
234
- const responseName = wireInterfaceName(domainName);
235
- const deps = collectFieldDependencies(model);
236
- const lines = [];
237
- const baselineDomain = ctx.apiSurface?.interfaces?.[domainName];
238
- const baselineResponse = ctx.apiSurface?.interfaces?.[responseName];
239
- const importableNames = /* @__PURE__ */ new Set();
240
- importableNames.add(domainName);
241
- importableNames.add(responseName);
242
- for (const dep of deps.models) {
243
- const depName = resolveInterfaceName(dep, ctx);
244
- importableNames.add(depName);
245
- importableNames.add(wireInterfaceName(depName));
246
- }
247
- for (const dep of deps.enums) {
248
- importableNames.add(dep);
249
- }
250
- const typeDecls = /* @__PURE__ */ new Map();
251
- const crossServiceImports = /* @__PURE__ */ new Map();
252
- const enumToService = assignEnumsToServices(ctx.spec.enums, ctx.spec.services);
253
- const resolvedEnumNames = /* @__PURE__ */ new Map();
254
- for (const e of ctx.spec.enums) {
255
- resolvedEnumNames.set(resolveInterfaceName(e.name, ctx), e.name);
256
- }
257
- for (const field of model.fields) {
258
- const baselineFields = [
259
- baselineDomain?.fields?.[fieldName(field.name)],
260
- baselineResponse?.fields?.[wireFieldName(field.name)]
261
- ].filter(Boolean);
262
- for (const bf of baselineFields) {
263
- const names = bf.type.match(/\b[A-Z][a-zA-Z0-9]*\b/g);
264
- if (!names) continue;
265
- for (const name of names) {
266
- if (BUILTINS.has(name)) continue;
267
- if (importableNames.has(name)) continue;
268
- if (typeDecls.has(name)) continue;
269
- if (crossServiceImports.has(name)) continue;
270
- const irEnumName = resolvedEnumNames.get(name);
271
- if (irEnumName && !deps.enums.has(irEnumName)) {
272
- const eService = enumToService.get(irEnumName);
273
- const eDir = resolveDir(eService);
274
- const relPath = eDir === dirName ? `./${fileName(irEnumName)}.interface` : `../../${eDir}/interfaces/${fileName(irEnumName)}.interface`;
275
- crossServiceImports.set(name, { name, relPath });
276
- importableNames.add(name);
277
- continue;
278
- }
279
- const candidates = [...importableNames].filter((n) => n.endsWith(name) && n !== name);
280
- if (candidates.length === 1) {
281
- typeDecls.set(name, candidates[0]);
282
- importableNames.add(name);
283
- } else {
284
- const innerType = field.type.kind === "nullable" ? field.type.inner : field.type;
285
- const typeExpr = mapTypeRef(innerType);
286
- typeDecls.set(name, typeExpr);
287
- importableNames.add(name);
288
- }
289
- }
290
- }
291
- }
292
- for (const dep of deps.models) {
293
- const depName = resolveInterfaceName(dep, ctx);
294
- const depService = modelToService.get(dep);
295
- const depDir = resolveDir(depService);
296
- const relPath = depDir === dirName ? `./${fileName(dep)}.interface` : `../../${depDir}/interfaces/${fileName(dep)}.interface`;
297
- lines.push(`import type { ${depName}, ${wireInterfaceName(depName)} } from '${relPath}';`);
298
- }
299
- for (const dep of deps.enums) {
300
- const depService = enumToService.get(dep);
301
- const depDir = resolveDir(depService);
302
- const relPath = depDir === dirName ? `./${fileName(dep)}.interface` : `../../${depDir}/interfaces/${fileName(dep)}.interface`;
303
- lines.push(`import type { ${dep} } from '${relPath}';`);
304
- }
305
- for (const [, imp] of crossServiceImports) {
306
- lines.push(`import type { ${imp.name} } from '${imp.relPath}';`);
307
- }
308
- if (lines.length > 0) lines.push("");
309
- for (const [alias, typeExpr] of typeDecls) {
310
- lines.push(`type ${alias} = ${typeExpr};`);
311
- }
312
- if (typeDecls.size > 0) lines.push("");
313
- const typeParams = renderTypeParams(model);
314
- const seenDomainFields = /* @__PURE__ */ new Set();
315
- if (model.description) {
316
- lines.push(...docComment(model.description));
317
- }
318
- lines.push(`export interface ${domainName}${typeParams} {`);
319
- for (const field of model.fields) {
320
- const domainFieldName = fieldName(field.name);
321
- if (seenDomainFields.has(domainFieldName)) continue;
322
- seenDomainFields.add(domainFieldName);
323
- if (field.description || field.deprecated) {
324
- const parts = [];
325
- if (field.description) parts.push(field.description);
326
- if (field.deprecated) parts.push("@deprecated");
327
- lines.push(...docComment(parts.join("\n"), 2));
328
- }
329
- const baselineField = baselineDomain?.fields?.[domainFieldName];
330
- const domainWireField = wireFieldName(field.name);
331
- const responseBaselineField = baselineResponse?.fields?.[domainWireField];
332
- const domainResponseOptionalMismatch = baselineField && !baselineField.optional && responseBaselineField && responseBaselineField.optional;
333
- if (baselineField && !domainResponseOptionalMismatch && baselineTypeResolvable(baselineField.type, importableNames) && baselineFieldCompatible(baselineField, field)) {
334
- const opt = baselineField.optional ? "?" : "";
335
- lines.push(` ${domainFieldName}${opt}: ${baselineField.type};`);
336
- } else {
337
- const opt = !field.required ? "?" : "";
338
- lines.push(` ${domainFieldName}${opt}: ${mapTypeRef(field.type)};`);
339
- }
340
- }
341
- lines.push("}");
342
- lines.push("");
343
- const seenWireFields = /* @__PURE__ */ new Set();
344
- lines.push(`export interface ${responseName}${typeParams} {`);
345
- for (const field of model.fields) {
346
- const wireField = wireFieldName(field.name);
347
- if (seenWireFields.has(wireField)) continue;
348
- seenWireFields.add(wireField);
349
- const baselineField = baselineResponse?.fields?.[wireField];
350
- if (baselineField && baselineTypeResolvable(baselineField.type, importableNames) && baselineFieldCompatible(baselineField, field)) {
351
- const opt = baselineField.optional ? "?" : "";
352
- lines.push(` ${wireField}${opt}: ${baselineField.type};`);
353
- } else {
354
- const opt = !field.required ? "?" : "";
355
- lines.push(` ${wireField}${opt}: ${mapWireTypeRef(field.type)};`);
356
- }
357
- }
358
- lines.push("}");
359
- files.push({
360
- path: `src/${dirName}/interfaces/${fileName(model.name)}.interface.ts`,
361
- content: lines.join("\n"),
362
- skipIfExists: true
363
- });
364
- }
365
- return files;
366
- }
367
- function baselineTypeResolvable(typeStr, importableNames) {
368
- const matches = typeStr.match(/\b[A-Z][a-zA-Z0-9]*\b/g);
369
- if (!matches) return true;
370
- for (const name of matches) {
371
- if (BUILTINS.has(name)) continue;
372
- if (importableNames.has(name)) continue;
373
- return false;
374
- }
375
- return true;
376
- }
377
- function baselineFieldCompatible(baselineField, irField) {
378
- const irNullable = irField.type.kind === "nullable";
379
- const baselineHasNull = baselineField.type.includes("null");
380
- if (irNullable && !baselineHasNull && irField.required) {
381
- return false;
382
- }
383
- if (!irField.required && !baselineField.optional && !baselineField.type.includes("undefined")) {
384
- return false;
385
- }
386
- return true;
387
- }
388
- function renderTypeParams(model) {
389
- if (!model.typeParams?.length) return "";
390
- const params = model.typeParams.map((tp) => {
391
- const def = tp.default ? ` = ${mapTypeRef(tp.default)}` : "";
392
- return `${tp.name}${def}`;
393
- });
394
- return `<${params.join(", ")}>`;
395
- }
396
- function assignEnumsToServices(enums, services) {
397
- const enumToService = /* @__PURE__ */ new Map();
398
- const enumNames = new Set(enums.map((e) => e.name));
399
- for (const service of services) {
400
- for (const op of service.operations) {
401
- const refs = /* @__PURE__ */ new Set();
402
- const collect = (ref) => {
403
- walkTypeRef2(ref, { enum: (r) => refs.add(r.name) });
404
- };
405
- if (op.requestBody) collect(op.requestBody);
406
- collect(op.response);
407
- for (const p of [...op.pathParams, ...op.queryParams, ...op.headerParams]) {
408
- collect(p.type);
409
- }
410
- for (const name of refs) {
411
- if (enumNames.has(name) && !enumToService.has(name)) {
412
- enumToService.set(name, service.name);
413
- }
414
- }
415
- }
416
- }
417
- return enumToService;
418
- }
419
-
420
- // src/node/enums.ts
421
- import { walkTypeRef as walkTypeRef3 } from "@workos/oagen";
422
- function generateEnums(enums, ctx) {
423
- if (enums.length === 0) return [];
424
- const enumToService = assignEnumsToServices2(enums, ctx.spec.services);
425
- const serviceNameMap = buildServiceNameMap(ctx.spec.services, ctx);
426
- const resolveDir = (irService) => irService ? serviceDirName(serviceNameMap.get(irService) ?? irService) : "common";
427
- const files = [];
428
- for (const enumDef of enums) {
429
- const service = enumToService.get(enumDef.name);
430
- const dirName = resolveDir(service);
431
- const baselineEnum = ctx.apiSurface?.enums?.[enumDef.name];
432
- const baselineAlias = ctx.apiSurface?.typeAliases?.[enumDef.name];
433
- const lines = [];
434
- if (baselineEnum?.members) {
435
- lines.push(`export enum ${enumDef.name} {`);
436
- for (const [memberName, memberValue] of Object.entries(baselineEnum.members)) {
437
- const valueStr = typeof memberValue === "string" ? `'${memberValue}'` : String(memberValue);
438
- lines.push(` ${memberName} = ${valueStr},`);
439
- }
440
- lines.push("}");
441
- } else if (baselineAlias?.value) {
442
- lines.push(`export type ${enumDef.name} = ${baselineAlias.value};`);
443
- } else {
444
- const values = enumDef.values;
445
- lines.push(`export type ${enumDef.name} =`);
446
- for (let i = 0; i < values.length; i++) {
447
- const v = values[i];
448
- const valueStr = typeof v.value === "string" ? `'${v.value}'` : String(v.value);
449
- if (v.description || v.deprecated) {
450
- const parts = [];
451
- if (v.description) parts.push(v.description);
452
- if (v.deprecated) parts.push("@deprecated");
453
- lines.push(...docComment(parts.join("\n"), 2));
454
- }
455
- const suffix = i === values.length - 1 ? ";" : "";
456
- lines.push(` | ${valueStr}${suffix}`);
457
- }
458
- }
459
- files.push({
460
- path: `src/${dirName}/interfaces/${fileName(enumDef.name)}.interface.ts`,
461
- content: lines.join("\n"),
462
- skipIfExists: true
463
- });
464
- }
465
- return files;
466
- }
467
- function assignEnumsToServices2(enums, services) {
468
- const enumToService = /* @__PURE__ */ new Map();
469
- const enumNames = new Set(enums.map((e) => e.name));
470
- for (const service of services) {
471
- for (const op of service.operations) {
472
- const refs = /* @__PURE__ */ new Set();
473
- const collect = (ref) => {
474
- walkTypeRef3(ref, { enum: (r) => refs.add(r.name) });
475
- };
476
- if (op.requestBody) collect(op.requestBody);
477
- collect(op.response);
478
- for (const p of [...op.pathParams, ...op.queryParams, ...op.headerParams]) {
479
- collect(p.type);
480
- }
481
- for (const name of refs) {
482
- if (enumNames.has(name) && !enumToService.has(name)) {
483
- enumToService.set(name, service.name);
484
- }
485
- }
486
- }
487
- }
488
- return enumToService;
489
- }
490
-
491
- // src/node/serializers.ts
492
- function generateSerializers(models, ctx) {
493
- if (models.length === 0) return [];
494
- const modelToService = assignModelsToServices(models, ctx.spec.services);
495
- const serviceNameMap = buildServiceNameMap(ctx.spec.services, ctx);
496
- const resolveDir = (irService) => irService ? serviceDirName(serviceNameMap.get(irService) ?? irService) : "common";
497
- const files = [];
498
- for (const model of models) {
499
- const service = modelToService.get(model.name);
500
- const dirName = resolveDir(service);
501
- const domainName = resolveInterfaceName(model.name, ctx);
502
- const responseName = wireInterfaceName(domainName);
503
- const serializerPath = `src/${dirName}/serializers/${fileName(model.name)}.serializer.ts`;
504
- const nestedModelRefs = /* @__PURE__ */ new Set();
505
- for (const field of model.fields) {
506
- for (const ref of collectSerializedModelRefs(field.type)) {
507
- if (ref !== model.name) nestedModelRefs.add(ref);
508
- }
509
- }
510
- const lines = [];
511
- const interfacePath = `src/${dirName}/interfaces/${fileName(model.name)}.interface.ts`;
512
- lines.push(
513
- `import type { ${domainName}, ${responseName} } from '${relativeImport(serializerPath, interfacePath)}';`
514
- );
515
- for (const dep of nestedModelRefs) {
516
- const depService = modelToService.get(dep);
517
- const depDir = resolveDir(depService);
518
- const depSerializerPath = `src/${depDir}/serializers/${fileName(dep)}.serializer.ts`;
519
- const depName = resolveInterfaceName(dep, ctx);
520
- const imports = [`deserialize${depName}`, `serialize${depName}`];
521
- lines.push(`import { ${imports.join(", ")} } from '${relativeImport(serializerPath, depSerializerPath)}';`);
522
- }
523
- lines.push("");
524
- const seenDeserFields = /* @__PURE__ */ new Set();
525
- lines.push(`export const deserialize${domainName} = (`);
526
- lines.push(` response: ${responseName},`);
527
- lines.push(`): ${domainName} => ({`);
528
- for (const field of model.fields) {
529
- const domain = fieldName(field.name);
530
- if (seenDeserFields.has(domain)) continue;
531
- seenDeserFields.add(domain);
532
- const wire = wireFieldName(field.name);
533
- const wireAccess = `response.${wire}`;
534
- const expr = deserializeExpression(field.type, wireAccess, ctx);
535
- if (!field.required && expr !== wireAccess && needsNullGuard(field.type)) {
536
- lines.push(` ${domain}: ${wireAccess} != null ? ${expr} : undefined,`);
537
- } else if (field.required && expr === wireAccess) {
538
- const fallback = defaultForType(field.type);
539
- if (fallback) {
540
- lines.push(` ${domain}: ${expr} ?? ${fallback},`);
541
- } else {
542
- lines.push(` ${domain}: ${expr},`);
543
- }
544
- } else {
545
- lines.push(` ${domain}: ${expr},`);
546
- }
547
- }
548
- lines.push("});");
549
- lines.push("");
550
- lines.push(`export const serialize${domainName} = (`);
551
- lines.push(` model: ${domainName},`);
552
- lines.push(`): ${responseName} => ({`);
553
- const seenSerFields = /* @__PURE__ */ new Set();
554
- for (const field of model.fields) {
555
- const wire = wireFieldName(field.name);
556
- if (seenSerFields.has(wire)) continue;
557
- seenSerFields.add(wire);
558
- const domain = fieldName(field.name);
559
- const domainAccess = `model.${domain}`;
560
- const expr = serializeExpression(field.type, domainAccess, ctx);
561
- if (!field.required && expr !== domainAccess && needsNullGuard(field.type)) {
562
- lines.push(` ${wire}: ${domainAccess} != null ? ${expr} : undefined,`);
563
- } else {
564
- lines.push(` ${wire}: ${expr},`);
565
- }
566
- }
567
- lines.push("});");
568
- files.push({
569
- path: serializerPath,
570
- content: lines.join("\n"),
571
- skipIfExists: true
572
- });
573
- }
574
- return files;
575
- }
576
- function collectSerializedModelRefs(ref) {
577
- switch (ref.kind) {
578
- case "model":
579
- return [ref.name];
580
- case "array":
581
- if (ref.items.kind === "model") return [ref.items.name];
582
- return collectSerializedModelRefs(ref.items);
583
- case "nullable":
584
- return collectSerializedModelRefs(ref.inner);
585
- case "union": {
586
- const models = uniqueModelVariants(ref);
587
- if (models.length === 1) return models;
588
- return [];
589
- }
590
- case "map":
591
- case "primitive":
592
- case "literal":
593
- case "enum":
594
- return [];
595
- }
596
- }
597
- function deserializeExpression(ref, wireExpr, ctx) {
598
- switch (ref.kind) {
599
- case "primitive":
600
- case "literal":
601
- case "enum":
602
- return wireExpr;
603
- case "model": {
604
- const name = resolveInterfaceName(ref.name, ctx);
605
- return `deserialize${name}(${wireExpr})`;
606
- }
607
- case "array":
608
- if (ref.items.kind === "model") {
609
- const name = resolveInterfaceName(ref.items.name, ctx);
610
- return `${wireExpr}.map(deserialize${name})`;
611
- }
612
- return wireExpr;
613
- case "nullable": {
614
- const innerExpr = deserializeExpression(ref.inner, wireExpr, ctx);
615
- if (innerExpr !== wireExpr) {
616
- return `${wireExpr} != null ? ${innerExpr} : null`;
617
- }
618
- return `${wireExpr} ?? null`;
619
- }
620
- case "union": {
621
- const deserModelVariants = uniqueModelVariants(ref);
622
- if (deserModelVariants.length === 1) {
623
- const name = resolveInterfaceName(deserModelVariants[0], ctx);
624
- return `deserialize${name}(${wireExpr})`;
625
- }
626
- return wireExpr;
627
- }
628
- case "map":
629
- return wireExpr;
630
- }
631
- }
632
- function serializeExpression(ref, domainExpr, ctx) {
633
- switch (ref.kind) {
634
- case "primitive":
635
- case "literal":
636
- case "enum":
637
- return domainExpr;
638
- case "model": {
639
- const name = resolveInterfaceName(ref.name, ctx);
640
- return `serialize${name}(${domainExpr})`;
641
- }
642
- case "array":
643
- if (ref.items.kind === "model") {
644
- const name = resolveInterfaceName(ref.items.name, ctx);
645
- return `${domainExpr}.map(serialize${name})`;
646
- }
647
- return domainExpr;
648
- case "nullable": {
649
- const innerExpr = serializeExpression(ref.inner, domainExpr, ctx);
650
- if (innerExpr !== domainExpr) {
651
- return `${domainExpr} != null ? ${innerExpr} : null`;
652
- }
653
- return domainExpr;
654
- }
655
- case "union": {
656
- const serModelVariants = uniqueModelVariants(ref);
657
- if (serModelVariants.length === 1) {
658
- const name = resolveInterfaceName(serModelVariants[0], ctx);
659
- return `serialize${name}(${domainExpr})`;
660
- }
661
- return domainExpr;
662
- }
663
- case "map":
664
- return domainExpr;
665
- }
666
- }
667
- function uniqueModelVariants(ref) {
668
- const modelNames = /* @__PURE__ */ new Set();
669
- for (const v of ref.variants) {
670
- if (v.kind === "model") modelNames.add(v.name);
671
- }
672
- return [...modelNames];
673
- }
674
- function needsNullGuard(ref) {
675
- switch (ref.kind) {
676
- case "model":
677
- return true;
678
- case "array":
679
- return ref.items.kind === "model";
680
- case "nullable":
681
- return needsNullGuard(ref.inner);
682
- case "union":
683
- return uniqueModelVariants(ref).length === 1;
684
- default:
685
- return false;
686
- }
687
- }
688
- function defaultForType(ref) {
689
- switch (ref.kind) {
690
- case "map":
691
- return "{}";
692
- case "primitive":
693
- switch (ref.type) {
694
- case "boolean":
695
- return "false";
696
- case "string":
697
- return "''";
698
- case "integer":
699
- case "number":
700
- return "0";
701
- default:
702
- return null;
703
- }
704
- case "array":
705
- return "[]";
706
- default:
707
- return null;
708
- }
709
- }
710
-
711
- // src/node/resources.ts
712
- import { planOperation, toPascalCase as toPascalCase2 } from "@workos/oagen";
713
- function generateResources(services, ctx) {
714
- if (services.length === 0) return [];
715
- return services.map((service) => generateResourceClass(service, ctx));
716
- }
717
- function generateResourceClass(service, ctx) {
718
- const resolvedName = resolveServiceName(service, ctx);
719
- const serviceDir = serviceDirName(resolvedName);
720
- const serviceClass = resolvedName;
721
- const resourcePath = `src/${serviceDir}/${fileName(resolvedName)}.ts`;
722
- const plans = service.operations.map((op) => ({
723
- op,
724
- plan: planOperation(op),
725
- method: resolveMethodName(op, service, ctx)
726
- }));
727
- const hasPaginated = plans.some((p) => p.plan.isPaginated);
728
- const responseModels = /* @__PURE__ */ new Set();
729
- const requestModels = /* @__PURE__ */ new Set();
730
- for (const { op, plan } of plans) {
731
- if (plan.responseModelName) responseModels.add(plan.responseModelName);
732
- if (op.requestBody) {
733
- for (const name of collectModelRefs(op.requestBody)) {
734
- requestModels.add(name);
735
- }
736
- }
737
- }
738
- const allModels = /* @__PURE__ */ new Set([...responseModels, ...requestModels]);
739
- const lines = [];
740
- lines.push("import type { WorkOS } from '../workos';");
741
- if (hasPaginated) {
742
- lines.push("import type { PaginationOptions } from '../common/interfaces/pagination-options.interface';");
743
- lines.push("import { AutoPaginatable } from '../common/utils/pagination';");
744
- lines.push("import { fetchAndDeserialize } from '../common/utils/fetch-and-deserialize';");
745
- }
746
- const hasIdempotentPost = plans.some((p) => p.plan.isIdempotentPost);
747
- if (hasIdempotentPost) {
748
- lines.push("import type { PostOptions } from '../common/interfaces/post-options.interface';");
749
- }
750
- const modelToService = assignModelsToServices(ctx.spec.models, ctx.spec.services);
751
- const serviceNameMap = buildServiceNameMap(ctx.spec.services, ctx);
752
- const resolveDir = (irService) => irService ? serviceDirName(serviceNameMap.get(irService) ?? irService) : "common";
753
- for (const name of allModels) {
754
- const resolved = resolveInterfaceName(name, ctx);
755
- const modelDir = modelToService.get(name);
756
- const modelServiceDir = resolveDir(modelDir);
757
- const relPath = modelServiceDir === serviceDir ? `./interfaces/${fileName(name)}.interface` : `../${modelServiceDir}/interfaces/${fileName(name)}.interface`;
758
- lines.push(`import type { ${resolved}, ${wireInterfaceName(resolved)} } from '${relPath}';`);
759
- }
760
- for (const name of responseModels) {
761
- const resolved = resolveInterfaceName(name, ctx);
762
- const modelDir = modelToService.get(name);
763
- const modelServiceDir = resolveDir(modelDir);
764
- const relPath = modelServiceDir === serviceDir ? `./serializers/${fileName(name)}.serializer` : `../${modelServiceDir}/serializers/${fileName(name)}.serializer`;
765
- lines.push(`import { deserialize${resolved} } from '${relPath}';`);
766
- }
767
- for (const name of requestModels) {
768
- const resolved = resolveInterfaceName(name, ctx);
769
- const modelDir = modelToService.get(name);
770
- const modelServiceDir = resolveDir(modelDir);
771
- const relPath = modelServiceDir === serviceDir ? `./serializers/${fileName(name)}.serializer` : `../${modelServiceDir}/serializers/${fileName(name)}.serializer`;
772
- lines.push(`import { serialize${resolved} } from '${relPath}';`);
773
- }
774
- lines.push("");
775
- for (const { op, plan, method } of plans) {
776
- if (plan.isPaginated) {
777
- const extraParams = op.queryParams.filter((p) => !["limit", "before", "after", "order"].includes(p.name));
778
- if (extraParams.length > 0) {
779
- const optionsName = toPascalCase2(method) + "Options";
780
- lines.push(`export interface ${optionsName} extends PaginationOptions {`);
781
- for (const param of extraParams) {
782
- const opt = !param.required ? "?" : "";
783
- if (param.description || param.deprecated) {
784
- const parts = [];
785
- if (param.description) parts.push(param.description);
786
- if (param.deprecated) parts.push("@deprecated");
787
- lines.push(...docComment(parts.join("\n"), 2));
788
- }
789
- lines.push(` ${fieldName(param.name)}${opt}: ${mapTypeRef(param.type)};`);
790
- }
791
- lines.push("}");
792
- lines.push("");
793
- }
794
- }
795
- }
796
- if (service.description) {
797
- lines.push(...docComment(service.description));
798
- }
799
- lines.push(`export class ${serviceClass} {`);
800
- lines.push(" constructor(private readonly workos: WorkOS) {}");
801
- for (const { op, plan, method } of plans) {
802
- lines.push("");
803
- lines.push(...renderMethod(op, plan, method, service, ctx));
804
- }
805
- lines.push("}");
806
- return { path: resourcePath, content: lines.join("\n"), skipIfExists: true };
807
- }
808
- function renderMethod(op, plan, method, service, ctx) {
809
- const lines = [];
810
- const responseModel = plan.responseModelName ? resolveInterfaceName(plan.responseModelName, ctx) : null;
811
- const interpolatedPath = op.path.replace(/\{(\w+)\}/g, (_, p) => `\${${fieldName(p)}}`);
812
- const usesTemplate = interpolatedPath.includes("${");
813
- const pathStr = usesTemplate ? `\`${interpolatedPath}\`` : `'${op.path}'`;
814
- const docParts = [];
815
- if (op.description) docParts.push(op.description);
816
- for (const param of op.pathParams) {
817
- if (param.description) {
818
- docParts.push(`@param ${fieldName(param.name)} - ${param.description}`);
819
- }
820
- }
821
- if (op.deprecated) docParts.push("@deprecated");
822
- for (const err of op.errors) {
823
- const exceptionName = statusToExceptionName(err.statusCode);
824
- if (exceptionName) {
825
- docParts.push(`@throws {${exceptionName}} ${err.statusCode}`);
826
- }
827
- }
828
- if (docParts.length > 0) {
829
- const allLines = [];
830
- for (const part of docParts) {
831
- for (const line of part.split("\n")) {
832
- allLines.push(line);
833
- }
834
- }
835
- if (allLines.length === 1) {
836
- lines.push(` /** ${allLines[0]} */`);
837
- } else {
838
- lines.push(" /**");
839
- for (const line of allLines) {
840
- lines.push(line === "" ? " *" : ` * ${line}`);
841
- }
842
- lines.push(" */");
843
- }
844
- }
845
- if (plan.isPaginated) {
846
- if (!responseModel) {
847
- console.warn(
848
- `[oagen] Warning: Skipping paginated method "${method}" (${op.httpMethod.toUpperCase()} ${op.path}) \u2014 response has no named model. Ensure the spec uses a $ref for paginated item types.`
849
- );
850
- return lines;
851
- }
852
- renderPaginatedMethod(lines, op, plan, method, responseModel);
853
- } else if (plan.isDelete) {
854
- renderDeleteMethod(lines, op, plan, method, pathStr);
855
- } else if (plan.hasBody && responseModel) {
856
- renderBodyMethod(lines, op, plan, method, responseModel, pathStr, ctx);
857
- } else if (responseModel) {
858
- renderGetMethod(lines, op, plan, method, responseModel, pathStr);
859
- } else {
860
- renderVoidMethod(lines, op, plan, method, pathStr);
861
- }
862
- return lines;
863
- }
864
- function renderPaginatedMethod(lines, op, plan, method, itemType) {
865
- const extraParams = op.queryParams.filter((p) => !["limit", "before", "after", "order"].includes(p.name));
866
- const optionsType = extraParams.length > 0 ? toPascalCase2(method) + "Options" : "PaginationOptions";
867
- const pathStr = buildPathStr(op);
868
- lines.push(` async ${method}(options?: ${optionsType}): Promise<AutoPaginatable<${itemType}, ${optionsType}>> {`);
869
- lines.push(" return new AutoPaginatable(");
870
- lines.push(` await fetchAndDeserialize<${wireInterfaceName(itemType)}, ${itemType}>(`);
871
- lines.push(" this.workos,");
872
- lines.push(` ${pathStr},`);
873
- lines.push(` deserialize${itemType},`);
874
- lines.push(" options,");
875
- lines.push(" ),");
876
- lines.push(" (params) =>");
877
- lines.push(` fetchAndDeserialize<${wireInterfaceName(itemType)}, ${itemType}>(`);
878
- lines.push(" this.workos,");
879
- lines.push(` ${pathStr},`);
880
- lines.push(` deserialize${itemType},`);
881
- lines.push(" params,");
882
- lines.push(" ),");
883
- lines.push(" options,");
884
- lines.push(" );");
885
- lines.push(" }");
886
- }
887
- function renderDeleteMethod(lines, op, plan, method, pathStr) {
888
- const params = buildPathParams(op);
889
- lines.push(` async ${method}(${params}): Promise<void> {`);
890
- lines.push(` await this.workos.delete(${pathStr});`);
891
- lines.push(" }");
892
- }
893
- function renderBodyMethod(lines, op, plan, method, responseModel, pathStr, ctx) {
894
- const requestBodyModel = extractRequestBodyModelName(op);
895
- const requestType = requestBodyModel ? resolveInterfaceName(requestBodyModel, ctx) : "any";
896
- const paramParts = [];
897
- for (const param of op.pathParams) {
898
- paramParts.push(`${fieldName(param.name)}: ${mapTypeRef(param.type)}`);
899
- }
900
- paramParts.push(`payload: ${requestType}`);
901
- if (plan.isIdempotentPost) {
902
- paramParts.push("requestOptions: PostOptions = {}");
903
- }
904
- const paramsStr = paramParts.join(", ");
905
- const bodyExpr = requestBodyModel && requestType !== "any" ? `serialize${requestType}(payload)` : "payload";
906
- lines.push(` async ${method}(${paramsStr}): Promise<${responseModel}> {`);
907
- if (plan.isIdempotentPost) {
908
- lines.push(` const { data } = await this.workos.${op.httpMethod}<${wireInterfaceName(responseModel)}>(`);
909
- lines.push(` ${pathStr},`);
910
- lines.push(` ${bodyExpr},`);
911
- lines.push(" requestOptions,");
912
- lines.push(" );");
913
- } else {
914
- lines.push(` const { data } = await this.workos.${op.httpMethod}<${wireInterfaceName(responseModel)}>(`);
915
- lines.push(` ${pathStr},`);
916
- lines.push(` ${bodyExpr},`);
917
- lines.push(" );");
918
- }
919
- lines.push(` return deserialize${responseModel}(data);`);
920
- lines.push(" }");
921
- }
922
- function renderGetMethod(lines, op, plan, method, responseModel, pathStr) {
923
- const params = buildPathParams(op);
924
- const hasQuery = op.queryParams.length > 0 && !plan.isPaginated;
925
- const allParams = hasQuery ? params ? `${params}, options?: Record<string, any>` : "options?: Record<string, any>" : params;
926
- lines.push(` async ${method}(${allParams}): Promise<${responseModel}> {`);
927
- if (hasQuery) {
928
- lines.push(
929
- ` const { data } = await this.workos.${op.httpMethod}<${wireInterfaceName(responseModel)}>(${pathStr}, {`
930
- );
931
- lines.push(" query: options,");
932
- lines.push(" });");
933
- } else {
934
- lines.push(
935
- ` const { data } = await this.workos.${op.httpMethod}<${wireInterfaceName(responseModel)}>(${pathStr});`
936
- );
937
- }
938
- lines.push(` return deserialize${responseModel}(data);`);
939
- lines.push(" }");
940
- }
941
- function renderVoidMethod(lines, op, plan, method, pathStr) {
942
- const params = buildPathParams(op);
943
- const allParams = plan.hasBody ? params ? `${params}, payload: any` : "payload: any" : params;
944
- lines.push(` async ${method}(${allParams}): Promise<void> {`);
945
- if (plan.hasBody) {
946
- lines.push(` await this.workos.${op.httpMethod}(${pathStr}, payload);`);
947
- } else {
948
- lines.push(` await this.workos.${op.httpMethod}(${pathStr});`);
949
- }
950
- lines.push(" }");
951
- }
952
- function buildPathStr(op) {
953
- const interpolated = op.path.replace(/\{(\w+)\}/g, (_, p) => `\${${fieldName(p)}}`);
954
- return interpolated.includes("${") ? `\`${interpolated}\`` : `'${op.path}'`;
955
- }
956
- function buildPathParams(op) {
957
- return op.pathParams.map((p) => `${fieldName(p.name)}: ${mapTypeRef(p.type)}`).join(", ");
958
- }
959
- function extractRequestBodyModelName(op) {
960
- if (!op.requestBody) return null;
961
- if (op.requestBody.kind === "model") return op.requestBody.name;
962
- return null;
963
- }
964
- var STATUS_TO_EXCEPTION = {
965
- 400: "BadRequestException",
966
- 401: "UnauthorizedException",
967
- 404: "NotFoundException",
968
- 409: "ConflictException",
969
- 422: "UnprocessableEntityException",
970
- 429: "RateLimitExceededException"
971
- };
972
- function statusToExceptionName(statusCode) {
973
- if (STATUS_TO_EXCEPTION[statusCode]) return STATUS_TO_EXCEPTION[statusCode];
974
- if (statusCode >= 500) return "GenericServerException";
975
- return null;
976
- }
977
-
978
- // src/node/client.ts
979
- function generateClient(spec, ctx) {
980
- const files = [];
981
- files.push(generateWorkOSClient(spec, ctx));
982
- files.push(generateBarrel(spec, ctx));
983
- files.push(generatePackageJson(ctx));
984
- files.push(generateTsConfig());
985
- return files;
986
- }
987
- function generateWorkOSClient(spec, ctx) {
988
- const lines = [];
989
- for (const service of spec.services) {
990
- const resolvedName = resolveServiceName(service, ctx);
991
- const serviceDir = serviceDirName(resolvedName);
992
- lines.push(`import { ${resolvedName} } from './${serviceDir}/${fileName(resolvedName)}';`);
993
- }
994
- lines.push("import type { WorkOSOptions } from './common/interfaces/workos-options.interface';");
995
- lines.push("import type { PostOptions } from './common/interfaces/post-options.interface';");
996
- lines.push("import type { GetOptions } from './common/interfaces/get-options.interface';");
997
- lines.push("import { NoApiKeyProvidedException } from './common/exceptions/no-api-key-provided.exception';");
998
- lines.push("import { UnauthorizedException } from './common/exceptions/unauthorized.exception';");
999
- lines.push("import { NotFoundException } from './common/exceptions/not-found.exception';");
1000
- lines.push("import { ConflictException } from './common/exceptions/conflict.exception';");
1001
- lines.push("import { UnprocessableEntityException } from './common/exceptions/unprocessable-entity.exception';");
1002
- lines.push("import { RateLimitExceededException } from './common/exceptions/rate-limit-exceeded.exception';");
1003
- lines.push("import { GenericServerException } from './common/exceptions/generic-server.exception';");
1004
- lines.push("import { BadRequestException } from './common/exceptions/bad-request.exception';");
1005
- lines.push("");
1006
- lines.push("export class WorkOS {");
1007
- lines.push(" readonly baseURL: string;");
1008
- lines.push(" readonly key: string;");
1009
- lines.push(" private readonly options: WorkOSOptions;");
1010
- lines.push("");
1011
- for (const service of spec.services) {
1012
- const resolvedName = resolveServiceName(service, ctx);
1013
- const propName = servicePropertyName(resolvedName);
1014
- lines.push(` readonly ${propName} = new ${resolvedName}(this);`);
1015
- }
1016
- lines.push("");
1017
- lines.push(" constructor(keyOrOptions?: string | WorkOSOptions, maybeOptions?: WorkOSOptions) {");
1018
- lines.push(" if (typeof keyOrOptions === 'object') {");
1019
- lines.push(" this.key = keyOrOptions.apiKey ?? '';");
1020
- lines.push(" this.options = keyOrOptions;");
1021
- lines.push(" } else {");
1022
- lines.push(" this.key = keyOrOptions ?? '';");
1023
- lines.push(" this.options = maybeOptions ?? {};");
1024
- lines.push(" }");
1025
- lines.push("");
1026
- lines.push(" if (!this.key) {");
1027
- lines.push(" const envKey = typeof process !== 'undefined' ? process.env?.WORKOS_API_KEY : undefined;");
1028
- lines.push(" if (envKey) this.key = envKey;");
1029
- lines.push(" }");
1030
- lines.push("");
1031
- lines.push(" const protocol = this.options.https === false ? 'http' : 'https';");
1032
- lines.push(" const hostname = this.options.apiHostname ?? 'api.workos.com';");
1033
- lines.push(" const port = this.options.port ? `:${this.options.port}` : '';");
1034
- lines.push(" this.baseURL = `${protocol}://${hostname}${port}`;");
1035
- lines.push(" }");
1036
- lines.push("");
1037
- lines.push(" async get<Result = any>(path: string, options: GetOptions = {}): Promise<{ data: Result }> {");
1038
- lines.push(" this.ensureApiKey(options);");
1039
- lines.push(" const url = this.buildUrl(path, options.query);");
1040
- lines.push(" const response = await fetch(url, {");
1041
- lines.push(" method: 'GET',");
1042
- lines.push(" headers: this.buildHeaders(options),");
1043
- lines.push(" });");
1044
- lines.push(" await this.handleHttpError(response, path);");
1045
- lines.push(" const data = await response.json() as Result;");
1046
- lines.push(" return { data };");
1047
- lines.push(" }");
1048
- lines.push("");
1049
- lines.push(
1050
- " async post<Result = any, Entity = any>(path: string, entity: Entity, options: PostOptions = {}): Promise<{ data: Result }> {"
1051
- );
1052
- lines.push(" this.ensureApiKey(options);");
1053
- lines.push(" const url = this.buildUrl(path, options.query);");
1054
- lines.push(" const response = await fetch(url, {");
1055
- lines.push(" method: 'POST',");
1056
- lines.push(" headers: this.buildHeaders(options),");
1057
- lines.push(" body: JSON.stringify(entity),");
1058
- lines.push(" });");
1059
- lines.push(" await this.handleHttpError(response, path);");
1060
- lines.push(" const data = await response.json() as Result;");
1061
- lines.push(" return { data };");
1062
- lines.push(" }");
1063
- lines.push("");
1064
- lines.push(
1065
- " async put<Result = any, Entity = any>(path: string, entity: Entity, options: PostOptions = {}): Promise<{ data: Result }> {"
1066
- );
1067
- lines.push(" this.ensureApiKey(options);");
1068
- lines.push(" const url = this.buildUrl(path, options.query);");
1069
- lines.push(" const response = await fetch(url, {");
1070
- lines.push(" method: 'PUT',");
1071
- lines.push(" headers: this.buildHeaders(options),");
1072
- lines.push(" body: JSON.stringify(entity),");
1073
- lines.push(" });");
1074
- lines.push(" await this.handleHttpError(response, path);");
1075
- lines.push(" const data = await response.json() as Result;");
1076
- lines.push(" return { data };");
1077
- lines.push(" }");
1078
- lines.push("");
1079
- lines.push(
1080
- " async patch<Result = any, Entity = any>(path: string, entity: Entity, options: PostOptions = {}): Promise<{ data: Result }> {"
1081
- );
1082
- lines.push(" this.ensureApiKey(options);");
1083
- lines.push(" const url = this.buildUrl(path, options.query);");
1084
- lines.push(" const response = await fetch(url, {");
1085
- lines.push(" method: 'PATCH',");
1086
- lines.push(" headers: this.buildHeaders(options),");
1087
- lines.push(" body: JSON.stringify(entity),");
1088
- lines.push(" });");
1089
- lines.push(" await this.handleHttpError(response, path);");
1090
- lines.push(" const data = await response.json() as Result;");
1091
- lines.push(" return { data };");
1092
- lines.push(" }");
1093
- lines.push("");
1094
- lines.push(" async delete(path: string, options: GetOptions = {}): Promise<void> {");
1095
- lines.push(" this.ensureApiKey(options);");
1096
- lines.push(" const url = this.buildUrl(path);");
1097
- lines.push(" const response = await fetch(url, {");
1098
- lines.push(" method: 'DELETE',");
1099
- lines.push(" headers: this.buildHeaders(options),");
1100
- lines.push(" });");
1101
- lines.push(" await this.handleHttpError(response, path);");
1102
- lines.push(" }");
1103
- lines.push("");
1104
- lines.push(" private buildUrl(path: string, query?: Record<string, any>): string {");
1105
- lines.push(" const url = new URL(path, this.baseURL);");
1106
- lines.push(" if (query) {");
1107
- lines.push(" for (const [key, value] of Object.entries(query)) {");
1108
- lines.push(" if (value !== null && value !== undefined && value !== '') {");
1109
- lines.push(" url.searchParams.set(key, String(value));");
1110
- lines.push(" }");
1111
- lines.push(" }");
1112
- lines.push(" }");
1113
- lines.push(" return url.toString();");
1114
- lines.push(" }");
1115
- lines.push("");
1116
- lines.push(" private buildHeaders(options: any = {}): Record<string, string> {");
1117
- lines.push(" const headers: Record<string, string> = {");
1118
- lines.push(" 'Content-Type': 'application/json',");
1119
- lines.push(` Authorization: \`Bearer \${this.key}\`,`);
1120
- lines.push(" };");
1121
- lines.push(" if (options.idempotencyKey) headers['Idempotency-Key'] = options.idempotencyKey;");
1122
- lines.push(" if (options.warrantToken) headers['Warrant-Token'] = options.warrantToken;");
1123
- lines.push(" return headers;");
1124
- lines.push(" }");
1125
- lines.push("");
1126
- lines.push(" private ensureApiKey(options: any = {}): void {");
1127
- lines.push(" if (!this.key && !options.skipApiKeyCheck) {");
1128
- lines.push(" throw new NoApiKeyProvidedException();");
1129
- lines.push(" }");
1130
- lines.push(" }");
1131
- lines.push("");
1132
- lines.push(" private async handleHttpError(response: Response, path: string): Promise<void> {");
1133
- lines.push(" if (response.ok) return;");
1134
- lines.push("");
1135
- lines.push(" const requestID = response.headers.get('x-request-id') ?? '';");
1136
- lines.push(" let data: any = {};");
1137
- lines.push(" try { data = await response.json(); } catch {}");
1138
- lines.push(" const { message, code, errors } = data;");
1139
- lines.push("");
1140
- lines.push(" switch (response.status) {");
1141
- lines.push(" case 400: throw new BadRequestException({ code, message, requestID });");
1142
- lines.push(" case 401: throw new UnauthorizedException(requestID);");
1143
- lines.push(" case 404: throw new NotFoundException({ code, message, path, requestID });");
1144
- lines.push(" case 409: throw new ConflictException({ message, requestID });");
1145
- lines.push(" case 422: throw new UnprocessableEntityException({ code, errors, message, requestID });");
1146
- lines.push(" case 429: {");
1147
- lines.push(" const retryAfter = Number(response.headers.get('retry-after')) || undefined;");
1148
- lines.push(" throw new RateLimitExceededException(message ?? 'Too many requests', requestID, retryAfter);");
1149
- lines.push(" }");
1150
- lines.push(" default: throw new GenericServerException(response.status, message ?? 'Server error', requestID);");
1151
- lines.push(" }");
1152
- lines.push(" }");
1153
- lines.push("}");
1154
- return { path: "src/workos.ts", content: lines.join("\n"), skipIfExists: true, integrateTarget: false };
1155
- }
1156
- var RESERVED_BARREL_NAMES = /* @__PURE__ */ new Set(["List", "ListResponse", "ListMetadata", "AutoPaginatable", "PaginationOptions"]);
1157
- function generateBarrel(spec, ctx) {
1158
- const lines = [];
1159
- const modelToService = assignModelsToServices(spec.models, spec.services);
1160
- const serviceNameMap = buildServiceNameMap(spec.services, ctx);
1161
- const resolveDir = (irService) => irService ? serviceDirName(serviceNameMap.get(irService) ?? irService) : "common";
1162
- lines.push("export * from './common/exceptions';");
1163
- lines.push("export { AutoPaginatable } from './common/utils/pagination';");
1164
- lines.push("export type { List, ListMetadata, ListResponse } from './common/utils/pagination';");
1165
- lines.push("export type { PaginationOptions } from './common/interfaces/pagination-options.interface';");
1166
- lines.push("export type { WorkOSOptions } from './common/interfaces/workos-options.interface';");
1167
- lines.push("export type { PostOptions } from './common/interfaces/post-options.interface';");
1168
- lines.push("export type { GetOptions } from './common/interfaces/get-options.interface';");
1169
- lines.push("");
1170
- for (const service of spec.services) {
1171
- const resolvedName = resolveServiceName(service, ctx);
1172
- const serviceDir = serviceDirName(resolvedName);
1173
- const serviceModels = spec.models.filter((m) => modelToService.get(m.name) === service.name);
1174
- for (const model of serviceModels) {
1175
- const name = resolveInterfaceName(model.name, ctx);
1176
- const wireName = wireInterfaceName(name);
1177
- if (RESERVED_BARREL_NAMES.has(name) || RESERVED_BARREL_NAMES.has(wireName)) continue;
1178
- lines.push(
1179
- `export type { ${name}, ${wireName} } from './${serviceDir}/interfaces/${fileName(model.name)}.interface';`
1180
- );
1181
- }
1182
- lines.push(`export { ${resolvedName} } from './${serviceDir}/${fileName(resolvedName)}';`);
1183
- lines.push("");
1184
- }
1185
- const unassignedModels = spec.models.filter((m) => !modelToService.has(m.name));
1186
- for (const model of unassignedModels) {
1187
- const name = resolveInterfaceName(model.name, ctx);
1188
- const wireName = wireInterfaceName(name);
1189
- if (RESERVED_BARREL_NAMES.has(name) || RESERVED_BARREL_NAMES.has(wireName)) continue;
1190
- lines.push(`export type { ${name}, ${wireName} } from './common/interfaces/${fileName(model.name)}.interface';`);
1191
- }
1192
- for (const enumDef of spec.enums) {
1193
- const enumService = findEnumService(enumDef.name, spec.services);
1194
- const dir = resolveDir(enumService);
1195
- lines.push(`export type { ${enumDef.name} } from './${dir}/interfaces/${fileName(enumDef.name)}.interface';`);
1196
- }
1197
- lines.push("");
1198
- lines.push("export { WorkOS } from './workos';");
1199
- return { path: "src/index.ts", content: lines.join("\n"), skipIfExists: true };
1200
- }
1201
- function findEnumService(enumName, services) {
1202
- for (const service of services) {
1203
- for (const op of service.operations) {
1204
- const refs = [];
1205
- const collect = (ref) => {
1206
- if (ref?.kind === "enum" && ref.name === enumName) refs.push(ref.name);
1207
- if (ref?.items) collect(ref.items);
1208
- if (ref?.inner) collect(ref.inner);
1209
- if (ref?.variants) ref.variants.forEach(collect);
1210
- if (ref?.valueType) collect(ref.valueType);
1211
- };
1212
- if (op.requestBody) collect(op.requestBody);
1213
- collect(op.response);
1214
- for (const p of [...op.pathParams, ...op.queryParams]) {
1215
- collect(p.type);
1216
- }
1217
- if (refs.length > 0) return service.name;
1218
- }
1219
- }
1220
- return void 0;
1221
- }
1222
- function generatePackageJson(ctx) {
1223
- const pkg = {
1224
- name: `@${ctx.namespace}/sdk`,
1225
- version: "0.0.0",
1226
- type: "module",
1227
- main: "src/index.ts",
1228
- types: "src/index.ts",
1229
- exports: {
1230
- ".": "./src/index.ts"
1231
- },
1232
- scripts: {
1233
- test: "jest",
1234
- build: "tsc"
1235
- },
1236
- devDependencies: {
1237
- typescript: "^5.0.0",
1238
- jest: "^29.0.0",
1239
- "jest-fetch-mock": "^3.0.0",
1240
- "@types/jest": "^29.0.0",
1241
- "ts-jest": "^29.0.0"
1242
- }
1243
- };
1244
- return {
1245
- path: "package.json",
1246
- content: JSON.stringify(pkg, null, 2),
1247
- skipIfExists: true,
1248
- integrateTarget: false
1249
- };
1250
- }
1251
- function generateTsConfig() {
1252
- const config = {
1253
- compilerOptions: {
1254
- target: "ES2020",
1255
- module: "CommonJS",
1256
- lib: ["ES2020"],
1257
- declaration: true,
1258
- strict: true,
1259
- esModuleInterop: true,
1260
- skipLibCheck: true,
1261
- forceConsistentCasingInFileNames: true,
1262
- resolveJsonModule: true,
1263
- outDir: "./lib",
1264
- rootDir: "./src"
1265
- },
1266
- include: ["src/**/*"],
1267
- exclude: ["node_modules", "lib", "**/*.spec.ts"]
1268
- };
1269
- return {
1270
- path: "tsconfig.json",
1271
- content: JSON.stringify(config, null, 2),
1272
- skipIfExists: true,
1273
- integrateTarget: false
1274
- };
1275
- }
1276
-
1277
- // src/node/errors.ts
1278
- function generateErrors() {
1279
- return [
1280
- {
1281
- path: "src/common/exceptions/bad-request.exception.ts",
1282
- content: `export class BadRequestException extends Error {
1283
- readonly status = 400;
1284
- readonly name = 'BadRequestException';
1285
- readonly requestID: string;
1286
- readonly code?: string;
1287
-
1288
- constructor({
1289
- code,
1290
- message,
1291
- requestID,
1292
- }: {
1293
- code?: string;
1294
- message?: string;
1295
- requestID: string;
1296
- }) {
1297
- super();
1298
- this.message = message ?? 'Bad request';
1299
- this.requestID = requestID;
1300
- if (code) this.code = code;
1301
- }
1302
- }`,
1303
- skipIfExists: true,
1304
- integrateTarget: false
1305
- },
1306
- {
1307
- path: "src/common/exceptions/unauthorized.exception.ts",
1308
- content: `export class UnauthorizedException extends Error {
1309
- readonly status = 401;
1310
- readonly name = 'UnauthorizedException';
1311
- readonly requestID: string;
1312
-
1313
- constructor(requestID: string) {
1314
- super();
1315
- this.message = 'Unauthorized';
1316
- this.requestID = requestID;
1317
- }
1318
- }`,
1319
- skipIfExists: true,
1320
- integrateTarget: false
1321
- },
1322
- {
1323
- path: "src/common/exceptions/not-found.exception.ts",
1324
- content: `export class NotFoundException extends Error {
1325
- readonly status = 404;
1326
- readonly name = 'NotFoundException';
1327
- readonly requestID: string;
1328
- readonly code?: string;
1329
-
1330
- constructor({
1331
- code,
1332
- message,
1333
- path,
1334
- requestID,
1335
- }: {
1336
- code?: string;
1337
- message?: string;
1338
- path: string;
1339
- requestID: string;
1340
- }) {
1341
- super();
1342
- this.message =
1343
- message ?? \`The requested path '\${path}' could not be found.\`;
1344
- this.requestID = requestID;
1345
- if (code) this.code = code;
1346
- }
1347
- }`,
1348
- skipIfExists: true,
1349
- integrateTarget: false
1350
- },
1351
- {
1352
- path: "src/common/exceptions/conflict.exception.ts",
1353
- content: `export class ConflictException extends Error {
1354
- readonly status = 409;
1355
- readonly name = 'ConflictException';
1356
- readonly requestID: string;
1357
-
1358
- constructor({
1359
- message,
1360
- requestID,
1361
- }: {
1362
- message?: string;
1363
- error?: string;
1364
- requestID: string;
1365
- }) {
1366
- super();
1367
- this.message = message ?? 'Conflict';
1368
- this.requestID = requestID;
1369
- }
1370
- }`,
1371
- skipIfExists: true,
1372
- integrateTarget: false
1373
- },
1374
- {
1375
- path: "src/common/exceptions/unprocessable-entity.exception.ts",
1376
- content: `export interface UnprocessableEntityError {
1377
- code: string;
1378
- }
1379
-
1380
- export class UnprocessableEntityException extends Error {
1381
- readonly status = 422;
1382
- readonly name = 'UnprocessableEntityException';
1383
- readonly requestID: string;
1384
- readonly code?: string;
1385
-
1386
- constructor({
1387
- code,
1388
- errors,
1389
- message,
1390
- requestID,
1391
- }: {
1392
- code?: string;
1393
- errors?: UnprocessableEntityError[];
1394
- message?: string;
1395
- requestID: string;
1396
- }) {
1397
- super();
1398
- this.requestID = requestID;
1399
- this.message = message ?? 'Unprocessable entity';
1400
- if (code) this.code = code;
1401
- if (errors) {
1402
- const requirement =
1403
- errors.length === 1 ? 'requirement' : 'requirements';
1404
- this.message = \`The following \${requirement} must be met:\\n\`;
1405
- for (const { code: errCode } of errors) {
1406
- this.message = this.message.concat(\`\\t\${errCode}\\n\`);
1407
- }
1408
- }
1409
- }
1410
- }`,
1411
- skipIfExists: true,
1412
- integrateTarget: false
1413
- },
1414
- {
1415
- path: "src/common/exceptions/rate-limit-exceeded.exception.ts",
1416
- content: `export class RateLimitExceededException extends Error {
1417
- readonly status = 429;
1418
- readonly name = 'RateLimitExceededException';
1419
- readonly requestID: string;
1420
- readonly retryAfter?: number;
1421
-
1422
- constructor(message: string, requestID: string, retryAfter?: number) {
1423
- super();
1424
- this.message = message ?? 'Too many requests';
1425
- this.requestID = requestID;
1426
- this.retryAfter = retryAfter;
1427
- }
1428
- }`,
1429
- skipIfExists: true,
1430
- integrateTarget: false
1431
- },
1432
- {
1433
- path: "src/common/exceptions/generic-server.exception.ts",
1434
- content: `export class GenericServerException extends Error {
1435
- readonly status: number;
1436
- readonly name = 'GenericServerException';
1437
- readonly requestID: string;
1438
-
1439
- constructor(status: number, message: string, requestID: string) {
1440
- super();
1441
- this.status = status;
1442
- this.message = message ?? 'Server error';
1443
- this.requestID = requestID;
1444
- }
1445
- }`,
1446
- skipIfExists: true,
1447
- integrateTarget: false
1448
- },
1449
- {
1450
- path: "src/common/exceptions/no-api-key-provided.exception.ts",
1451
- content: `export class NoApiKeyProvidedException extends Error {
1452
- readonly name = 'NoApiKeyProvidedException';
1453
-
1454
- constructor() {
1455
- super();
1456
- this.message =
1457
- 'No API key provided. Pass it to the WorkOS constructor or set the WORKOS_API_KEY environment variable.';
1458
- }
1459
- }`,
1460
- skipIfExists: true,
1461
- integrateTarget: false
1462
- },
1463
- {
1464
- path: "src/common/exceptions/index.ts",
1465
- content: `export { BadRequestException } from './bad-request.exception';
1466
- export { UnauthorizedException } from './unauthorized.exception';
1467
- export { NotFoundException } from './not-found.exception';
1468
- export { ConflictException } from './conflict.exception';
1469
- export {
1470
- UnprocessableEntityException,
1471
- type UnprocessableEntityError,
1472
- } from './unprocessable-entity.exception';
1473
- export { RateLimitExceededException } from './rate-limit-exceeded.exception';
1474
- export { GenericServerException } from './generic-server.exception';
1475
- export { NoApiKeyProvidedException } from './no-api-key-provided.exception';`,
1476
- skipIfExists: true,
1477
- integrateTarget: false
1478
- }
1479
- ];
1480
- }
1481
-
1482
- // src/node/config.ts
1483
- function generateConfig() {
1484
- return [
1485
- {
1486
- path: "src/common/interfaces/workos-options.interface.ts",
1487
- content: `export interface WorkOSOptions {
1488
- apiKey?: string;
1489
- apiHostname?: string;
1490
- https?: boolean;
1491
- port?: number;
1492
- config?: RequestInit;
1493
- fetchFn?: typeof fetch;
1494
- clientId?: string;
1495
- timeout?: number;
1496
- }
1497
-
1498
- export interface AppInfo {
1499
- name: string;
1500
- version?: string;
1501
- }`,
1502
- skipIfExists: true,
1503
- integrateTarget: false
1504
- },
1505
- {
1506
- path: "src/common/interfaces/post-options.interface.ts",
1507
- content: `export interface PostOptions {
1508
- query?: Record<string, any>;
1509
- idempotencyKey?: string;
1510
- warrantToken?: string;
1511
- skipApiKeyCheck?: boolean;
1512
- }`,
1513
- skipIfExists: true,
1514
- integrateTarget: false
1515
- },
1516
- {
1517
- path: "src/common/interfaces/get-options.interface.ts",
1518
- content: `export interface GetOptions {
1519
- query?: Record<string, any>;
1520
- accessToken?: string;
1521
- warrantToken?: string;
1522
- skipApiKeyCheck?: boolean;
1523
- }`,
1524
- skipIfExists: true,
1525
- integrateTarget: false
1526
- },
1527
- {
1528
- path: "src/common/interfaces/pagination-options.interface.ts",
1529
- content: `export interface PaginationOptions {
1530
- limit?: number;
1531
- before?: string | null;
1532
- after?: string | null;
1533
- order?: 'asc' | 'desc';
1534
- }`,
1535
- skipIfExists: true,
1536
- integrateTarget: false
1537
- },
1538
- {
1539
- path: "src/common/interfaces/request-exception.interface.ts",
1540
- content: `export interface RequestException {
1541
- readonly status: number;
1542
- readonly name: string;
1543
- readonly requestID: string;
1544
- readonly code?: string;
1545
- }`,
1546
- skipIfExists: true,
1547
- integrateTarget: false
1548
- }
1549
- ];
1550
- }
1551
-
1552
- // src/node/common.ts
1553
- function generateCommon() {
1554
- return [
1555
- {
1556
- path: "src/common/utils/pagination.ts",
1557
- content: paginationContent(),
1558
- skipIfExists: true,
1559
- integrateTarget: false
1560
- },
1561
- {
1562
- path: "src/common/utils/fetch-and-deserialize.ts",
1563
- content: fetchAndDeserializeContent(),
1564
- skipIfExists: true,
1565
- integrateTarget: false
1566
- },
1567
- {
1568
- path: "src/common/serializers/list.serializer.ts",
1569
- content: listSerializerContent(),
1570
- skipIfExists: true,
1571
- integrateTarget: false
1572
- },
1573
- {
1574
- path: "src/common/utils/test-utils.ts",
1575
- content: testUtilsContent(),
1576
- skipIfExists: true,
1577
- integrateTarget: false
1578
- }
1579
- ];
1580
- }
1581
- function paginationContent() {
1582
- return `import type { PaginationOptions } from '../interfaces/pagination-options.interface';
1583
-
1584
- export interface ListMetadata {
1585
- before: string | null;
1586
- after: string | null;
1587
- }
1588
-
1589
- export interface List<T> {
1590
- object: 'list';
1591
- data: T[];
1592
- listMetadata: ListMetadata;
1593
- }
1594
-
1595
- export interface ListResponse<T> {
1596
- object: 'list';
1597
- data: T[];
1598
- list_metadata: {
1599
- before: string | null;
1600
- after: string | null;
1601
- };
1602
- }
1603
-
1604
- export class AutoPaginatable<
1605
- ResourceType,
1606
- ParametersType extends PaginationOptions = PaginationOptions,
1607
- > {
1608
- readonly object = 'list' as const;
1609
- readonly options: ParametersType;
1610
-
1611
- constructor(
1612
- protected list: List<ResourceType>,
1613
- private apiCall: (params: PaginationOptions) => Promise<List<ResourceType>>,
1614
- options?: ParametersType,
1615
- ) {
1616
- this.options = options ?? ({} as ParametersType);
1617
- }
1618
-
1619
- get data(): ResourceType[] {
1620
- return this.list.data;
1621
- }
1622
-
1623
- get listMetadata() {
1624
- return this.list.listMetadata;
1625
- }
1626
-
1627
- private async *generatePages(
1628
- params: PaginationOptions,
1629
- ): AsyncGenerator<ResourceType[]> {
1630
- const result = await this.apiCall({
1631
- ...this.options,
1632
- limit: 100,
1633
- after: params.after,
1634
- });
1635
- yield result.data;
1636
- if (result.listMetadata.after) {
1637
- await new Promise((resolve) => setTimeout(resolve, 350));
1638
- yield* this.generatePages({ after: result.listMetadata.after });
1639
- }
1640
- }
1641
-
1642
- async autoPagination(): Promise<ResourceType[]> {
1643
- if (this.options.limit) {
1644
- return this.data;
1645
- }
1646
- const results: ResourceType[] = [];
1647
- for await (const page of this.generatePages({
1648
- after: this.options.after,
1649
- })) {
1650
- results.push(...page);
1651
- }
1652
- return results;
1653
- }
1654
- }`;
1655
- }
1656
- function fetchAndDeserializeContent() {
1657
- return `import type { WorkOS } from '../../workos';
1658
- import type { PaginationOptions } from '../interfaces/pagination-options.interface';
1659
- import type { List, ListResponse } from './pagination';
1660
-
1661
- function setDefaultOptions(
1662
- options?: PaginationOptions,
1663
- ): Record<string, any> {
1664
- return {
1665
- order: 'desc',
1666
- ...options,
1667
- };
1668
- }
1669
-
1670
- function deserializeList<T, U>(
1671
- data: ListResponse<T>,
1672
- deserializeFn: (item: T) => U,
1673
- ): List<U> {
1674
- return {
1675
- data: data.data.map(deserializeFn),
1676
- listMetadata: {
1677
- before: data.list_metadata.before,
1678
- after: data.list_metadata.after,
1679
- },
1680
- };
1681
- }
1682
-
1683
- export const fetchAndDeserialize = async <T, U>(
1684
- workos: WorkOS,
1685
- endpoint: string,
1686
- deserializeFn: (data: T) => U,
1687
- options?: PaginationOptions,
1688
- ): Promise<List<U>> => {
1689
- const { data } = await workos.get<ListResponse<T>>(endpoint, {
1690
- query: setDefaultOptions(options),
1691
- });
1692
- return deserializeList(data, deserializeFn);
1693
- };`;
1694
- }
1695
- function listSerializerContent() {
1696
- return `import type { ListMetadata, ListResponse } from '../utils/pagination';
1697
-
1698
- export function deserializeListMetadata(
1699
- metadata: ListResponse<any>['list_metadata'],
1700
- ): ListMetadata {
1701
- return {
1702
- before: metadata.before,
1703
- after: metadata.after,
1704
- };
1705
- }`;
1706
- }
1707
- function testUtilsContent() {
1708
- return `import fetch from 'jest-fetch-mock';
1709
-
1710
- interface MockParams {
1711
- status?: number;
1712
- headers?: Record<string, string>;
1713
- [key: string]: any;
1714
- }
1715
-
1716
- export function fetchOnce(
1717
- response: any = {},
1718
- { status = 200, headers, ...rest }: MockParams = {},
1719
- ) {
1720
- return fetch.once(JSON.stringify(response), {
1721
- status,
1722
- headers: {
1723
- 'content-type': 'application/json;charset=UTF-8',
1724
- ...headers,
1725
- },
1726
- ...rest,
1727
- });
1728
- }
1729
-
1730
- export function fetchURL(): string {
1731
- return String(fetch.mock.calls[0][0]);
1732
- }
1733
-
1734
- export function fetchSearchParams(): Record<string, string> {
1735
- return Object.fromEntries(new URL(fetchURL()).searchParams);
1736
- }
1737
-
1738
- export function fetchHeaders(): Record<string, string> {
1739
- const headers = fetch.mock.calls[0][1]?.headers ?? {};
1740
- return headers as Record<string, string>;
1741
- }
1742
-
1743
- export function fetchBody({ raw = false } = {}): any {
1744
- const body = fetch.mock.calls[0][1]?.body;
1745
- if (body instanceof URLSearchParams) return body.toString();
1746
- if (raw) return body;
1747
- return JSON.parse(String(body));
1748
- }`;
1749
- }
1750
-
1751
- // src/node/tests.ts
1752
- import { planOperation as planOperation2, toCamelCase as toCamelCase2 } from "@workos/oagen";
1753
-
1754
- // src/node/fixtures.ts
1755
- function generateFixtures(spec, ctx) {
1756
- if (spec.models.length === 0) return [];
1757
- const modelToService = assignModelsToServices(spec.models, spec.services);
1758
- const serviceNameMap = ctx ? buildServiceNameMap(ctx.spec.services, ctx) : /* @__PURE__ */ new Map();
1759
- const resolveDir = (irService) => irService ? serviceDirName(serviceNameMap.get(irService) ?? irService) : "common";
1760
- const modelMap = new Map(spec.models.map((m) => [m.name, m]));
1761
- const enumMap = new Map(spec.enums.map((e) => [e.name, e]));
1762
- const files = [];
1763
- for (const model of spec.models) {
1764
- const service = modelToService.get(model.name);
1765
- const dirName = resolveDir(service);
1766
- const fixture = generateModelFixture(model, modelMap, enumMap);
1767
- files.push({
1768
- path: `src/${dirName}/fixtures/${fileName(model.name)}.fixture.json`,
1769
- content: JSON.stringify(fixture, null, 2)
1770
- });
1771
- }
1772
- for (const service of spec.services) {
1773
- const resolvedName = ctx ? resolveServiceName(service, ctx) : service.name;
1774
- const serviceDir = serviceDirName(resolvedName);
1775
- for (const op of service.operations) {
1776
- if (op.pagination) {
1777
- const itemModel = op.pagination.itemType.kind === "model" ? modelMap.get(op.pagination.itemType.name) : null;
1778
- if (itemModel) {
1779
- const fixture = generateModelFixture(itemModel, modelMap, enumMap);
1780
- const listFixture = {
1781
- data: [fixture],
1782
- list_metadata: {
1783
- before: null,
1784
- after: null
1785
- }
1786
- };
1787
- files.push({
1788
- path: `src/${serviceDir}/fixtures/list-${fileName(itemModel.name)}.fixture.json`,
1789
- content: JSON.stringify(listFixture, null, 2)
1790
- });
1791
- }
1792
- }
1793
- }
1794
- }
1795
- return files;
1796
- }
1797
- function generateModelFixture(model, modelMap, enumMap) {
1798
- const fixture = {};
1799
- for (const field of model.fields) {
1800
- const wireName = wireFieldName(field.name);
1801
- fixture[wireName] = generateFieldValue(field.type, field.name, modelMap, enumMap);
1802
- }
1803
- return fixture;
1804
- }
1805
- function generateFieldValue(ref, fieldName2, modelMap, enumMap) {
1806
- switch (ref.kind) {
1807
- case "primitive":
1808
- return generatePrimitiveValue(ref.type, ref.format, fieldName2);
1809
- case "literal":
1810
- return ref.value;
1811
- case "enum": {
1812
- const e = enumMap.get(ref.name);
1813
- return e?.values[0]?.value ?? "unknown";
1814
- }
1815
- case "model": {
1816
- const nested = modelMap.get(ref.name);
1817
- if (nested) return generateModelFixture(nested, modelMap, enumMap);
1818
- return {};
1819
- }
1820
- case "array": {
1821
- const item = generateFieldValue(ref.items, fieldName2, modelMap, enumMap);
1822
- return [item];
1823
- }
1824
- case "nullable":
1825
- return generateFieldValue(ref.inner, fieldName2, modelMap, enumMap);
1826
- case "union":
1827
- if (ref.variants.length > 0) {
1828
- return generateFieldValue(ref.variants[0], fieldName2, modelMap, enumMap);
1829
- }
1830
- return null;
1831
- case "map":
1832
- return { key: generateFieldValue(ref.valueType, "value", modelMap, enumMap) };
1833
- }
1834
- }
1835
- function generatePrimitiveValue(type, format, name) {
1836
- switch (type) {
1837
- case "string":
1838
- if (format === "date-time") return "2023-01-01T00:00:00.000Z";
1839
- if (format === "date") return "2023-01-01";
1840
- if (format === "uuid") return "00000000-0000-0000-0000-000000000000";
1841
- if (name.includes("id")) return `${name}_01234`;
1842
- if (name.includes("email")) return "test@example.com";
1843
- if (name.includes("url") || name.includes("uri")) return "https://example.com";
1844
- if (name.includes("name")) return "Test";
1845
- return `test_${name}`;
1846
- case "integer":
1847
- return 1;
1848
- case "number":
1849
- return 1;
1850
- case "boolean":
1851
- return true;
1852
- case "unknown":
1853
- return {};
1854
- default:
1855
- return null;
1856
- }
1857
- }
1858
-
1859
- // src/node/tests.ts
1860
- function generateTests(spec, ctx) {
1861
- const files = [];
1862
- const fixtures = generateFixtures(spec, ctx);
1863
- for (const f of fixtures) {
1864
- files.push({ path: f.path, content: f.content, headerPlacement: "skip" });
1865
- }
1866
- const modelMap = new Map(spec.models.map((m) => [m.name, m]));
1867
- for (const service of spec.services) {
1868
- files.push(generateServiceTest(service, spec, ctx, modelMap));
1869
- }
1870
- return files;
1871
- }
1872
- function generateServiceTest(service, spec, ctx, modelMap) {
1873
- const resolvedName = resolveServiceName(service, ctx);
1874
- const serviceDir = serviceDirName(resolvedName);
1875
- const serviceClass = resolvedName;
1876
- const serviceProp = servicePropertyName(resolvedName);
1877
- const testPath = `src/${serviceDir}/${fileName(resolvedName)}.spec.ts`;
1878
- const lines = [];
1879
- lines.push("import fetch from 'jest-fetch-mock';");
1880
- lines.push("import {");
1881
- lines.push(" fetchOnce,");
1882
- lines.push(" fetchURL,");
1883
- lines.push(" fetchSearchParams,");
1884
- lines.push(" fetchBody,");
1885
- lines.push("} from '../common/utils/test-utils';");
1886
- lines.push("import { WorkOS } from '../workos';");
1887
- lines.push("");
1888
- const fixtureImports = /* @__PURE__ */ new Set();
1889
- for (const op of service.operations) {
1890
- const plan = planOperation2(op);
1891
- if (plan.isPaginated && op.pagination) {
1892
- const itemModelName = op.pagination.itemType.kind === "model" ? op.pagination.itemType.name : null;
1893
- if (itemModelName) {
1894
- fixtureImports.add(
1895
- `import list${itemModelName}Fixture from './fixtures/list-${fileName(itemModelName)}.fixture.json';`
1896
- );
1897
- }
1898
- } else if (plan.responseModelName) {
1899
- fixtureImports.add(
1900
- `import ${toCamelCase2(plan.responseModelName)}Fixture from './fixtures/${fileName(plan.responseModelName)}.fixture.json';`
1901
- );
1902
- }
1903
- }
1904
- for (const imp of fixtureImports) {
1905
- lines.push(imp);
1906
- }
1907
- lines.push("");
1908
- lines.push("const workos = new WorkOS('sk_test_Sz3IQjepeSWaI4cMS4ms4sMuU');");
1909
- lines.push("");
1910
- lines.push(`describe('${serviceClass}', () => {`);
1911
- lines.push(" beforeEach(() => fetch.resetMocks());");
1912
- for (const op of service.operations) {
1913
- const plan = planOperation2(op);
1914
- const method = resolveMethodName(op, service, ctx);
1915
- lines.push("");
1916
- lines.push(` describe('${method}', () => {`);
1917
- if (plan.isPaginated) {
1918
- renderPaginatedTest(lines, op, plan, method, serviceProp, modelMap);
1919
- } else if (plan.isDelete) {
1920
- renderDeleteTest(lines, op, method, serviceProp);
1921
- } else if (plan.hasBody && plan.responseModelName) {
1922
- renderBodyTest(lines, op, plan, method, serviceProp, modelMap);
1923
- } else if (plan.responseModelName) {
1924
- renderGetTest(lines, op, plan, method, serviceProp, modelMap);
1925
- } else {
1926
- renderVoidTest(lines, op, method, serviceProp);
1927
- }
1928
- if (plan.responseModelName || plan.isPaginated) {
1929
- renderErrorTest(lines, op, plan, method, serviceProp);
1930
- }
1931
- lines.push(" });");
1932
- }
1933
- lines.push("});");
1934
- return { path: testPath, content: lines.join("\n"), skipIfExists: true, integrateTarget: false };
1935
- }
1936
- function renderPaginatedTest(lines, op, plan, method, serviceProp, modelMap) {
1937
- const itemModelName = op.pagination?.itemType.kind === "model" ? op.pagination.itemType.name : "Item";
1938
- lines.push(" it('returns paginated results', async () => {");
1939
- lines.push(` fetchOnce(list${itemModelName}Fixture);`);
1940
- lines.push("");
1941
- lines.push(` const { data, listMetadata } = await workos.${serviceProp}.${method}();`);
1942
- lines.push("");
1943
- lines.push(` expect(fetchURL()).toContain('${op.path.split("{")[0]}');`);
1944
- lines.push(" expect(fetchSearchParams()).toHaveProperty('order');");
1945
- lines.push(" expect(Array.isArray(data)).toBe(true);");
1946
- lines.push(" expect(listMetadata).toBeDefined();");
1947
- const itemModel = modelMap.get(itemModelName);
1948
- if (itemModel) {
1949
- const assertions = buildFieldAssertions(itemModel, "data[0]");
1950
- if (assertions.length > 0) {
1951
- lines.push(" expect(data.length).toBeGreaterThan(0);");
1952
- for (const assertion of assertions) {
1953
- lines.push(` ${assertion}`);
1954
- }
1955
- }
1956
- }
1957
- lines.push(" });");
1958
- }
1959
- function renderDeleteTest(lines, op, method, serviceProp) {
1960
- const hasPathParam = op.pathParams.length > 0;
1961
- const args = hasPathParam ? "'test_id'" : "";
1962
- lines.push(" it('sends a DELETE request', async () => {");
1963
- lines.push(" fetchOnce({}, { status: 204 });");
1964
- lines.push("");
1965
- lines.push(` await workos.${serviceProp}.${method}(${args});`);
1966
- lines.push("");
1967
- lines.push(` expect(fetchURL()).toContain('${op.path.split("{")[0]}');`);
1968
- if (hasPathParam) {
1969
- lines.push(" expect(fetchURL()).toContain('test_id');");
1970
- }
1971
- lines.push(" });");
1972
- }
1973
- function renderBodyTest(lines, op, plan, method, serviceProp, modelMap) {
1974
- const responseModelName = plan.responseModelName;
1975
- const fixture = `${toCamelCase2(responseModelName)}Fixture`;
1976
- const hasPathParam = op.pathParams.length > 0;
1977
- const pathArg = hasPathParam ? "'test_id', " : "";
1978
- lines.push(" it('sends the correct request and returns result', async () => {");
1979
- lines.push(` fetchOnce(${fixture});`);
1980
- lines.push("");
1981
- lines.push(` const result = await workos.${serviceProp}.${method}(${pathArg}{});`);
1982
- lines.push("");
1983
- lines.push(` expect(fetchURL()).toContain('${op.path.split("{")[0]}');`);
1984
- if (hasPathParam) {
1985
- lines.push(" expect(fetchURL()).toContain('test_id');");
1986
- }
1987
- lines.push(" expect(fetchBody()).toBeDefined();");
1988
- lines.push(" expect(result).toBeDefined();");
1989
- const responseModel = modelMap.get(responseModelName);
1990
- if (responseModel) {
1991
- const assertions = buildFieldAssertions(responseModel, "result");
1992
- for (const assertion of assertions) {
1993
- lines.push(` ${assertion}`);
1994
- }
1995
- }
1996
- lines.push(" });");
1997
- }
1998
- function renderGetTest(lines, op, plan, method, serviceProp, modelMap) {
1999
- const responseModelName = plan.responseModelName;
2000
- const fixture = `${toCamelCase2(responseModelName)}Fixture`;
2001
- const hasPathParam = op.pathParams.length > 0;
2002
- const args = hasPathParam ? "'test_id'" : "";
2003
- lines.push(" it('returns the expected result', async () => {");
2004
- lines.push(` fetchOnce(${fixture});`);
2005
- lines.push("");
2006
- lines.push(` const result = await workos.${serviceProp}.${method}(${args});`);
2007
- lines.push("");
2008
- lines.push(` expect(fetchURL()).toContain('${op.path.split("{")[0]}');`);
2009
- if (hasPathParam) {
2010
- lines.push(" expect(fetchURL()).toContain('test_id');");
2011
- }
2012
- lines.push(" expect(result).toBeDefined();");
2013
- const responseModel = modelMap.get(responseModelName);
2014
- if (responseModel) {
2015
- const assertions = buildFieldAssertions(responseModel, "result");
2016
- for (const assertion of assertions) {
2017
- lines.push(` ${assertion}`);
2018
- }
2019
- }
2020
- lines.push(" });");
2021
- }
2022
- function renderVoidTest(lines, op, method, serviceProp) {
2023
- const hasPathParam = op.pathParams.length > 0;
2024
- const args = hasPathParam ? "'test_id'" : "";
2025
- lines.push(" it('sends the request', async () => {");
2026
- lines.push(" fetchOnce({});");
2027
- lines.push("");
2028
- lines.push(` await workos.${serviceProp}.${method}(${args});`);
2029
- lines.push("");
2030
- lines.push(` expect(fetchURL()).toContain('${op.path.split("{")[0]}');`);
2031
- if (hasPathParam) {
2032
- lines.push(" expect(fetchURL()).toContain('test_id');");
2033
- }
2034
- lines.push(" });");
2035
- }
2036
- function renderErrorTest(lines, op, plan, method, serviceProp) {
2037
- const hasPathParam = op.pathParams.length > 0;
2038
- const isPaginated = plan.isPaginated;
2039
- const hasBody = plan.hasBody;
2040
- let args;
2041
- if (isPaginated) {
2042
- args = "";
2043
- } else if (hasBody && hasPathParam) {
2044
- args = "'test_id', {}";
2045
- } else if (hasBody) {
2046
- args = "{}";
2047
- } else if (hasPathParam) {
2048
- args = "'test_id'";
2049
- } else {
2050
- args = "";
2051
- }
2052
- lines.push("");
2053
- lines.push(" it('throws on unauthorized', async () => {");
2054
- lines.push(" fetchOnce({ message: 'Unauthorized' }, { status: 401 });");
2055
- lines.push("");
2056
- lines.push(` await expect(workos.${serviceProp}.${method}(${args})).rejects.toThrow();`);
2057
- lines.push(" });");
2058
- }
2059
- function buildFieldAssertions(model, accessor) {
2060
- const assertions = [];
2061
- for (const field of model.fields) {
2062
- if (!field.required) continue;
2063
- const value = fixtureValueForType(field.type, field.name);
2064
- if (value === null) continue;
2065
- const domainField = fieldName(field.name);
2066
- assertions.push(`expect(${accessor}.${domainField}).toBe(${value});`);
2067
- }
2068
- return assertions;
2069
- }
2070
- function fixtureValueForType(ref, name) {
2071
- switch (ref.kind) {
2072
- case "primitive":
2073
- return fixtureValueForPrimitive(ref.type, ref.format, name);
2074
- case "literal":
2075
- return typeof ref.value === "string" ? `'${ref.value}'` : String(ref.value);
2076
- default:
2077
- return null;
2078
- }
2079
- }
2080
- function fixtureValueForPrimitive(type, format, name) {
2081
- switch (type) {
2082
- case "string":
2083
- if (format === "date-time") return "'2023-01-01T00:00:00.000Z'";
2084
- if (format === "date") return "'2023-01-01'";
2085
- if (format === "uuid") return "'00000000-0000-0000-0000-000000000000'";
2086
- if (name.includes("id")) return `'${wireFieldName(name)}_01234'`;
2087
- if (name.includes("email")) return "'test@example.com'";
2088
- if (name.includes("url") || name.includes("uri")) return "'https://example.com'";
2089
- if (name.includes("name")) return "'Test'";
2090
- return `'test_${wireFieldName(name)}'`;
2091
- case "integer":
2092
- return "1";
2093
- case "number":
2094
- return "1";
2095
- case "boolean":
2096
- return "true";
2097
- default:
2098
- return null;
2099
- }
2100
- }
2101
-
2102
- // src/node/manifest.ts
2103
- function generateManifest(spec, ctx) {
2104
- const manifest = {};
2105
- for (const service of spec.services) {
2106
- const propName = servicePropertyName(resolveServiceName(service, ctx));
2107
- for (const op of service.operations) {
2108
- const httpKey = `${op.httpMethod.toUpperCase()} ${op.path}`;
2109
- const method = resolveMethodName(op, service, ctx);
2110
- manifest[httpKey] = { sdkMethod: method, service: propName };
2111
- }
2112
- }
2113
- return [
2114
- {
2115
- path: "smoke-manifest.json",
2116
- content: JSON.stringify(manifest, null, 2),
2117
- integrateTarget: false
2118
- }
2119
- ];
2120
- }
2121
-
2122
- // src/node/index.ts
2123
- var nodeEmitter = {
2124
- language: "node",
2125
- generateModels(models, ctx) {
2126
- return [...generateModels(models, ctx), ...generateSerializers(models, ctx)];
2127
- },
2128
- generateEnums(enums, ctx) {
2129
- return generateEnums(enums, ctx);
2130
- },
2131
- generateResources(services, ctx) {
2132
- return generateResources(services, ctx);
2133
- },
2134
- generateClient(spec, ctx) {
2135
- return generateClient(spec, ctx);
2136
- },
2137
- generateErrors(_ctx) {
2138
- return generateErrors();
2139
- },
2140
- generateConfig(_ctx) {
2141
- return [...generateConfig(), ...generateCommon()];
2142
- },
2143
- generateTypeSignatures(_spec, _ctx) {
2144
- return [];
2145
- },
2146
- generateTests(spec, ctx) {
2147
- return generateTests(spec, ctx);
2148
- },
2149
- generateManifest(spec, ctx) {
2150
- return generateManifest(spec, ctx);
2151
- },
2152
- fileHeader() {
2153
- return "// This file is auto-generated by oagen. Do not edit.";
2154
- }
2155
- };
2156
- export {
2157
- nodeEmitter
2158
- };