@workos/oagen 0.23.0 → 0.24.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.
@@ -2954,6 +2954,58 @@ function spliceExtraImports(existingLines, resultLines) {
2954
2954
  resultLines.splice(insertAt, 0, ...allExtra);
2955
2955
  }
2956
2956
  //#endregion
2957
+ //#region src/engine/scoped-services.ts
2958
+ /**
2959
+ * Validate a `--services` selection and return it as a set of POST-MOUNT service
2960
+ * names for emitters to scope on.
2961
+ *
2962
+ * Scoped generation does NOT filter the IR: model placement, dedup/aliasing, and
2963
+ * shared-schema decisions are global computations over the full service/model set
2964
+ * (filtering them churns shared `common/` files). Instead the full spec flows to
2965
+ * the emitters unchanged and this set is passed as `ctx.scopedServices`; each
2966
+ * emitter gates only its per-service resource/test emission on it.
2967
+ *
2968
+ * Matching is on the POST-MOUNT name. Mount resolution never mutates
2969
+ * `spec.services` — the post-mount target lives on `ResolvedOperation.mountOn`
2970
+ * (`mountOn = hint?.mountOn ?? mountRules[service.name] ?? service.name`) — so the
2971
+ * post-mount name of a source service is derived here as
2972
+ * `mountRules[service.name] ?? service.name`. Selecting a target (e.g.
2973
+ * `DirectorySync`) is valid because every source tag that mounts into it
2974
+ * (`DirectoryUsers`, `DirectoryGroups`, …) shares that post-mount name, and the
2975
+ * emitters group resources by mount, so all siblings emit together.
2976
+ *
2977
+ * @param spec - The parsed API spec (never mutated; never filtered).
2978
+ * @param selected - Post-mount service names from `--services` (normalized/trimmed).
2979
+ * @param mountRules - Source-service-name → post-mount-target mappings.
2980
+ * @returns The validated selection as a `Set` of post-mount names.
2981
+ * @throws A `ConfigError` listing valid post-mount names when a selection is
2982
+ * unknown — scoped generation never silently emits the wrong set.
2983
+ */
2984
+ /**
2985
+ * Canonicalize a service name to a casing/separator-insensitive key so that
2986
+ * `MultiFactorAuth` (post-mount IR name), `multi_factor_auth` (snake-cased
2987
+ * manifest record), and `multiFactorAuth` (camel-cased manifest record) all
2988
+ * collapse to `multifactorauth`. Used to match a spec service against the
2989
+ * services a prior manifest recorded as already on disk, where the casing of
2990
+ * the recorded `service` field varies per emitter.
2991
+ */
2992
+ function canonicalServiceKey(name) {
2993
+ return name.toLowerCase().replace(/[^a-z0-9]/g, "");
2994
+ }
2995
+ function resolveScopedServices(spec, selected, mountRules) {
2996
+ const mounts = mountRules ?? {};
2997
+ const postMountName = (name) => mounts[name] ?? name;
2998
+ const validPostMount = new Set(spec.services.map((s) => postMountName(s.name)));
2999
+ const selectedSet = new Set(selected);
3000
+ const unknown = [...selectedSet].filter((s) => !validPostMount.has(s));
3001
+ if (unknown.length > 0) {
3002
+ const err = /* @__PURE__ */ new Error(`Unknown --services: ${unknown.sort().join(", ")}. Valid post-mount services: ${[...validPostMount].sort().join(", ")}`);
3003
+ err.name = "ConfigError";
3004
+ throw err;
3005
+ }
3006
+ return selectedSet;
3007
+ }
3008
+ //#endregion
2957
3009
  //#region src/engine/generate-files.ts
2958
3010
  function buildEmitterContext(spec, options) {
2959
3011
  const resolvedOperations = resolveOperations(spec, options.operationHints, options.mountRules);
@@ -2978,11 +3030,32 @@ function buildEmitterContext(spec, options) {
2978
3030
  targetDir: options.target,
2979
3031
  priorTargetManifestPaths: options.priorTargetManifestPaths,
2980
3032
  scopedServices: options.scopedServices,
3033
+ presentServiceKeys: options.presentServiceKeys,
2981
3034
  scopedModelNames,
2982
3035
  scopedEnumNames
2983
3036
  };
2984
3037
  }
2985
3038
  /**
3039
+ * The services an aggregate (barrel/client/test) generator may reference in a
3040
+ * scoped run: the selected services plus every service the prior manifest
3041
+ * recorded as already on disk. A service the spec just added that is neither
3042
+ * selected nor already generated is excluded — otherwise the client/barrels
3043
+ * would import a resource/model whose file this run does not emit (the orphaned
3044
+ * `agents/__init__.py → ._resource` class of build break).
3045
+ *
3046
+ * Outside a scoped run (full generation), every service is in surface.
3047
+ */
3048
+ function aggregateSurfaceServices(spec, ctx) {
3049
+ const scope = ctx.scopedServices;
3050
+ if (!scope || scope.size === 0) return spec.services;
3051
+ const present = ctx.presentServiceKeys ?? /* @__PURE__ */ new Set();
3052
+ const postMountOf = (service) => ctx.resolvedOperations?.find((r) => r.service.name === service.name)?.mountOn ?? service.name;
3053
+ return spec.services.filter((s) => {
3054
+ const postMount = postMountOf(s);
3055
+ return scope.has(postMount) || present.has(canonicalServiceKey(postMount));
3056
+ });
3057
+ }
3058
+ /**
2986
3059
  * Collect model and enum names transitively referenced by service operations.
2987
3060
  * Walks operation parameters, request bodies, responses, and pagination item
2988
3061
  * types, then chases model field references until the set stabilizes.
@@ -3047,27 +3120,38 @@ function isDiscriminatedModel(model) {
3047
3120
  /**
3048
3121
  * Collect all generated files from an emitter (no headers, no path prefixes).
3049
3122
  *
3050
- * In a scoped (`--services`) run the spec is NOT filtered: every generator runs
3051
- * over the full spec so model placement, dedup, the root client, and all
3052
- * aggregate/barrel files are byte-identical to a full run (and a brand-new
3053
- * selected service is wired into the client automatically). The emitters
3054
- * themselves consult `ctx.scopedServices` to emit only the selected services'
3055
- * per-service resource/test files.
3123
+ * A full run emits over the whole spec. A scoped (`--services`) run still emits
3124
+ * model placement, dedup, the root client, and aggregate/barrel files
3125
+ * byte-identically for every service ALREADY on disk (plus the brand-new
3126
+ * selected service, which is wired into the client automatically) — but it
3127
+ * narrows that surface to `selected ∪ already-on-disk` services
3128
+ * (`aggregateSurfaceServices`). A service the spec just added that is neither
3129
+ * selected nor already generated is dropped from the surface entirely, so its
3130
+ * models/discriminators are not pulled into reachability and barrels/the client
3131
+ * never import a resource this run does not emit. The emitters additionally
3132
+ * consult `ctx.scopedServices` to emit only the selected services' per-service
3133
+ * resource/test files.
3056
3134
  */
3057
3135
  function generateAllFiles(spec, emitter, ctx) {
3058
- const referenced = collectReferencedNames(spec.services, spec.models);
3136
+ const scoped = !!ctx.scopedServices && ctx.scopedServices.size > 0;
3137
+ const surfaceServices = aggregateSurfaceServices(spec, ctx);
3138
+ const referenced = collectReferencedNames(surfaceServices, spec.models, { preserveAllDiscriminated: !scoped });
3059
3139
  const reachableModels = spec.models.filter((m) => referenced.models.has(m.name));
3060
3140
  const reachableEnums = spec.enums.filter((e) => referenced.enums.has(e.name));
3061
- const reachableSpec = {
3141
+ const surfaceSpec = {
3062
3142
  ...spec,
3143
+ services: surfaceServices
3144
+ };
3145
+ const reachableSpec = {
3146
+ ...surfaceSpec,
3063
3147
  models: reachableModels,
3064
3148
  enums: reachableEnums
3065
3149
  };
3066
3150
  return [
3067
3151
  ...emitter.generateModels(reachableModels, ctx),
3068
3152
  ...emitter.generateEnums(reachableEnums, ctx),
3069
- ...emitter.generateResources(spec.services, ctx),
3070
- ...emitter.generateClient(spec, ctx),
3153
+ ...emitter.generateResources(surfaceServices, ctx),
3154
+ ...emitter.generateClient(surfaceSpec, ctx),
3071
3155
  ...emitter.generateErrors(ctx),
3072
3156
  ...emitter.generateTypeSignatures?.(reachableSpec, ctx) ?? [],
3073
3157
  ...emitter.generateTests(reachableSpec, ctx)
@@ -3084,7 +3168,11 @@ function applyFileHeaders(files, header) {
3084
3168
  function generateFiles(spec, emitter, options) {
3085
3169
  const ctx = buildEmitterContext(spec, options);
3086
3170
  const files = generateAllFiles(spec, emitter, ctx);
3087
- const operations = emitter.buildOperationsMap?.(spec, ctx);
3171
+ const surfaceSpec = {
3172
+ ...spec,
3173
+ services: aggregateSurfaceServices(spec, ctx)
3174
+ };
3175
+ const operations = emitter.buildOperationsMap?.(surfaceSpec, ctx);
3088
3176
  const header = emitter.fileHeader();
3089
3177
  return {
3090
3178
  files: applyFileHeaders(files, header),
@@ -3094,47 +3182,6 @@ function generateFiles(spec, emitter, options) {
3094
3182
  };
3095
3183
  }
3096
3184
  //#endregion
3097
- //#region src/engine/scoped-services.ts
3098
- /**
3099
- * Validate a `--services` selection and return it as a set of POST-MOUNT service
3100
- * names for emitters to scope on.
3101
- *
3102
- * Scoped generation does NOT filter the IR: model placement, dedup/aliasing, and
3103
- * shared-schema decisions are global computations over the full service/model set
3104
- * (filtering them churns shared `common/` files). Instead the full spec flows to
3105
- * the emitters unchanged and this set is passed as `ctx.scopedServices`; each
3106
- * emitter gates only its per-service resource/test emission on it.
3107
- *
3108
- * Matching is on the POST-MOUNT name. Mount resolution never mutates
3109
- * `spec.services` — the post-mount target lives on `ResolvedOperation.mountOn`
3110
- * (`mountOn = hint?.mountOn ?? mountRules[service.name] ?? service.name`) — so the
3111
- * post-mount name of a source service is derived here as
3112
- * `mountRules[service.name] ?? service.name`. Selecting a target (e.g.
3113
- * `DirectorySync`) is valid because every source tag that mounts into it
3114
- * (`DirectoryUsers`, `DirectoryGroups`, …) shares that post-mount name, and the
3115
- * emitters group resources by mount, so all siblings emit together.
3116
- *
3117
- * @param spec - The parsed API spec (never mutated; never filtered).
3118
- * @param selected - Post-mount service names from `--services` (normalized/trimmed).
3119
- * @param mountRules - Source-service-name → post-mount-target mappings.
3120
- * @returns The validated selection as a `Set` of post-mount names.
3121
- * @throws A `ConfigError` listing valid post-mount names when a selection is
3122
- * unknown — scoped generation never silently emits the wrong set.
3123
- */
3124
- function resolveScopedServices(spec, selected, mountRules) {
3125
- const mounts = mountRules ?? {};
3126
- const postMountName = (name) => mounts[name] ?? name;
3127
- const validPostMount = new Set(spec.services.map((s) => postMountName(s.name)));
3128
- const selectedSet = new Set(selected);
3129
- const unknown = [...selectedSet].filter((s) => !validPostMount.has(s));
3130
- if (unknown.length > 0) {
3131
- const err = /* @__PURE__ */ new Error(`Unknown --services: ${unknown.sort().join(", ")}. Valid post-mount services: ${[...validPostMount].sort().join(", ")}`);
3132
- err.name = "ConfigError";
3133
- throw err;
3134
- }
3135
- return selectedSet;
3136
- }
3137
- //#endregion
3138
3185
  //#region src/engine/integrate.ts
3139
3186
  /**
3140
3187
  * Resolve a relative import path against a source file path to get the
@@ -3446,6 +3493,35 @@ function mergeScopedManifestRecords(prev, scopedFiles, scopedOperations) {
3446
3493
  };
3447
3494
  }
3448
3495
  /**
3496
+ * Extract the set of services a prior manifest records as already generated on
3497
+ * disk, as canonical (casing/separator-insensitive) keys.
3498
+ *
3499
+ * The manifest's `operations` map carries each operation's owning `service`
3500
+ * (snake_case for most emitters, camelCase for Kotlin). A scoped (`--services`)
3501
+ * run uses this to tell "service in the spec that is already on disk" apart from
3502
+ * "service the spec just added that this SDK has never generated" — only the
3503
+ * former may be wired into aggregate/barrel/client files alongside the
3504
+ * selection, so a brand-new out-of-scope service is never referenced by files
3505
+ * whose implementation this run does not emit.
3506
+ *
3507
+ * Returns an empty set when the manifest is null or carries no operations
3508
+ * (a legacy manifest with no operations data); callers treat that as "present
3509
+ * set unknown" and fall back to not narrowing the surface.
3510
+ */
3511
+ function manifestServiceKeys(manifest) {
3512
+ const keys = /* @__PURE__ */ new Set();
3513
+ const ops = manifest?.operations;
3514
+ if (!ops) return keys;
3515
+ for (const value of Object.values(ops)) {
3516
+ const entries = Array.isArray(value) ? value : [value];
3517
+ for (const entry of entries) {
3518
+ const service = entry?.service;
3519
+ if (typeof service === "string" && service.length > 0) keys.add(canonicalServiceKey(service));
3520
+ }
3521
+ }
3522
+ return keys;
3523
+ }
3524
+ /**
3449
3525
  * Delete stale files from `dir`.
3450
3526
  *
3451
3527
  * When `header` is provided, a file is only deleted if its contents start with
@@ -3520,11 +3596,14 @@ async function generate(spec, emitter, options) {
3520
3596
  const skipManifestRead = options.noPrune === true && !scoped;
3521
3597
  const outputPrevManifestForCtx = skipManifestRead ? null : await readManifest(options.outputDir);
3522
3598
  const targetManifestForCtx = options.target && !skipManifestRead ? await readManifest(options.target) : null;
3523
- const priorTargetManifestPaths = targetManifestForCtx ? new Set(targetManifestForCtx.files) : outputPrevManifestForCtx ? new Set(outputPrevManifestForCtx.files) : void 0;
3599
+ const priorManifestForCtx = targetManifestForCtx ?? outputPrevManifestForCtx;
3600
+ const priorTargetManifestPaths = priorManifestForCtx ? new Set(priorManifestForCtx.files) : void 0;
3601
+ const presentServiceKeys = scoped ? manifestServiceKeys(priorManifestForCtx) : void 0;
3524
3602
  const { files: withHeaders, header, operations } = generateFiles(spec, emitter, {
3525
3603
  ...options,
3526
3604
  priorTargetManifestPaths,
3527
- scopedServices
3605
+ scopedServices,
3606
+ presentServiceKeys
3528
3607
  });
3529
3608
  if (options.dryRun) {
3530
3609
  if (options.target) {
@@ -4069,4 +4148,4 @@ function summarize(changes) {
4069
4148
  //#endregion
4070
4149
  export { MANIFEST_FILENAME as a, treeShakeFiles as c, collectReferencedNames as d, generateAllFiles as f, resolveOperations as h, generate as i, applyFileHeaders as l, deriveMethodName as m, getEmitter as n, integrateGeneratedFiles as o, generateFiles as p, registerEmitter as r, mapFilesForTargetIntegration as s, diffSpecs as t, buildEmitterContext as u };
4071
4150
 
4072
- //# sourceMappingURL=diff-B7dMUCoJ.mjs.map
4151
+ //# sourceMappingURL=diff-8vcYeHKf.mjs.map