@workos/oagen 0.27.0 → 0.28.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.
@@ -7360,14 +7360,107 @@ async function generateCommand(opts) {
7360
7360
  }
7361
7361
  }
7362
7362
  //#endregion
7363
+ //#region src/differ/direction.ts
7364
+ /**
7365
+ * Classify every model in `specs` as request-facing, response-facing, both, or
7366
+ * unknown.
7367
+ *
7368
+ * Pass both the baseline and the candidate spec: a model that was request-facing
7369
+ * in either one keeps its request direction for the whole diff, so a schema that
7370
+ * moves request → response mid-release doesn't get that release's requiredness
7371
+ * changes silently downgraded.
7372
+ *
7373
+ * Models reachable from neither side (unreferenced schemas, or a spec whose
7374
+ * operations weren't supplied) stay `unknown`, which preserves the conservative
7375
+ * pre-direction classification.
7376
+ */
7377
+ function buildDirectionIndex(...specs) {
7378
+ const models = /* @__PURE__ */ new Map();
7379
+ for (const spec of specs) for (const model of spec.models) if (!models.has(model.name)) models.set(model.name, model);
7380
+ const requestSeeds = [];
7381
+ const responseSeeds = [];
7382
+ for (const spec of specs) for (const service of spec.services) for (const op of service.operations) {
7383
+ if (op.requestBody) requestSeeds.push(op.requestBody);
7384
+ for (const param of [
7385
+ ...op.pathParams,
7386
+ ...op.queryParams,
7387
+ ...op.headerParams,
7388
+ ...op.cookieParams ?? []
7389
+ ]) requestSeeds.push(param.type);
7390
+ responseSeeds.push(op.response);
7391
+ for (const success of op.successResponses ?? []) responseSeeds.push(success.type);
7392
+ for (const error of op.errors) if (error.type) responseSeeds.push(error.type);
7393
+ }
7394
+ const request = reachableModels(requestSeeds, models);
7395
+ const response = reachableModels(responseSeeds, models);
7396
+ const index = /* @__PURE__ */ new Map();
7397
+ for (const name of models.keys()) {
7398
+ const inRequest = request.has(name);
7399
+ const inResponse = response.has(name);
7400
+ index.set(name, inRequest && inResponse ? "both" : inRequest ? "request" : inResponse ? "response" : "unknown");
7401
+ }
7402
+ return index;
7403
+ }
7404
+ function directionOf(index, modelName) {
7405
+ return index?.get(modelName) ?? "unknown";
7406
+ }
7407
+ /**
7408
+ * Every model name reachable from `seeds`, following model fields (and
7409
+ * discriminated-union variants) transitively.
7410
+ */
7411
+ function reachableModels(seeds, models) {
7412
+ const found = /* @__PURE__ */ new Set();
7413
+ const queue = [...seeds];
7414
+ while (queue.length > 0) {
7415
+ const ref = queue.pop();
7416
+ switch (ref.kind) {
7417
+ case "model": {
7418
+ if (found.has(ref.name)) break;
7419
+ found.add(ref.name);
7420
+ const model = models.get(ref.name);
7421
+ if (!model) break;
7422
+ for (const field of model.fields) queue.push(field.type);
7423
+ for (const variant of Object.values(model.discriminator?.mapping ?? {})) queue.push({
7424
+ kind: "model",
7425
+ name: variant
7426
+ });
7427
+ break;
7428
+ }
7429
+ case "array":
7430
+ queue.push(ref.items);
7431
+ break;
7432
+ case "nullable":
7433
+ queue.push(ref.inner);
7434
+ break;
7435
+ case "map":
7436
+ queue.push(ref.valueType);
7437
+ break;
7438
+ case "union":
7439
+ queue.push(...ref.variants);
7440
+ break;
7441
+ case "primitive":
7442
+ case "enum":
7443
+ case "literal": break;
7444
+ }
7445
+ }
7446
+ return found;
7447
+ }
7448
+ //#endregion
7363
7449
  //#region src/differ/classify.ts
7364
- function classifyFieldChange(kind, fieldName, isRequired) {
7450
+ function classifyFieldChange(kind, fieldName, isRequired, direction = "unknown") {
7365
7451
  switch (kind) {
7366
- case "field-added": return {
7367
- kind,
7368
- fieldName,
7369
- classification: isRequired ? "breaking" : "additive"
7370
- };
7452
+ case "field-added":
7453
+ if (isRequired && direction === "response") return {
7454
+ kind,
7455
+ fieldName,
7456
+ classification: "additive",
7457
+ details: "required on a response-only model"
7458
+ };
7459
+ return {
7460
+ kind,
7461
+ fieldName,
7462
+ classification: isRequired ? "breaking" : "additive"
7463
+ };
7371
7464
  case "field-removed": return {
7372
7465
  kind,
7373
7466
  fieldName,
@@ -7432,7 +7525,12 @@ function classifyParamChange(kind, paramName, isRequired) {
7432
7525
  }
7433
7526
  //#endregion
7434
7527
  //#region src/differ/models.ts
7435
- function diffModels(oldModels, newModels) {
7528
+ /**
7529
+ * `directions` (from `buildDirectionIndex`) lets requiredness changes be judged
7530
+ * against the side of the wire each model is used on. Omit it and every model is
7531
+ * treated as unknown-direction, i.e. the conservative classification.
7532
+ */
7533
+ function diffModels(oldModels, newModels, directions) {
7436
7534
  const changes = [];
7437
7535
  const oldByName = new Map(oldModels.map((m) => [m.name, m]));
7438
7536
  const newByName = new Map(newModels.map((m) => [m.name, m]));
@@ -7449,7 +7547,7 @@ function diffModels(oldModels, newModels) {
7449
7547
  for (const [name, newModel] of newByName) {
7450
7548
  const oldModel = oldByName.get(name);
7451
7549
  if (!oldModel) continue;
7452
- const fieldChanges = diffFields(oldModel, newModel);
7550
+ const fieldChanges = diffFields(oldModel, newModel, directionOf(directions, name));
7453
7551
  if (fieldChanges.length > 0) {
7454
7552
  const hasBreaking = fieldChanges.some((fc) => fc.classification === "breaking");
7455
7553
  changes.push({
@@ -7462,11 +7560,11 @@ function diffModels(oldModels, newModels) {
7462
7560
  }
7463
7561
  return changes;
7464
7562
  }
7465
- function diffFields(oldModel, newModel) {
7563
+ function diffFields(oldModel, newModel, direction = "unknown") {
7466
7564
  const changes = [];
7467
7565
  const oldByName = new Map(oldModel.fields.map((f) => [f.name, f]));
7468
7566
  const newByName = new Map(newModel.fields.map((f) => [f.name, f]));
7469
- for (const [name, field] of newByName) if (!oldByName.has(name)) changes.push(classifyFieldChange("field-added", name, field.required));
7567
+ for (const [name, field] of newByName) if (!oldByName.has(name)) changes.push(classifyFieldChange("field-added", name, field.required, direction));
7470
7568
  for (const [name] of oldByName) if (!newByName.has(name)) changes.push(classifyFieldChange("field-removed", name));
7471
7569
  for (const [name, newField] of newByName) {
7472
7570
  const oldField = oldByName.get(name);
@@ -7681,8 +7779,9 @@ function diffServices(oldServices, newServices) {
7681
7779
  //#endregion
7682
7780
  //#region src/differ/diff.ts
7683
7781
  function diffSpecs(oldSpec, newSpec) {
7782
+ const directions = buildDirectionIndex(oldSpec, newSpec);
7684
7783
  const changes = [
7685
- ...diffModels(oldSpec.models, newSpec.models),
7784
+ ...diffModels(oldSpec.models, newSpec.models, directions),
7686
7785
  ...diffEnums(oldSpec.enums, newSpec.enums),
7687
7786
  ...diffServices(oldSpec.services, newSpec.services)
7688
7787
  ];