@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/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +14 -0
- package/dist/index.mjs +1 -1
- package/dist/{plugin-CO4RFgAW.mjs → plugin-Xkr83G9A.mjs} +90 -30
- package/dist/plugin-Xkr83G9A.mjs.map +1 -0
- package/dist/plugin.mjs +1 -1
- package/package.json +1 -1
- package/src/node/client.ts +40 -1
- package/src/node/discriminated-models.ts +60 -24
- package/src/node/resources.ts +27 -9
- package/dist/plugin-CO4RFgAW.mjs.map +0 -1
package/dist/plugin.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as workosEmittersPlugin } from "./plugin-
|
|
1
|
+
import { t as workosEmittersPlugin } from "./plugin-Xkr83G9A.mjs";
|
|
2
2
|
export { workosEmittersPlugin };
|
package/package.json
CHANGED
package/src/node/client.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
}
|
|
591
|
-
.
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
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 {
|
package/src/node/resources.ts
CHANGED
|
@@ -311,7 +311,9 @@ function renderOptionsParam(param: OptionsObjectParam): string {
|
|
|
311
311
|
}
|
|
312
312
|
|
|
313
313
|
function autoPaginatableItemType(returnType: string | undefined): string | undefined {
|
|
314
|
-
|
|
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 (!
|
|
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
|
|
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(`
|
|
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
|
-
|
|
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(`
|
|
1909
|
+
lines.push(` ${serializeCall},`);
|
|
1892
1910
|
lines.push(` );`);
|
|
1893
1911
|
lines.push(' }');
|
|
1894
1912
|
}
|