@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.
- package/dist/cli/index.mjs +198 -54
- package/dist/cli/index.mjs.map +1 -1
- package/dist/{diff-B7dMUCoJ.mjs → diff-8vcYeHKf.mjs} +134 -55
- package/dist/{diff-B7dMUCoJ.mjs.map → diff-8vcYeHKf.mjs.map} +1 -1
- package/dist/index.d.mts +32 -10
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +66 -1
- package/dist/index.mjs.map +1 -1
- package/dist/scripts/smoke/baseline.mjs +65 -0
- package/dist/scripts/smoke/baseline.mjs.map +1 -1
- package/dist/smoke.mjs +65 -0
- package/dist/smoke.mjs.map +1 -1
- package/dist/verify/index.mjs +1 -1
- package/package.json +1 -1
package/dist/cli/index.mjs
CHANGED
|
@@ -2353,6 +2353,10 @@ function extractRequestBody(body, op, inlineModels) {
|
|
|
2353
2353
|
return {};
|
|
2354
2354
|
}
|
|
2355
2355
|
const contextName = cleanSchemaName(op?.operationId ? toPascalCase(normalizeOperationIdForNaming(op.operationId)) + "Request" : "RequestBody");
|
|
2356
|
+
if (schema.oneOf && schema.discriminator) {
|
|
2357
|
+
const merged = flattenDiscriminatedUnionBody(schema);
|
|
2358
|
+
if (merged) schema = merged;
|
|
2359
|
+
}
|
|
2356
2360
|
if (schema.properties && (schema.type === "object" || !schema.type)) {
|
|
2357
2361
|
const requiredSet = new Set(schema.required ?? []);
|
|
2358
2362
|
const fields = [];
|
|
@@ -2418,6 +2422,67 @@ function extractRequestBody(body, op, inlineModels) {
|
|
|
2418
2422
|
encoding
|
|
2419
2423
|
};
|
|
2420
2424
|
}
|
|
2425
|
+
/**
|
|
2426
|
+
* Fold a discriminated-union request body into the equivalent single object so
|
|
2427
|
+
* its fields can be spread into method arguments instead of forcing callers to
|
|
2428
|
+
* construct an opaque union.
|
|
2429
|
+
*
|
|
2430
|
+
* The result is the union of every variant's properties: the discriminator is
|
|
2431
|
+
* collapsed to an enum of the pinned const values, and any field that isn't
|
|
2432
|
+
* present-and-required in *every* variant becomes optional (it varies by
|
|
2433
|
+
* branch, so it can't be unconditionally required). The discriminator itself is
|
|
2434
|
+
* always required. This mirrors what a spec author would write by hand to model
|
|
2435
|
+
* the same shape as a flat object.
|
|
2436
|
+
*
|
|
2437
|
+
* Returns null — leaving the union intact — unless the body is a clean
|
|
2438
|
+
* inline-object discriminated union: a variant that is a `$ref`, a non-object,
|
|
2439
|
+
* or that doesn't pin the discriminator to a string const can't be safely
|
|
2440
|
+
* merged. Gating on an *explicit* `discriminator` keeps undiscriminated `oneOf`
|
|
2441
|
+
* bodies untouched.
|
|
2442
|
+
*/
|
|
2443
|
+
function flattenDiscriminatedUnionBody(schema) {
|
|
2444
|
+
const discProp = schema.discriminator?.propertyName;
|
|
2445
|
+
if (!discProp || !Array.isArray(schema.oneOf) || schema.oneOf.length === 0) return null;
|
|
2446
|
+
const variants = schema.oneOf;
|
|
2447
|
+
const discValues = [];
|
|
2448
|
+
let discPropSchema;
|
|
2449
|
+
for (const variant of variants) {
|
|
2450
|
+
if (variant.$ref || !variant.properties || !(variant.type === "object" || variant.type === void 0)) return null;
|
|
2451
|
+
const dp = variant.properties[discProp];
|
|
2452
|
+
const constVal = typeof dp?.const === "string" ? dp.const : Array.isArray(dp?.enum) && dp.enum.length === 1 && typeof dp.enum[0] === "string" ? dp.enum[0] : void 0;
|
|
2453
|
+
if (constVal === void 0) return null;
|
|
2454
|
+
if (!discValues.includes(constVal)) discValues.push(constVal);
|
|
2455
|
+
if (!discPropSchema) discPropSchema = dp;
|
|
2456
|
+
}
|
|
2457
|
+
const variantCount = variants.length;
|
|
2458
|
+
const mergedProps = {};
|
|
2459
|
+
const requiredCounts = {};
|
|
2460
|
+
const order = [discProp];
|
|
2461
|
+
for (const variant of variants) {
|
|
2462
|
+
const required = new Set(variant.required ?? []);
|
|
2463
|
+
for (const [name, propSchema] of Object.entries(variant.properties)) {
|
|
2464
|
+
if (name === discProp || !propSchema) continue;
|
|
2465
|
+
if (!(name in mergedProps)) {
|
|
2466
|
+
mergedProps[name] = propSchema;
|
|
2467
|
+
order.push(name);
|
|
2468
|
+
}
|
|
2469
|
+
if (required.has(name)) requiredCounts[name] = (requiredCounts[name] ?? 0) + 1;
|
|
2470
|
+
}
|
|
2471
|
+
}
|
|
2472
|
+
mergedProps[discProp] = {
|
|
2473
|
+
type: "string",
|
|
2474
|
+
enum: discValues,
|
|
2475
|
+
...discPropSchema?.description ? { description: discPropSchema.description } : {}
|
|
2476
|
+
};
|
|
2477
|
+
const properties = {};
|
|
2478
|
+
for (const name of order) properties[name] = mergedProps[name];
|
|
2479
|
+
return {
|
|
2480
|
+
type: "object",
|
|
2481
|
+
properties,
|
|
2482
|
+
required: order.filter((name) => name === discProp || requiredCounts[name] === variantCount),
|
|
2483
|
+
...schema.description ? { description: schema.description } : {}
|
|
2484
|
+
};
|
|
2485
|
+
}
|
|
2421
2486
|
/** Derive a unique variant name for a oneOf inline model. */
|
|
2422
2487
|
function deriveOneOfVariantName(variant, baseName, existingModels) {
|
|
2423
2488
|
if (variant.properties) {
|
|
@@ -5714,6 +5779,58 @@ function resolveResponseModelName(op) {
|
|
|
5714
5779
|
return null;
|
|
5715
5780
|
}
|
|
5716
5781
|
//#endregion
|
|
5782
|
+
//#region src/engine/scoped-services.ts
|
|
5783
|
+
/**
|
|
5784
|
+
* Validate a `--services` selection and return it as a set of POST-MOUNT service
|
|
5785
|
+
* names for emitters to scope on.
|
|
5786
|
+
*
|
|
5787
|
+
* Scoped generation does NOT filter the IR: model placement, dedup/aliasing, and
|
|
5788
|
+
* shared-schema decisions are global computations over the full service/model set
|
|
5789
|
+
* (filtering them churns shared `common/` files). Instead the full spec flows to
|
|
5790
|
+
* the emitters unchanged and this set is passed as `ctx.scopedServices`; each
|
|
5791
|
+
* emitter gates only its per-service resource/test emission on it.
|
|
5792
|
+
*
|
|
5793
|
+
* Matching is on the POST-MOUNT name. Mount resolution never mutates
|
|
5794
|
+
* `spec.services` — the post-mount target lives on `ResolvedOperation.mountOn`
|
|
5795
|
+
* (`mountOn = hint?.mountOn ?? mountRules[service.name] ?? service.name`) — so the
|
|
5796
|
+
* post-mount name of a source service is derived here as
|
|
5797
|
+
* `mountRules[service.name] ?? service.name`. Selecting a target (e.g.
|
|
5798
|
+
* `DirectorySync`) is valid because every source tag that mounts into it
|
|
5799
|
+
* (`DirectoryUsers`, `DirectoryGroups`, …) shares that post-mount name, and the
|
|
5800
|
+
* emitters group resources by mount, so all siblings emit together.
|
|
5801
|
+
*
|
|
5802
|
+
* @param spec - The parsed API spec (never mutated; never filtered).
|
|
5803
|
+
* @param selected - Post-mount service names from `--services` (normalized/trimmed).
|
|
5804
|
+
* @param mountRules - Source-service-name → post-mount-target mappings.
|
|
5805
|
+
* @returns The validated selection as a `Set` of post-mount names.
|
|
5806
|
+
* @throws A `ConfigError` listing valid post-mount names when a selection is
|
|
5807
|
+
* unknown — scoped generation never silently emits the wrong set.
|
|
5808
|
+
*/
|
|
5809
|
+
/**
|
|
5810
|
+
* Canonicalize a service name to a casing/separator-insensitive key so that
|
|
5811
|
+
* `MultiFactorAuth` (post-mount IR name), `multi_factor_auth` (snake-cased
|
|
5812
|
+
* manifest record), and `multiFactorAuth` (camel-cased manifest record) all
|
|
5813
|
+
* collapse to `multifactorauth`. Used to match a spec service against the
|
|
5814
|
+
* services a prior manifest recorded as already on disk, where the casing of
|
|
5815
|
+
* the recorded `service` field varies per emitter.
|
|
5816
|
+
*/
|
|
5817
|
+
function canonicalServiceKey(name) {
|
|
5818
|
+
return name.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
5819
|
+
}
|
|
5820
|
+
function resolveScopedServices(spec, selected, mountRules) {
|
|
5821
|
+
const mounts = mountRules ?? {};
|
|
5822
|
+
const postMountName = (name) => mounts[name] ?? name;
|
|
5823
|
+
const validPostMount = new Set(spec.services.map((s) => postMountName(s.name)));
|
|
5824
|
+
const selectedSet = new Set(selected);
|
|
5825
|
+
const unknown = [...selectedSet].filter((s) => !validPostMount.has(s));
|
|
5826
|
+
if (unknown.length > 0) {
|
|
5827
|
+
const err = /* @__PURE__ */ new Error(`Unknown --services: ${unknown.sort().join(", ")}. Valid post-mount services: ${[...validPostMount].sort().join(", ")}`);
|
|
5828
|
+
err.name = "ConfigError";
|
|
5829
|
+
throw err;
|
|
5830
|
+
}
|
|
5831
|
+
return selectedSet;
|
|
5832
|
+
}
|
|
5833
|
+
//#endregion
|
|
5717
5834
|
//#region src/engine/generate-files.ts
|
|
5718
5835
|
function buildEmitterContext(spec, options) {
|
|
5719
5836
|
const resolvedOperations = resolveOperations(spec, options.operationHints, options.mountRules);
|
|
@@ -5738,11 +5855,32 @@ function buildEmitterContext(spec, options) {
|
|
|
5738
5855
|
targetDir: options.target,
|
|
5739
5856
|
priorTargetManifestPaths: options.priorTargetManifestPaths,
|
|
5740
5857
|
scopedServices: options.scopedServices,
|
|
5858
|
+
presentServiceKeys: options.presentServiceKeys,
|
|
5741
5859
|
scopedModelNames,
|
|
5742
5860
|
scopedEnumNames
|
|
5743
5861
|
};
|
|
5744
5862
|
}
|
|
5745
5863
|
/**
|
|
5864
|
+
* The services an aggregate (barrel/client/test) generator may reference in a
|
|
5865
|
+
* scoped run: the selected services plus every service the prior manifest
|
|
5866
|
+
* recorded as already on disk. A service the spec just added that is neither
|
|
5867
|
+
* selected nor already generated is excluded — otherwise the client/barrels
|
|
5868
|
+
* would import a resource/model whose file this run does not emit (the orphaned
|
|
5869
|
+
* `agents/__init__.py → ._resource` class of build break).
|
|
5870
|
+
*
|
|
5871
|
+
* Outside a scoped run (full generation), every service is in surface.
|
|
5872
|
+
*/
|
|
5873
|
+
function aggregateSurfaceServices(spec, ctx) {
|
|
5874
|
+
const scope = ctx.scopedServices;
|
|
5875
|
+
if (!scope || scope.size === 0) return spec.services;
|
|
5876
|
+
const present = ctx.presentServiceKeys ?? /* @__PURE__ */ new Set();
|
|
5877
|
+
const postMountOf = (service) => ctx.resolvedOperations?.find((r) => r.service.name === service.name)?.mountOn ?? service.name;
|
|
5878
|
+
return spec.services.filter((s) => {
|
|
5879
|
+
const postMount = postMountOf(s);
|
|
5880
|
+
return scope.has(postMount) || present.has(canonicalServiceKey(postMount));
|
|
5881
|
+
});
|
|
5882
|
+
}
|
|
5883
|
+
/**
|
|
5746
5884
|
* Collect model and enum names transitively referenced by service operations.
|
|
5747
5885
|
* Walks operation parameters, request bodies, responses, and pagination item
|
|
5748
5886
|
* types, then chases model field references until the set stabilizes.
|
|
@@ -5807,27 +5945,38 @@ function isDiscriminatedModel(model) {
|
|
|
5807
5945
|
/**
|
|
5808
5946
|
* Collect all generated files from an emitter (no headers, no path prefixes).
|
|
5809
5947
|
*
|
|
5810
|
-
*
|
|
5811
|
-
*
|
|
5812
|
-
*
|
|
5813
|
-
* selected service is wired into the client automatically)
|
|
5814
|
-
*
|
|
5815
|
-
*
|
|
5948
|
+
* A full run emits over the whole spec. A scoped (`--services`) run still emits
|
|
5949
|
+
* model placement, dedup, the root client, and aggregate/barrel files
|
|
5950
|
+
* byte-identically for every service ALREADY on disk (plus the brand-new
|
|
5951
|
+
* selected service, which is wired into the client automatically) — but it
|
|
5952
|
+
* narrows that surface to `selected ∪ already-on-disk` services
|
|
5953
|
+
* (`aggregateSurfaceServices`). A service the spec just added that is neither
|
|
5954
|
+
* selected nor already generated is dropped from the surface entirely, so its
|
|
5955
|
+
* models/discriminators are not pulled into reachability and barrels/the client
|
|
5956
|
+
* never import a resource this run does not emit. The emitters additionally
|
|
5957
|
+
* consult `ctx.scopedServices` to emit only the selected services' per-service
|
|
5958
|
+
* resource/test files.
|
|
5816
5959
|
*/
|
|
5817
5960
|
function generateAllFiles(spec, emitter, ctx) {
|
|
5818
|
-
const
|
|
5961
|
+
const scoped = !!ctx.scopedServices && ctx.scopedServices.size > 0;
|
|
5962
|
+
const surfaceServices = aggregateSurfaceServices(spec, ctx);
|
|
5963
|
+
const referenced = collectReferencedNames(surfaceServices, spec.models, { preserveAllDiscriminated: !scoped });
|
|
5819
5964
|
const reachableModels = spec.models.filter((m) => referenced.models.has(m.name));
|
|
5820
5965
|
const reachableEnums = spec.enums.filter((e) => referenced.enums.has(e.name));
|
|
5821
|
-
const
|
|
5966
|
+
const surfaceSpec = {
|
|
5822
5967
|
...spec,
|
|
5968
|
+
services: surfaceServices
|
|
5969
|
+
};
|
|
5970
|
+
const reachableSpec = {
|
|
5971
|
+
...surfaceSpec,
|
|
5823
5972
|
models: reachableModels,
|
|
5824
5973
|
enums: reachableEnums
|
|
5825
5974
|
};
|
|
5826
5975
|
return [
|
|
5827
5976
|
...emitter.generateModels(reachableModels, ctx),
|
|
5828
5977
|
...emitter.generateEnums(reachableEnums, ctx),
|
|
5829
|
-
...emitter.generateResources(
|
|
5830
|
-
...emitter.generateClient(
|
|
5978
|
+
...emitter.generateResources(surfaceServices, ctx),
|
|
5979
|
+
...emitter.generateClient(surfaceSpec, ctx),
|
|
5831
5980
|
...emitter.generateErrors(ctx),
|
|
5832
5981
|
...emitter.generateTypeSignatures?.(reachableSpec, ctx) ?? [],
|
|
5833
5982
|
...emitter.generateTests(reachableSpec, ctx)
|
|
@@ -5844,7 +5993,11 @@ function applyFileHeaders(files, header) {
|
|
|
5844
5993
|
function generateFiles(spec, emitter, options) {
|
|
5845
5994
|
const ctx = buildEmitterContext(spec, options);
|
|
5846
5995
|
const files = generateAllFiles(spec, emitter, ctx);
|
|
5847
|
-
const
|
|
5996
|
+
const surfaceSpec = {
|
|
5997
|
+
...spec,
|
|
5998
|
+
services: aggregateSurfaceServices(spec, ctx)
|
|
5999
|
+
};
|
|
6000
|
+
const operations = emitter.buildOperationsMap?.(surfaceSpec, ctx);
|
|
5848
6001
|
const header = emitter.fileHeader();
|
|
5849
6002
|
return {
|
|
5850
6003
|
files: applyFileHeaders(files, header),
|
|
@@ -5854,47 +6007,6 @@ function generateFiles(spec, emitter, options) {
|
|
|
5854
6007
|
};
|
|
5855
6008
|
}
|
|
5856
6009
|
//#endregion
|
|
5857
|
-
//#region src/engine/scoped-services.ts
|
|
5858
|
-
/**
|
|
5859
|
-
* Validate a `--services` selection and return it as a set of POST-MOUNT service
|
|
5860
|
-
* names for emitters to scope on.
|
|
5861
|
-
*
|
|
5862
|
-
* Scoped generation does NOT filter the IR: model placement, dedup/aliasing, and
|
|
5863
|
-
* shared-schema decisions are global computations over the full service/model set
|
|
5864
|
-
* (filtering them churns shared `common/` files). Instead the full spec flows to
|
|
5865
|
-
* the emitters unchanged and this set is passed as `ctx.scopedServices`; each
|
|
5866
|
-
* emitter gates only its per-service resource/test emission on it.
|
|
5867
|
-
*
|
|
5868
|
-
* Matching is on the POST-MOUNT name. Mount resolution never mutates
|
|
5869
|
-
* `spec.services` — the post-mount target lives on `ResolvedOperation.mountOn`
|
|
5870
|
-
* (`mountOn = hint?.mountOn ?? mountRules[service.name] ?? service.name`) — so the
|
|
5871
|
-
* post-mount name of a source service is derived here as
|
|
5872
|
-
* `mountRules[service.name] ?? service.name`. Selecting a target (e.g.
|
|
5873
|
-
* `DirectorySync`) is valid because every source tag that mounts into it
|
|
5874
|
-
* (`DirectoryUsers`, `DirectoryGroups`, …) shares that post-mount name, and the
|
|
5875
|
-
* emitters group resources by mount, so all siblings emit together.
|
|
5876
|
-
*
|
|
5877
|
-
* @param spec - The parsed API spec (never mutated; never filtered).
|
|
5878
|
-
* @param selected - Post-mount service names from `--services` (normalized/trimmed).
|
|
5879
|
-
* @param mountRules - Source-service-name → post-mount-target mappings.
|
|
5880
|
-
* @returns The validated selection as a `Set` of post-mount names.
|
|
5881
|
-
* @throws A `ConfigError` listing valid post-mount names when a selection is
|
|
5882
|
-
* unknown — scoped generation never silently emits the wrong set.
|
|
5883
|
-
*/
|
|
5884
|
-
function resolveScopedServices(spec, selected, mountRules) {
|
|
5885
|
-
const mounts = mountRules ?? {};
|
|
5886
|
-
const postMountName = (name) => mounts[name] ?? name;
|
|
5887
|
-
const validPostMount = new Set(spec.services.map((s) => postMountName(s.name)));
|
|
5888
|
-
const selectedSet = new Set(selected);
|
|
5889
|
-
const unknown = [...selectedSet].filter((s) => !validPostMount.has(s));
|
|
5890
|
-
if (unknown.length > 0) {
|
|
5891
|
-
const err = /* @__PURE__ */ new Error(`Unknown --services: ${unknown.sort().join(", ")}. Valid post-mount services: ${[...validPostMount].sort().join(", ")}`);
|
|
5892
|
-
err.name = "ConfigError";
|
|
5893
|
-
throw err;
|
|
5894
|
-
}
|
|
5895
|
-
return selectedSet;
|
|
5896
|
-
}
|
|
5897
|
-
//#endregion
|
|
5898
6010
|
//#region src/engine/integrate.ts
|
|
5899
6011
|
/**
|
|
5900
6012
|
* Resolve a relative import path against a source file path to get the
|
|
@@ -6206,6 +6318,35 @@ function mergeScopedManifestRecords(prev, scopedFiles, scopedOperations) {
|
|
|
6206
6318
|
};
|
|
6207
6319
|
}
|
|
6208
6320
|
/**
|
|
6321
|
+
* Extract the set of services a prior manifest records as already generated on
|
|
6322
|
+
* disk, as canonical (casing/separator-insensitive) keys.
|
|
6323
|
+
*
|
|
6324
|
+
* The manifest's `operations` map carries each operation's owning `service`
|
|
6325
|
+
* (snake_case for most emitters, camelCase for Kotlin). A scoped (`--services`)
|
|
6326
|
+
* run uses this to tell "service in the spec that is already on disk" apart from
|
|
6327
|
+
* "service the spec just added that this SDK has never generated" — only the
|
|
6328
|
+
* former may be wired into aggregate/barrel/client files alongside the
|
|
6329
|
+
* selection, so a brand-new out-of-scope service is never referenced by files
|
|
6330
|
+
* whose implementation this run does not emit.
|
|
6331
|
+
*
|
|
6332
|
+
* Returns an empty set when the manifest is null or carries no operations
|
|
6333
|
+
* (a legacy manifest with no operations data); callers treat that as "present
|
|
6334
|
+
* set unknown" and fall back to not narrowing the surface.
|
|
6335
|
+
*/
|
|
6336
|
+
function manifestServiceKeys(manifest) {
|
|
6337
|
+
const keys = /* @__PURE__ */ new Set();
|
|
6338
|
+
const ops = manifest?.operations;
|
|
6339
|
+
if (!ops) return keys;
|
|
6340
|
+
for (const value of Object.values(ops)) {
|
|
6341
|
+
const entries = Array.isArray(value) ? value : [value];
|
|
6342
|
+
for (const entry of entries) {
|
|
6343
|
+
const service = entry?.service;
|
|
6344
|
+
if (typeof service === "string" && service.length > 0) keys.add(canonicalServiceKey(service));
|
|
6345
|
+
}
|
|
6346
|
+
}
|
|
6347
|
+
return keys;
|
|
6348
|
+
}
|
|
6349
|
+
/**
|
|
6209
6350
|
* Delete stale files from `dir`.
|
|
6210
6351
|
*
|
|
6211
6352
|
* When `header` is provided, a file is only deleted if its contents start with
|
|
@@ -6280,11 +6421,14 @@ async function generate(spec, emitter, options) {
|
|
|
6280
6421
|
const skipManifestRead = options.noPrune === true && !scoped;
|
|
6281
6422
|
const outputPrevManifestForCtx = skipManifestRead ? null : await readManifest(options.outputDir);
|
|
6282
6423
|
const targetManifestForCtx = options.target && !skipManifestRead ? await readManifest(options.target) : null;
|
|
6283
|
-
const
|
|
6424
|
+
const priorManifestForCtx = targetManifestForCtx ?? outputPrevManifestForCtx;
|
|
6425
|
+
const priorTargetManifestPaths = priorManifestForCtx ? new Set(priorManifestForCtx.files) : void 0;
|
|
6426
|
+
const presentServiceKeys = scoped ? manifestServiceKeys(priorManifestForCtx) : void 0;
|
|
6284
6427
|
const { files: withHeaders, header, operations } = generateFiles(spec, emitter, {
|
|
6285
6428
|
...options,
|
|
6286
6429
|
priorTargetManifestPaths,
|
|
6287
|
-
scopedServices
|
|
6430
|
+
scopedServices,
|
|
6431
|
+
presentServiceKeys
|
|
6288
6432
|
});
|
|
6289
6433
|
if (options.dryRun) {
|
|
6290
6434
|
if (options.target) {
|