@workos/oagen-emitters 0.14.3 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +19 -0
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{plugin-D0qLBiGv.mjs → plugin-CO4RFgAW.mjs} +983 -270
- package/dist/plugin-CO4RFgAW.mjs.map +1 -0
- package/dist/plugin.mjs +1 -1
- package/package.json +9 -9
- package/renovate.json +1 -61
- package/src/go/client.ts +1 -1
- package/src/go/enums.ts +77 -0
- package/src/kotlin/enums.ts +11 -4
- package/src/node/client.ts +119 -2
- package/src/node/discriminated-models.ts +20 -28
- package/src/node/field-plan.ts +64 -8
- package/src/node/index.ts +59 -3
- package/src/node/models.ts +73 -30
- package/src/node/naming.ts +14 -1
- package/src/node/node-overrides.ts +4 -37
- package/src/node/options.ts +29 -1
- package/src/node/resources.ts +533 -83
- package/src/node/tests.ts +108 -7
- package/src/php/fixtures.ts +4 -1
- package/src/php/models.ts +3 -1
- package/src/php/resources.ts +40 -11
- package/src/php/tests.ts +22 -12
- package/src/python/client.ts +0 -8
- package/src/python/enums.ts +41 -15
- package/src/python/fixtures.ts +23 -7
- package/src/python/models.ts +26 -5
- package/src/python/resources.ts +71 -3
- package/src/python/tests.ts +70 -12
- package/src/python/wrappers.ts +25 -4
- package/src/ruby/client.ts +0 -1
- package/src/ruby/rbi.ts +12 -6
- package/src/rust/resources.ts +10 -7
- package/src/shared/non-spec-services.ts +0 -5
- package/test/go/enums.test.ts +24 -0
- package/test/node/resources.test.ts +11 -1
- package/test/node/tests.test.ts +3 -3
- package/test/php/client.test.ts +0 -1
- package/test/php/resources.test.ts +50 -0
- package/test/rust/resources.test.ts +9 -0
- package/dist/plugin-D0qLBiGv.mjs.map +0 -1
package/src/node/field-plan.ts
CHANGED
|
@@ -7,8 +7,15 @@ import {
|
|
|
7
7
|
isBaselineGeneric,
|
|
8
8
|
createServiceDirResolver,
|
|
9
9
|
modelHasNewFields,
|
|
10
|
+
assignModelsToServices,
|
|
10
11
|
} from './utils.js';
|
|
11
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
liveSurfaceHasFunction,
|
|
14
|
+
liveSurfaceHasFile,
|
|
15
|
+
liveSurfaceFunctionPath,
|
|
16
|
+
liveSurfaceHasAutogenFile,
|
|
17
|
+
} from './live-surface.js';
|
|
18
|
+
import { isNodeOwnedService } from './options.js';
|
|
12
19
|
|
|
13
20
|
// ---------------------------------------------------------------------------
|
|
14
21
|
// Guard strategy
|
|
@@ -51,6 +58,19 @@ function helperExists(helperName: string, depModelName: string, ctx: EmitterCont
|
|
|
51
58
|
if (liveSurfaceHasFunction(helperName)) return true;
|
|
52
59
|
const depModel = ctx.spec.models.find((m) => m.name === depModelName);
|
|
53
60
|
if (!depModel) return false;
|
|
61
|
+
const modelToService = assignModelsToServices(ctx.spec.models, ctx.spec.services, ctx.modelHints);
|
|
62
|
+
const depService = modelToService.get(depModelName);
|
|
63
|
+
const resolvedName = resolveInterfaceName(depModelName, ctx);
|
|
64
|
+
const siblingPrefix = helperName.startsWith('serialize') ? 'deserialize' : 'serialize';
|
|
65
|
+
const siblingPath = liveSurfaceFunctionPath(`${siblingPrefix}${resolvedName}`);
|
|
66
|
+
if (siblingPath && liveSurfaceHasFile(siblingPath) && !liveSurfaceHasAutogenFile(siblingPath)) return false;
|
|
67
|
+
const sourceFile = (ctx.apiSurface?.interfaces?.[resolvedName] as { sourceFile?: string } | undefined)?.sourceFile;
|
|
68
|
+
const { resolveDir } = createServiceDirResolver(ctx.spec.models, ctx.spec.services, ctx);
|
|
69
|
+
const candidate = sourceFile
|
|
70
|
+
? sourceFile.replace('/interfaces/', '/serializers/').replace('.interface.ts', '.serializer.ts')
|
|
71
|
+
: `src/${resolveDir(depService)}/serializers/${fileName(depModelName)}.serializer.ts`;
|
|
72
|
+
if (liveSurfaceHasFile(candidate) && !liveSurfaceHasAutogenFile(candidate)) return false;
|
|
73
|
+
if (isNodeOwnedService(ctx, depService)) return true;
|
|
54
74
|
return modelHasNewFields(depModel, ctx);
|
|
55
75
|
}
|
|
56
76
|
|
|
@@ -699,6 +719,7 @@ export function buildSerializerImports(
|
|
|
699
719
|
const depService = sctx.modelToService.get(dep);
|
|
700
720
|
const depDir = sctx.resolveDir(depService);
|
|
701
721
|
const depName = resolveInterfaceName(dep, sctx.ctx);
|
|
722
|
+
const depIsOwned = isNodeOwnedService(sctx.ctx, depService);
|
|
702
723
|
|
|
703
724
|
// Locate the serializer file, in priority order:
|
|
704
725
|
// 1. The actual file containing `deserialize${depName}` per
|
|
@@ -708,14 +729,16 @@ export function buildSerializerImports(
|
|
|
708
729
|
// 2. The baseline interface's adjacent serializer file path.
|
|
709
730
|
// 3. The IR-name path — this is where the emitter writes the
|
|
710
731
|
// serializer it's producing this run.
|
|
711
|
-
const baselineSrc =
|
|
732
|
+
const baselineSrc = depIsOwned
|
|
733
|
+
? undefined
|
|
734
|
+
: (sctx.ctx.apiSurface?.interfaces?.[depName] as { sourceFile?: string } | undefined)?.sourceFile;
|
|
712
735
|
const baselineSerializerPath = baselineSrc
|
|
713
736
|
? baselineSrc.replace('/interfaces/', '/serializers/').replace('.interface.ts', '.serializer.ts')
|
|
714
737
|
: null;
|
|
715
738
|
const irNameSerializerPath = `src/${depDir}/serializers/${fileName(dep)}.serializer.ts`;
|
|
716
739
|
|
|
717
|
-
const liveDeserPath = liveSurfaceFunctionPath(`deserialize${depName}`);
|
|
718
|
-
const liveSerPath = liveSurfaceFunctionPath(`serialize${depName}`);
|
|
740
|
+
const liveDeserPath = depIsOwned ? undefined : liveSurfaceFunctionPath(`deserialize${depName}`);
|
|
741
|
+
const liveSerPath = depIsOwned ? undefined : liveSurfaceFunctionPath(`serialize${depName}`);
|
|
719
742
|
const depSerializerPath =
|
|
720
743
|
liveDeserPath ??
|
|
721
744
|
liveSerPath ??
|
|
@@ -742,11 +765,11 @@ export function buildSerializerImports(
|
|
|
742
765
|
// pass-through expression when it can't call the helper.
|
|
743
766
|
const hasDeser = liveSurfaceHasFunction(`deserialize${depName}`);
|
|
744
767
|
const hasSer = liveSurfaceHasFunction(`serialize${depName}`);
|
|
745
|
-
const fileExists = liveSurfaceHasFile(depSerializerPath);
|
|
768
|
+
const fileExists = !depIsOwned && liveSurfaceHasFile(depSerializerPath);
|
|
746
769
|
if (fileExists && !hasDeser && !hasSer) continue;
|
|
747
770
|
if (!fileExists) {
|
|
748
771
|
const depModel = sctx.ctx.spec.models.find((m) => m.name === dep);
|
|
749
|
-
const willGenerateSerializer = depModel ? modelHasNewFields(depModel, sctx.ctx) : true;
|
|
772
|
+
const willGenerateSerializer = depModel ? depIsOwned || modelHasNewFields(depModel, sctx.ctx) : true;
|
|
750
773
|
if (!willGenerateSerializer) continue;
|
|
751
774
|
}
|
|
752
775
|
|
|
@@ -803,7 +826,9 @@ export function shouldSkipSerializeForModel(
|
|
|
803
826
|
skippedSerializeModels: Set<string>,
|
|
804
827
|
ctx: EmitterContext,
|
|
805
828
|
): boolean {
|
|
806
|
-
let shouldSkip =
|
|
829
|
+
let shouldSkip =
|
|
830
|
+
serializerHasBaselineIncompatibility(model, baselineResponse, baselineDomain, ctx) ||
|
|
831
|
+
hasUnsafeSerializePassthrough(model, baselineDomain, baselineResponse, ctx);
|
|
807
832
|
if (!shouldSkip) {
|
|
808
833
|
for (const field of model.fields) {
|
|
809
834
|
for (const ref of collectSerializedModelRefs(field.type)) {
|
|
@@ -816,6 +841,11 @@ export function shouldSkipSerializeForModel(
|
|
|
816
841
|
shouldSkip = true;
|
|
817
842
|
break;
|
|
818
843
|
}
|
|
844
|
+
const resolved = resolveInterfaceName(ref, ctx);
|
|
845
|
+
if (wireInterfaceName(resolved) !== resolved && !helperExists(`serialize${resolved}`, ref, ctx)) {
|
|
846
|
+
shouldSkip = true;
|
|
847
|
+
break;
|
|
848
|
+
}
|
|
819
849
|
}
|
|
820
850
|
if (shouldSkip) break;
|
|
821
851
|
}
|
|
@@ -836,6 +866,8 @@ export function emitSerializerBody(
|
|
|
836
866
|
ctx: EmitterContext,
|
|
837
867
|
): string[] {
|
|
838
868
|
const lines: string[] = [];
|
|
869
|
+
const effectiveShouldSkipSerialize =
|
|
870
|
+
shouldSkipSerialize || hasUnsafeSerializePassthrough(model, baselineDomain, baselineResponse, ctx);
|
|
839
871
|
|
|
840
872
|
if (!shouldSkipDeserialize) {
|
|
841
873
|
const seenDeserFields = new Set<string>();
|
|
@@ -853,7 +885,7 @@ export function emitSerializerBody(
|
|
|
853
885
|
lines.push('});');
|
|
854
886
|
}
|
|
855
887
|
|
|
856
|
-
if (!
|
|
888
|
+
if (!effectiveShouldSkipSerialize) {
|
|
857
889
|
if (!shouldSkipDeserialize) lines.push('');
|
|
858
890
|
const serParamPrefix = model.fields.length === 0 ? '_' : '';
|
|
859
891
|
lines.push(`export const serialize${domainName} = ${typeParams.decl}(`);
|
|
@@ -872,3 +904,27 @@ export function emitSerializerBody(
|
|
|
872
904
|
|
|
873
905
|
return lines;
|
|
874
906
|
}
|
|
907
|
+
|
|
908
|
+
function hasUnsafeSerializePassthrough(
|
|
909
|
+
model: Model,
|
|
910
|
+
baselineDomain: BaselineInterface | undefined,
|
|
911
|
+
baselineResponse: BaselineInterface | undefined,
|
|
912
|
+
ctx: EmitterContext,
|
|
913
|
+
): boolean {
|
|
914
|
+
if (!baselineDomain?.fields || !baselineResponse?.fields) return false;
|
|
915
|
+
|
|
916
|
+
for (const field of model.fields) {
|
|
917
|
+
const domain = fieldName(field.name);
|
|
918
|
+
const wire = wireFieldName(field.name);
|
|
919
|
+
const domainField = baselineDomain.fields[domain];
|
|
920
|
+
const wireField = baselineResponse.fields[wire];
|
|
921
|
+
if (!domainField || !wireField || domainField.type === wireField.type) continue;
|
|
922
|
+
|
|
923
|
+
const domainAccess = `model.${domain}`;
|
|
924
|
+
if (serializeExpression(field.type, domainAccess, ctx) === domainAccess) {
|
|
925
|
+
return true;
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
return false;
|
|
930
|
+
}
|
package/src/node/index.ts
CHANGED
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
setBaselineSerializedNames,
|
|
24
24
|
setBaselineInterfaceNames,
|
|
25
25
|
setAdoptedModelNames,
|
|
26
|
+
setDiscriminatedModelNames,
|
|
26
27
|
setStructurallyRenamedDomainNames,
|
|
27
28
|
resolveInterfaceName,
|
|
28
29
|
} from './naming.js';
|
|
@@ -341,14 +342,66 @@ function isOwnedPath(relPath: string, policy: LiveSurfacePolicy): boolean {
|
|
|
341
342
|
return dir !== undefined && policy.ownedServiceDirs.has(dir);
|
|
342
343
|
}
|
|
343
344
|
|
|
345
|
+
function extractRelativeImportPaths(content: string, fromPath: string): string[] {
|
|
346
|
+
const dir = path.dirname(fromPath);
|
|
347
|
+
const paths: string[] = [];
|
|
348
|
+
const re = /from\s+['"](\.[^'"]+)['"]/g;
|
|
349
|
+
let match: RegExpExecArray | null;
|
|
350
|
+
while ((match = re.exec(content)) !== null) {
|
|
351
|
+
paths.push(path.normalize(path.join(dir, match[1])) + '.ts');
|
|
352
|
+
}
|
|
353
|
+
return paths;
|
|
354
|
+
}
|
|
355
|
+
|
|
344
356
|
function applyLiveSurface(files: GeneratedFile[], ctx: EmitterContext, surface: LiveSurface): GeneratedFile[] {
|
|
345
357
|
const out: GeneratedFile[] = [];
|
|
346
358
|
const policy = buildLiveSurfacePolicy(ctx, surface);
|
|
359
|
+
const filesByPath = new Map(files.map((f) => [f.path, f]));
|
|
360
|
+
const dependencyAllowedPaths = new Set<string>();
|
|
361
|
+
const queue: string[] = [];
|
|
362
|
+
|
|
363
|
+
for (const f of files) {
|
|
364
|
+
if (f.integrateTarget === false) continue;
|
|
365
|
+
if (!canCreateNewPath(f.path, policy)) continue;
|
|
366
|
+
for (const importPath of extractRelativeImportPaths(f.content, f.path)) {
|
|
367
|
+
if (
|
|
368
|
+
filesByPath.has(importPath) &&
|
|
369
|
+
!canCreateNewPath(importPath, policy) &&
|
|
370
|
+
!dependencyAllowedPaths.has(importPath)
|
|
371
|
+
) {
|
|
372
|
+
dependencyAllowedPaths.add(importPath);
|
|
373
|
+
queue.push(importPath);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
while (queue.length > 0) {
|
|
379
|
+
const relPath = queue.pop()!;
|
|
380
|
+
const file = filesByPath.get(relPath);
|
|
381
|
+
if (!file) continue;
|
|
382
|
+
for (const importPath of extractRelativeImportPaths(file.content, relPath)) {
|
|
383
|
+
if (
|
|
384
|
+
filesByPath.has(importPath) &&
|
|
385
|
+
!canCreateNewPath(importPath, policy) &&
|
|
386
|
+
!dependencyAllowedPaths.has(importPath)
|
|
387
|
+
) {
|
|
388
|
+
dependencyAllowedPaths.add(importPath);
|
|
389
|
+
queue.push(importPath);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
347
394
|
for (const f of files) {
|
|
348
395
|
const ownedPath = isOwnedPath(f.path, policy);
|
|
349
396
|
if (f.integrateTarget === false) continue;
|
|
350
397
|
if (surface.protectedFiles.has(f.path)) continue;
|
|
351
|
-
if (
|
|
398
|
+
if (
|
|
399
|
+
policy.hasExistingSdk &&
|
|
400
|
+
!policy.managedPaths.has(f.path) &&
|
|
401
|
+
!canCreateNewPath(f.path, policy) &&
|
|
402
|
+
!dependencyAllowedPaths.has(f.path)
|
|
403
|
+
)
|
|
404
|
+
continue;
|
|
352
405
|
|
|
353
406
|
// Hand-written files (on disk, no `auto-generated by oagen` header) →
|
|
354
407
|
// drop. The engine would otherwise prepend the header on
|
|
@@ -372,7 +425,8 @@ function applyLiveSurface(files: GeneratedFile[], ctx: EmitterContext, surface:
|
|
|
372
425
|
const dir = topLevelDir(f.path);
|
|
373
426
|
const isAdoptedDir = dir !== undefined && policy.adoptedServiceDirs.has(dir);
|
|
374
427
|
const isManagedDir = ownedPath || isAdoptedDir;
|
|
375
|
-
if (surface.files.has(f.path) && !surface.autogenFiles.has(f.path))
|
|
428
|
+
if (surface.files.has(f.path) && !surface.autogenFiles.has(f.path) && !(ownedPath && policy.regenerateOwnedTests))
|
|
429
|
+
continue;
|
|
376
430
|
if (!isManagedDir && !surface.autogenFiles.has(f.path)) continue;
|
|
377
431
|
if (isManagedDir && !policy.regenerateOwnedTests) continue;
|
|
378
432
|
}
|
|
@@ -487,7 +541,9 @@ export const nodeEmitter: Emitter = {
|
|
|
487
541
|
// name set on ctx so models.ts skips emitting an interface/serializer —
|
|
488
542
|
// the discriminated module owns those paths instead.
|
|
489
543
|
const discPlans = planDiscriminatedModels(enriched, nodeCtx);
|
|
490
|
-
|
|
544
|
+
const discriminatedNames = new Set(discPlans.keys());
|
|
545
|
+
(nodeCtx as { _discriminatedModelNames?: Set<string> })._discriminatedModelNames = discriminatedNames;
|
|
546
|
+
setDiscriminatedModelNames(discriminatedNames);
|
|
491
547
|
const standardFiles = generateModelsAndSerializers(enriched, nodeCtx);
|
|
492
548
|
const discFiles = generateDiscriminatedFiles(discPlans, nodeCtx);
|
|
493
549
|
return applyLiveSurface([...standardFiles, ...discFiles], nodeCtx, surface);
|
package/src/node/models.ts
CHANGED
|
@@ -223,6 +223,20 @@ export function generateModels(models: Model[], ctx: EmitterContext, shared?: Sh
|
|
|
223
223
|
// wrapper's interface importing from a file that was never written.
|
|
224
224
|
const listMetadataNeeded = collectReferencedListMetadataModels(models, nonPaginatedRefs);
|
|
225
225
|
|
|
226
|
+
for (const originalModel of models) {
|
|
227
|
+
const model = projectedByName.get(originalModel.name) ?? originalModel;
|
|
228
|
+
if (!reachableModels.has(model.name)) continue;
|
|
229
|
+
if (interfaceEligibleModels && !interfaceEligibleModels.has(model.name)) continue;
|
|
230
|
+
const service = modelToService.get(model.name);
|
|
231
|
+
const isOwnedModel = isNodeOwnedService(ctx, service);
|
|
232
|
+
if (!isOwnedModel && !modelHasNewFields(model, ctx) && !forceGenerate.has(model.name)) continue;
|
|
233
|
+
const canonicalName = dedup.get(model.name);
|
|
234
|
+
if (canonicalName) {
|
|
235
|
+
forceGenerate.add(canonicalName);
|
|
236
|
+
if (interfaceEligibleModels) interfaceEligibleModels.add(canonicalName);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
226
240
|
for (const originalModel of models) {
|
|
227
241
|
const model = projectedByName.get(originalModel.name) ?? originalModel;
|
|
228
242
|
if (!reachableModels.has(model.name)) continue;
|
|
@@ -392,6 +406,7 @@ export function generateModels(models: Model[], ctx: EmitterContext, shared?: Sh
|
|
|
392
406
|
const depName = resolveInterfaceName(dep, ctx);
|
|
393
407
|
const depService = modelToService.get(dep);
|
|
394
408
|
const depDir = resolveDir(depService);
|
|
409
|
+
const depIsOwned = isNodeOwnedService(ctx, depService);
|
|
395
410
|
|
|
396
411
|
// When the resolver maps the IR name to a different baseline interface
|
|
397
412
|
// (via `overlayLookup.modelNameByIR` structural match), the import
|
|
@@ -400,7 +415,9 @@ export function generateModels(models: Model[], ctx: EmitterContext, shared?: Sh
|
|
|
400
415
|
// emitter never generates — the canonical baseline file is at a
|
|
401
416
|
// different stem (e.g. `create-audit-log-event-options.interface`).
|
|
402
417
|
const currentFilePath = `src/${dirName}/interfaces/${fileName(model.name)}.interface.ts`;
|
|
403
|
-
const baselineSrc =
|
|
418
|
+
const baselineSrc = depIsOwned
|
|
419
|
+
? undefined
|
|
420
|
+
: (ctx.apiSurface?.interfaces?.[depName] as { sourceFile?: string } | undefined)?.sourceFile;
|
|
404
421
|
|
|
405
422
|
// Self-reference: the dependency lives in the file we're currently
|
|
406
423
|
// emitting. Skip the import — it's already in scope.
|
|
@@ -697,6 +714,13 @@ export function generateSerializers(
|
|
|
697
714
|
const responseReachableModels = resourceUsage
|
|
698
715
|
? expandModelRoots(resourceUsage.responseRoots, projectedByName)
|
|
699
716
|
: undefined;
|
|
717
|
+
// Models reachable from any request — only these need a `serialize<X>`.
|
|
718
|
+
// A model used solely as a response body can safely be deserialize-only;
|
|
719
|
+
// emitting its serialize half is both unused and brittle when it contains
|
|
720
|
+
// legacy nested response models that intentionally have no serialize helper.
|
|
721
|
+
const requestReachableModels = resourceUsage
|
|
722
|
+
? expandModelRoots(resourceUsage.requestRoots, projectedByName)
|
|
723
|
+
: undefined;
|
|
700
724
|
|
|
701
725
|
const serializerReachable = computeNonEventReachable(ctx.spec.services, models);
|
|
702
726
|
|
|
@@ -759,6 +783,20 @@ export function generateSerializers(
|
|
|
759
783
|
// Mirror the interface-emission gate (see `generateModels`).
|
|
760
784
|
const serializerListMetadataNeeded = collectReferencedListMetadataModels(models, serializerNonPaginatedRefs);
|
|
761
785
|
|
|
786
|
+
for (const originalModel of models) {
|
|
787
|
+
const model = projectedByName.get(originalModel.name) ?? originalModel;
|
|
788
|
+
if (!serializerReachable.has(model.name)) continue;
|
|
789
|
+
if (serializerEligibleModels && !serializerEligibleModels.has(model.name)) continue;
|
|
790
|
+
const service = modelToService.get(model.name);
|
|
791
|
+
const isOwnedModel = isNodeOwnedService(ctx, service);
|
|
792
|
+
if (!isOwnedModel && !modelHasNewFields(model, ctx) && !forceGenerateSerializer.has(model.name)) continue;
|
|
793
|
+
const canonicalName = dedup.get(model.name);
|
|
794
|
+
if (canonicalName) {
|
|
795
|
+
forceGenerateSerializer.add(canonicalName);
|
|
796
|
+
if (serializerEligibleModels) serializerEligibleModels.add(canonicalName);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
|
|
762
800
|
const eligibleModels: Model[] = [];
|
|
763
801
|
for (const originalModel of models) {
|
|
764
802
|
const model = projectedByName.get(originalModel.name) ?? originalModel;
|
|
@@ -781,14 +819,9 @@ export function generateSerializers(
|
|
|
781
819
|
const responseName = wireInterfaceName(domainName);
|
|
782
820
|
const baselineResponse = ctx.apiSurface?.interfaces?.[responseName];
|
|
783
821
|
const baselineDomain = ctx.apiSurface?.interfaces?.[domainName];
|
|
784
|
-
const shouldSkip =
|
|
785
|
-
model
|
|
786
|
-
baselineResponse,
|
|
787
|
-
baselineDomain,
|
|
788
|
-
dedup,
|
|
789
|
-
skippedSerializeModels,
|
|
790
|
-
ctx,
|
|
791
|
-
);
|
|
822
|
+
const shouldSkip =
|
|
823
|
+
(requestReachableModels !== undefined && !requestReachableModels.has(model.name)) ||
|
|
824
|
+
shouldSkipSerializeForModel(model, baselineResponse, baselineDomain, dedup, skippedSerializeModels, ctx);
|
|
792
825
|
if (shouldSkip) {
|
|
793
826
|
skippedSerializeModels.add(model.name);
|
|
794
827
|
}
|
|
@@ -812,27 +845,37 @@ export function generateSerializers(
|
|
|
812
845
|
|
|
813
846
|
if (serializerPath === canonSerializerPath) continue;
|
|
814
847
|
if (domainName === canonDomainName) continue;
|
|
815
|
-
|
|
816
|
-
const
|
|
817
|
-
const
|
|
818
|
-
responseReachableModels
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
848
|
+
|
|
849
|
+
const aliasNeedsDeserialize = responseReachableModels === undefined || responseReachableModels.has(model.name);
|
|
850
|
+
const canonicalHasDeserialize =
|
|
851
|
+
responseReachableModels === undefined || responseReachableModels.has(canonicalName);
|
|
852
|
+
const canAliasToCanonical = !aliasNeedsDeserialize || canonicalHasDeserialize;
|
|
853
|
+
if (canAliasToCanonical) {
|
|
854
|
+
const rel = relativeImport(serializerPath, canonSerializerPath);
|
|
855
|
+
const canonSkipSerialize = skippedSerializeModels.has(canonicalName) || skippedSerializeModels.has(model.name);
|
|
856
|
+
const canonSkipDeserialize =
|
|
857
|
+
responseReachableModels !== undefined &&
|
|
858
|
+
!responseReachableModels.has(canonicalName) &&
|
|
859
|
+
!responseReachableModels.has(model.name);
|
|
860
|
+
if (canonSkipSerialize && canonSkipDeserialize) continue;
|
|
861
|
+
const parts: string[] = [];
|
|
862
|
+
if (!canonSkipDeserialize) {
|
|
863
|
+
parts.push(`deserialize${canonDomainName} as deserialize${domainName}`);
|
|
864
|
+
}
|
|
865
|
+
if (!canonSkipSerialize) {
|
|
866
|
+
parts.push(`serialize${canonDomainName} as serialize${domainName}`);
|
|
867
|
+
}
|
|
868
|
+
const reexportContent = `export { ${parts.join(', ')} } from '${rel}';`;
|
|
869
|
+
files.push({
|
|
870
|
+
path: serializerPath,
|
|
871
|
+
content: reexportContent,
|
|
872
|
+
overwriteExisting: true,
|
|
873
|
+
});
|
|
874
|
+
continue;
|
|
828
875
|
}
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
content: reexportContent,
|
|
833
|
-
overwriteExisting: true,
|
|
834
|
-
});
|
|
835
|
-
continue;
|
|
876
|
+
// The alias is response-reachable, but the canonical model is
|
|
877
|
+
// request-only. Generate a local serializer instead of re-exporting a
|
|
878
|
+
// deserialize helper that the canonical serializer intentionally omits.
|
|
836
879
|
}
|
|
837
880
|
|
|
838
881
|
const dirName = resolveDir(service);
|
|
@@ -906,7 +949,7 @@ export function generateSerializers(
|
|
|
906
949
|
}
|
|
907
950
|
const liveRootForBarrel = ctx.outputDir ?? ctx.targetDir;
|
|
908
951
|
for (const [dir, stems] of serializersByDir) {
|
|
909
|
-
if (liveRootForBarrel) {
|
|
952
|
+
if (liveRootForBarrel && !isNodeOwnedService(ctx, dir)) {
|
|
910
953
|
const serializersDir = path.join(liveRootForBarrel, 'src', dir, 'serializers');
|
|
911
954
|
try {
|
|
912
955
|
for (const entry of fs.readdirSync(serializersDir)) {
|
package/src/node/naming.ts
CHANGED
|
@@ -73,6 +73,16 @@ export function isAdoptedModelName(name: string): boolean {
|
|
|
73
73
|
return adoptedModelNames.has(name);
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
/**
|
|
77
|
+
* IR model names handled by the discriminated-models module. These must not
|
|
78
|
+
* be remapped by the structural matcher because that module emits files and
|
|
79
|
+
* helpers under the original IR names.
|
|
80
|
+
*/
|
|
81
|
+
let discriminatedModelNames: Set<string> = new Set();
|
|
82
|
+
export function setDiscriminatedModelNames(names: Set<string>): void {
|
|
83
|
+
discriminatedModelNames = names;
|
|
84
|
+
}
|
|
85
|
+
|
|
76
86
|
/**
|
|
77
87
|
* Domain names that `resolveInterfaceName` reached via a structural rename
|
|
78
88
|
* — the resolved name differs from the IR model's own name. `wireInterfaceName`
|
|
@@ -226,7 +236,10 @@ export function resolveInterfaceName(name: string, ctx: EmitterContext, opts?: {
|
|
|
226
236
|
const existing = ctx.overlayLookup?.interfaceByName?.get(name);
|
|
227
237
|
if (existing) return existing;
|
|
228
238
|
|
|
229
|
-
let inferred =
|
|
239
|
+
let inferred =
|
|
240
|
+
adoptedModelNames.has(name) || discriminatedModelNames.has(name)
|
|
241
|
+
? undefined
|
|
242
|
+
: ctx.overlayLookup?.modelNameByIR?.get(name);
|
|
230
243
|
if (inferred) {
|
|
231
244
|
if (inferred.startsWith('Serialized')) {
|
|
232
245
|
const stripped = inferred.slice('Serialized'.length);
|
|
@@ -1,41 +1,6 @@
|
|
|
1
1
|
import type { EmitterContext, Model, ResolvedOperation } from '@workos/oagen';
|
|
2
2
|
import { enrichModelsFromSpec } from '../shared/model-utils.js';
|
|
3
|
-
|
|
4
|
-
type OperationOverride = {
|
|
5
|
-
methodName?: string;
|
|
6
|
-
mountOn?: string;
|
|
7
|
-
};
|
|
8
|
-
|
|
9
|
-
const OPERATION_OVERRIDES: Record<string, OperationOverride> = {
|
|
10
|
-
'POST /organizations/{organizationId}/groups': {
|
|
11
|
-
methodName: 'create_group',
|
|
12
|
-
},
|
|
13
|
-
'GET /organizations/{organizationId}/groups': {
|
|
14
|
-
methodName: 'list_groups',
|
|
15
|
-
},
|
|
16
|
-
'GET /organizations/{organizationId}/groups/{groupId}': {
|
|
17
|
-
methodName: 'get_group',
|
|
18
|
-
},
|
|
19
|
-
'PATCH /organizations/{organizationId}/groups/{groupId}': {
|
|
20
|
-
methodName: 'update_group',
|
|
21
|
-
},
|
|
22
|
-
'DELETE /organizations/{organizationId}/groups/{groupId}': {
|
|
23
|
-
methodName: 'delete_group',
|
|
24
|
-
},
|
|
25
|
-
'POST /organizations/{organizationId}/groups/{groupId}/organization-memberships': {
|
|
26
|
-
methodName: 'add_organization_membership',
|
|
27
|
-
},
|
|
28
|
-
'GET /organizations/{organizationId}/groups/{groupId}/organization-memberships': {
|
|
29
|
-
methodName: 'list_organization_memberships',
|
|
30
|
-
},
|
|
31
|
-
'DELETE /organizations/{organizationId}/groups/{groupId}/organization-memberships/{omId}': {
|
|
32
|
-
methodName: 'remove_organization_membership',
|
|
33
|
-
},
|
|
34
|
-
'GET /user_management/organization_memberships/{omId}/groups': {
|
|
35
|
-
methodName: 'list_groups_for_organization_membership',
|
|
36
|
-
mountOn: 'UserManagement',
|
|
37
|
-
},
|
|
38
|
-
};
|
|
3
|
+
import { nodeOptions } from './options.js';
|
|
39
4
|
|
|
40
5
|
const contextCache = new WeakMap<EmitterContext, EmitterContext>();
|
|
41
6
|
|
|
@@ -88,9 +53,11 @@ export function withNodeOperationOverrides(ctx: EmitterContext): EmitterContext
|
|
|
88
53
|
return next;
|
|
89
54
|
}
|
|
90
55
|
|
|
56
|
+
const configOverrides = nodeOptions(ctx).operationOverrides ?? {};
|
|
57
|
+
|
|
91
58
|
let opsChanged = false;
|
|
92
59
|
const nextResolved = resolvedOperations.map((resolved) => {
|
|
93
|
-
const override =
|
|
60
|
+
const override = configOverrides[operationKey(resolved)];
|
|
94
61
|
if (!override) return resolved;
|
|
95
62
|
|
|
96
63
|
const methodName = override.methodName ?? resolved.methodName;
|
package/src/node/options.ts
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
import type { EmitterContext } from '@workos/oagen';
|
|
2
2
|
|
|
3
|
+
export interface OperationOverride {
|
|
4
|
+
methodName?: string;
|
|
5
|
+
mountOn?: string;
|
|
6
|
+
optionsType?: string;
|
|
7
|
+
bodyFieldMap?: Record<string, string>;
|
|
8
|
+
returnType?: string;
|
|
9
|
+
returnDataProperty?: string;
|
|
10
|
+
returnTypeImports?: string[];
|
|
11
|
+
returnExpression?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
3
14
|
export interface NodeEmitterOptions {
|
|
4
15
|
/**
|
|
5
16
|
* Existing SDK mode normally drops brand-new paths to avoid large accidental
|
|
@@ -21,6 +32,12 @@ export interface NodeEmitterOptions {
|
|
|
21
32
|
* and fixtures remain hand-owned.
|
|
22
33
|
*/
|
|
23
34
|
regenerateOwnedTests?: boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Node-specific operation overrides keyed by "METHOD /path".
|
|
37
|
+
* Allows renaming methods or remounting operations for the Node SDK
|
|
38
|
+
* without affecting other languages.
|
|
39
|
+
*/
|
|
40
|
+
operationOverrides?: Record<string, OperationOverride>;
|
|
24
41
|
}
|
|
25
42
|
|
|
26
43
|
export function nodeOptions(ctx: EmitterContext): NodeEmitterOptions {
|
|
@@ -32,10 +49,21 @@ function normalizeServiceName(name: string): string {
|
|
|
32
49
|
return name.replace(/[^a-zA-Z0-9]/g, '').toLowerCase();
|
|
33
50
|
}
|
|
34
51
|
|
|
52
|
+
function ownedLookupNames(name: string): string[] {
|
|
53
|
+
const names = [name];
|
|
54
|
+
const baselineDirPrefix = '__baseline_dir__:';
|
|
55
|
+
if (name.startsWith(baselineDirPrefix)) {
|
|
56
|
+
names.push(name.slice(baselineDirPrefix.length));
|
|
57
|
+
}
|
|
58
|
+
return names;
|
|
59
|
+
}
|
|
60
|
+
|
|
35
61
|
export function isNodeOwnedService(ctx: EmitterContext, ...names: Array<string | undefined>): boolean {
|
|
36
62
|
const configured = nodeOptions(ctx).ownedServices ?? [];
|
|
37
63
|
if (configured.length === 0) return false;
|
|
38
64
|
|
|
39
65
|
const owned = new Set(configured.map(normalizeServiceName));
|
|
40
|
-
return names.some((name) =>
|
|
66
|
+
return names.some((name) =>
|
|
67
|
+
name !== undefined ? ownedLookupNames(name).some((candidate) => owned.has(normalizeServiceName(candidate))) : false,
|
|
68
|
+
);
|
|
41
69
|
}
|