@workos/oagen-emitters 0.15.0 → 0.15.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/plugin.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { t as workosEmittersPlugin } from "./plugin-CO4RFgAW.mjs";
1
+ import { t as workosEmittersPlugin } from "./plugin-Xkr83G9A.mjs";
2
2
  export { workosEmittersPlugin };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@workos/oagen-emitters",
3
- "version": "0.15.0",
3
+ "version": "0.15.2",
4
4
  "description": "WorkOS' oagen emitters",
5
5
  "license": "MIT",
6
6
  "author": "WorkOS",
@@ -191,7 +191,14 @@ function generateServiceBarrels(spec: ApiSpec, ctx: EmitterContext): GeneratedFi
191
191
  const ownedDirNames = new Set<string>();
192
192
  for (const service of spec.services) {
193
193
  if (isNodeOwnedService(ctx, service.name)) {
194
- ownedDirNames.add(resolveDir(service.name));
194
+ const dir = resolveDir(service.name);
195
+ ownedDirNames.add(dir);
196
+ // Ensure owned directories always get a barrel entry, even if no
197
+ // model interfaces are generated (hand-written files still need it).
198
+ if (!dirExports.has(dir)) {
199
+ dirExports.set(dir, []);
200
+ if (!dirSymbols.has(dir)) dirSymbols.set(dir, new Set());
201
+ }
195
202
  }
196
203
  }
197
204
 
@@ -466,6 +473,38 @@ function generateServiceBarrels(spec: ApiSpec, ctx: EmitterContext): GeneratedFi
466
473
  }
467
474
  }
468
475
 
476
+ // For owned directories, scan the interfaces directory for hand-written
477
+ // files that still need to be in the barrel (e.g., options interfaces
478
+ // preserved from the baseline).
479
+ const ownedScanRoot = ctx.targetDir ?? ctx.outputDir;
480
+ if (ownedScanRoot && isDirOwned) {
481
+ const interfacesDir = path.join(ownedScanRoot, 'src', dirName, 'interfaces');
482
+ const symbols = dirSymbols.get(dirName) ?? new Set<string>();
483
+ try {
484
+ for (const entry of fs.readdirSync(interfacesDir)) {
485
+ if (entry === 'index.ts') continue;
486
+ if (!entry.endsWith('.ts')) continue;
487
+ const stem = entry.replace(/\.ts$/, '');
488
+ const exportLine = `export * from './${stem}';`;
489
+ if (exportSet.has(exportLine)) continue;
490
+ const content = fs.readFileSync(path.join(interfacesDir, entry), 'utf-8');
491
+ const exportedNames: string[] = [];
492
+ for (const m of content.matchAll(/export\s+(?:interface|type|enum|class|const|function)\s+(\w+)/g)) {
493
+ exportedNames.push(m[1]);
494
+ }
495
+ const hasCollision = exportedNames.some((name) => globalExistingSymbols.has(name));
496
+ if (hasCollision) continue;
497
+ for (const name of exportedNames) {
498
+ symbols.add(name);
499
+ globalExistingSymbols.add(name);
500
+ }
501
+ exportSet.add(exportLine);
502
+ }
503
+ } catch {
504
+ // Directory doesn't exist in target
505
+ }
506
+ }
507
+
469
508
  // Deduplicate and sort
470
509
  const uniqueExports = [...exportSet];
471
510
  uniqueExports.sort();
@@ -456,6 +456,8 @@ function resolveRef(schema: RawSchema, rawSchemas: Record<string, RawSchema>): R
456
456
  export interface DiscriminatedPlan {
457
457
  shape: DiscriminatedShape;
458
458
  modelDir: string;
459
+ /** Maps raw spec schema names to their resolved service directories. */
460
+ depDirMap: Map<string, string>;
459
461
  }
460
462
 
461
463
  export function planDiscriminatedModels(models: Model[], ctx: EmitterContext): Map<string, DiscriminatedPlan> {
@@ -464,11 +466,47 @@ export function planDiscriminatedModels(models: Model[], ctx: EmitterContext): M
464
466
  if (!spec?.components?.schemas) return plans;
465
467
  const rawSchemas = spec.components.schemas as Record<string, RawSchema>;
466
468
  const { modelToService, resolveDir } = createServiceDirResolver(models, ctx.spec.services, ctx);
469
+
470
+ // Build a lookup from IR model names to their resolved service directories.
471
+ const irModelDir = new Map<string, string>();
472
+ for (const model of models) {
473
+ irModelDir.set(model.name, resolveDir(modelToService.get(model.name)));
474
+ }
475
+
476
+ // Map raw spec schema names to service directories so discriminated model
477
+ // imports can point to the correct cross-service path. Raw names may differ
478
+ // from IR names due to schemaNameTransform (e.g. Dto stripping).
479
+ const depDirMap = new Map<string, string>();
480
+ for (const rawName of Object.keys(rawSchemas)) {
481
+ if (irModelDir.has(rawName)) {
482
+ depDirMap.set(rawName, irModelDir.get(rawName)!);
483
+ continue;
484
+ }
485
+ const stripped = rawName.replace(/Dto/g, '').replace(/DTO/g, '').replace(/Json$/, '');
486
+ if (stripped !== rawName && irModelDir.has(stripped)) {
487
+ depDirMap.set(rawName, irModelDir.get(stripped)!);
488
+ }
489
+ }
490
+
467
491
  for (const model of models) {
468
492
  const shape = detectDiscriminatedShape(model.name, rawSchemas);
469
493
  if (!shape) continue;
494
+ // Skip models whose variant field dependencies can't all be resolved to
495
+ // existing interface files. EventSchema, for instance, references models
496
+ // from many services that may not have generated files yet.
497
+ const allDeps = new Set<string>();
498
+ for (const field of shape.baseFields) {
499
+ for (const d of field.modelDeps) allDeps.add(d);
500
+ }
501
+ for (const variant of shape.variants) {
502
+ for (const field of variant.fields) {
503
+ for (const d of field.modelDeps) allDeps.add(d);
504
+ }
505
+ }
506
+ const hasUnresolvableDeps = [...allDeps].some((dep) => !depDirMap.has(dep) && !irModelDir.has(dep));
507
+ if (hasUnresolvableDeps) continue;
470
508
  const modelDir = resolveDir(modelToService.get(model.name));
471
- plans.set(model.name, { shape, modelDir });
509
+ plans.set(model.name, { shape, modelDir, depDirMap });
472
510
  }
473
511
  return plans;
474
512
  }
@@ -526,8 +564,13 @@ function buildInterfaceFile(plan: DiscriminatedPlan, _ctx: EmitterContext): Gene
526
564
  function buildInterfaceBody(name: string, shape: DiscriminatedShape, variant: VariantSpec, isWire: boolean): string[] {
527
565
  const lines: string[] = [];
528
566
  lines.push(`export interface ${name} {`);
529
- // Base fields
567
+ // Variant fields override base fields when both define the same property
568
+ // (variants have narrower types, e.g. `event: 'foo'` vs base `event: string`).
569
+ // The discriminator is also emitted separately as a const literal below.
570
+ const variantFieldNames = new Set(variant.fields.map((f) => f.name));
530
571
  for (const field of shape.baseFields) {
572
+ if (variantFieldNames.has(field.name)) continue;
573
+ if (field.name === shape.discriminatorProperty) continue;
531
574
  pushFieldLine(lines, field, isWire);
532
575
  }
533
576
  // Discriminator (typed as the variant's const value)
@@ -569,31 +612,24 @@ function collectImports(plan: DiscriminatedPlan): ImportSpec[] {
569
612
  for (const d of field.modelDeps) deps.add(d);
570
613
  }
571
614
  }
572
- // Group by directory — all deps under the same modelDir get one import.
573
- // We assume all deps live in the same service for now (same dir as this
574
- // model). Cross-service imports would need ctx.spec.services lookups; the
575
- // current discriminated-shape cases (ConnectApplication) are all
576
- // intra-service.
577
- const symbols: string[] = [];
615
+ const result: ImportSpec[] = [];
578
616
  for (const dep of [...deps].sort()) {
579
617
  const domain = toPascalCase(dep);
580
- symbols.push(domain);
581
618
  const wire = wireInterfaceName(domain);
582
- if (wire !== domain) symbols.push(wire);
583
- }
584
- if (symbols.length === 0) return [];
585
- // Single import block from sibling files in the same interfaces directory.
586
- return symbols
587
- .map((sym) => {
588
- const fname = fileName(toSnakeFromPascal(sym.replace(/Response$/, '')));
589
- return { path: `./${fname}.interface`, symbols: [sym] };
590
- })
591
- .reduce((acc, cur) => {
592
- const existing = acc.find((a) => a.path === cur.path);
593
- if (existing) existing.symbols.push(...cur.symbols);
594
- else acc.push(cur);
595
- return acc;
596
- }, [] as ImportSpec[]);
619
+ const symbols = wire !== domain ? [domain, wire] : [domain];
620
+ const depDir = plan.depDirMap.get(dep);
621
+ const baseName = fileName(toSnakeFromPascal(domain));
622
+ let importPath: string;
623
+ if (!depDir || depDir === plan.modelDir) {
624
+ importPath = `./${baseName}.interface`;
625
+ } else {
626
+ importPath = `../../${depDir}/interfaces/${baseName}.interface`;
627
+ }
628
+ const existing = result.find((a) => a.path === importPath);
629
+ if (existing) existing.symbols.push(...symbols);
630
+ else result.push({ path: importPath, symbols });
631
+ }
632
+ return result;
597
633
  }
598
634
 
599
635
  function toSnakeFromPascal(s: string): string {
@@ -311,7 +311,9 @@ function renderOptionsParam(param: OptionsObjectParam): string {
311
311
  }
312
312
 
313
313
  function autoPaginatableItemType(returnType: string | undefined): string | undefined {
314
- return returnType?.match(/\bAutoPaginatable<\s*([A-Za-z_$][\w$]*)/)?.[1];
314
+ // Match both AutoPaginatable<T> and the legacy List<T> pattern so baseline
315
+ // item types are extracted even when the hand-written code predates AutoPaginatable.
316
+ return returnType?.match(/\b(?:AutoPaginatable|List)<\s*([A-Za-z_$][\w$]*)/)?.[1];
315
317
  }
316
318
 
317
319
  function baselineTypeSourceFile(ctx: EmitterContext, typeName: string): string | undefined {
@@ -347,9 +349,15 @@ function preferredBaselineTypeName(ctx: EmitterContext, typeName: string | undef
347
349
  }
348
350
 
349
351
  function preferredBaselineReturnType(ctx: EmitterContext, returnType: string | undefined): string | undefined {
352
+ if (!returnType) return undefined;
353
+ // Only preserve baseline return types that already use AutoPaginatable.
354
+ // Legacy patterns like List<T> can't be used as-is since the generated
355
+ // method body returns new AutoPaginatable(...).
356
+ if (!/\bAutoPaginatable\b/.test(returnType)) return undefined;
350
357
  const itemType = autoPaginatableItemType(returnType);
358
+ if (!itemType) return undefined;
351
359
  const preferred = preferredBaselineTypeName(ctx, itemType);
352
- if (!returnType || !itemType || !preferred || preferred === itemType) return returnType;
360
+ if (!preferred || preferred === itemType) return returnType;
353
361
  return returnType.replace(new RegExp(`\\b${itemType}\\b`, 'g'), preferred);
354
362
  }
355
363
 
@@ -1146,7 +1154,11 @@ function generateResourceClass(service: Service, ctx: EmitterContext): Generated
1146
1154
  // extension fields land on top and the camelCase keys don't also
1147
1155
  // leak into the query string.
1148
1156
  lines.push(' const wire: Record<string, unknown> = {');
1157
+ const baselineFields = (
1158
+ ctx.apiSurface?.interfaces as Record<string, { fields?: Record<string, unknown> }> | undefined
1159
+ )?.[optionsName]?.fields;
1149
1160
  for (const p of PAGINATION_PARAM_NAMES) {
1161
+ if (baselineFields && !(p in baselineFields) && !baselineFields[toCamelCase(p)]) continue;
1150
1162
  lines.push(` ${p}: options.${p},`);
1151
1163
  }
1152
1164
  lines.push(' };');
@@ -1503,7 +1515,9 @@ function renderMethod(
1503
1515
  const unwrapped = unwrapListModel(pModel, modelMap);
1504
1516
  if (unwrapped) itemRawName = unwrapped.name;
1505
1517
  }
1506
- const itemTypeName = resolveInterfaceName(itemRawName, ctx);
1518
+ const baselineItemType =
1519
+ autoPaginatableItemType(baselineClassMethod?.returnType) ?? autoPaginatableItemType(overlayMethod?.returnType);
1520
+ const itemTypeName = preferredBaselineTypeName(ctx, baselineItemType) ?? resolveInterfaceName(itemRawName, ctx);
1507
1521
  docParts.push(`@returns {Promise<AutoPaginatable<${itemTypeName}>>}`);
1508
1522
  } else if (responseModel) {
1509
1523
  const returnTypeDoc = plan.isArrayResponse ? `${responseModel}[]` : responseModel;
@@ -1659,15 +1673,18 @@ function renderOptionsObjectMethod(
1659
1673
  (itemRawName ? resolveInterfaceName(itemRawName, ctx) : responseModel);
1660
1674
  if (!itemType) return false;
1661
1675
  const wireType = wireInterfaceName(itemType);
1662
- const returnType =
1663
- preferredBaselineReturnType(ctx, baselineMethod?.returnType) ?? `Promise<AutoPaginatable<${itemType}>>`;
1664
1676
  const extraParams = op.queryParams.filter((p) => !PAGINATION_PARAM_NAMES.has(p.name));
1665
1677
  const needsWireSerializer = extraParams.some((p) => fieldName(p.name) !== wireFieldName(p.name));
1678
+ const paginationType = needsWireSerializer ? 'PaginationOptions' : optionParam.type;
1679
+ const returnType = needsWireSerializer
1680
+ ? `Promise<AutoPaginatable<${itemType}, ${paginationType}>>`
1681
+ : (preferredBaselineReturnType(ctx, baselineMethod?.returnType) ??
1682
+ `Promise<AutoPaginatable<${itemType}, ${paginationType}>>`);
1666
1683
  const listOptionsExpr = needsWireSerializer
1667
1684
  ? `options ? serialize${optionParam.type}(options) : undefined`
1668
1685
  : 'paginationOptions';
1669
1686
  lines.push(` async ${method}(${renderOptionsParam(optionParam)}): ${returnType} {`);
1670
- renderOptionsObjectDestructure(lines, pathBindings, 'paginationOptions');
1687
+ renderOptionsObjectDestructure(lines, pathBindings, needsWireSerializer ? undefined : 'paginationOptions');
1671
1688
  lines.push(` return new AutoPaginatable(`);
1672
1689
  lines.push(` await fetchAndDeserialize<${wireType}, ${itemType}>(`);
1673
1690
  lines.push(` this.workos,`);
@@ -1682,7 +1699,7 @@ function renderOptionsObjectMethod(
1682
1699
  lines.push(` deserialize${itemType},`);
1683
1700
  lines.push(` params,`);
1684
1701
  lines.push(` ),`);
1685
- lines.push(` paginationOptions,`);
1702
+ lines.push(` ${listOptionsExpr},`);
1686
1703
  lines.push(` );`);
1687
1704
  lines.push(' }');
1688
1705
  return true;
@@ -1873,7 +1890,8 @@ function renderPaginatedMethod(
1873
1890
  const wireType = wireInterfaceName(itemType);
1874
1891
  const serializeCall = serializerArg ? `options ? serialize${optionsType}(options) : undefined` : 'options';
1875
1892
 
1876
- lines.push(` async ${method}(${allParams}): Promise<AutoPaginatable<${itemType}, ${optionsType}>> {`);
1893
+ const paginationType = needsWireSerializer ? 'PaginationOptions' : optionsType;
1894
+ lines.push(` async ${method}(${allParams}): Promise<AutoPaginatable<${itemType}, ${paginationType}>> {`);
1877
1895
  lines.push(` return new AutoPaginatable(`);
1878
1896
  lines.push(` await fetchAndDeserialize<${wireType}, ${itemType}>(`);
1879
1897
  lines.push(` this.workos,`);
@@ -1888,7 +1906,7 @@ function renderPaginatedMethod(
1888
1906
  lines.push(` deserialize${itemType},`);
1889
1907
  lines.push(` params,`);
1890
1908
  lines.push(` ),`);
1891
- lines.push(` options,`);
1909
+ lines.push(` ${serializeCall},`);
1892
1910
  lines.push(` );`);
1893
1911
  lines.push(' }');
1894
1912
  }