@orval/mock 8.22.0 → 8.24.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/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { EnumGeneration, OutputMockType, OutputMode, PropertySortOrder, camel, compareVersions, escapeRegExp, generalJSTypesWithArray, generateDependencyImports, getKey, getOperationTagKey, getRefInfo, isBoolean, isFunction, isMswMock, isNumber, isObject, isReference, isSchema, isString, jsStringLiteralEscape, mergeDeep, pascal, resolveRef, sanitize, stringify } from "@orval/core";
1
+ import { EnumGeneration, OutputMockType, OutputMode, PropertySortOrder, camelPathParamName, compareVersions, escapeRegExp, generalJSTypesWithArray, generateDependencyImports, getKey, getOperationTagKey, getRefInfo, isBoolean, isFunction, isMswMock, isNumber, isObject, isReference, isString, jsStringLiteralEscape, mergeDeep, pascal, resolveRef, stringify, toColonRoutePath } from "@orval/core";
2
2
  import { prop } from "remeda";
3
3
  //#region src/mock-types.ts
4
4
  function isStrictMock(mockOptions) {
@@ -265,6 +265,43 @@ const getDelay = (override, options) => {
265
265
  return false;
266
266
  };
267
267
  //#endregion
268
+ //#region src/faker/getters/all-of-required.ts
269
+ function derefAllOfMember(member, context, seen) {
270
+ let current = member;
271
+ while (current && typeof current === "object" && isReference(current)) {
272
+ const ref = current.$ref;
273
+ if (typeof ref !== "string" || seen.has(ref)) return;
274
+ seen.add(ref);
275
+ const { refPaths } = getRefInfo(ref, context);
276
+ current = Array.isArray(refPaths) ? prop(context.spec, ...refPaths) : void 0;
277
+ }
278
+ return current && typeof current === "object" ? current : void 0;
279
+ }
280
+ function collectAllOfRequiredWithDeclared(schemas, context, seen) {
281
+ const required = [];
282
+ const declared = /* @__PURE__ */ new Set();
283
+ for (const val of schemas) {
284
+ const memberSeen = new Set(seen);
285
+ const schema = derefAllOfMember(val, context, memberSeen);
286
+ if (!schema) continue;
287
+ const properties = schema.properties;
288
+ if (properties && typeof properties === "object") for (const key of Object.keys(properties)) declared.add(key);
289
+ if (Array.isArray(schema.required)) required.push(...schema.required);
290
+ if (Array.isArray(schema.allOf)) {
291
+ const inner = collectAllOfRequiredWithDeclared(schema.allOf, context, memberSeen);
292
+ required.push(...inner.required.filter((name) => inner.declared.has(name) || properties && name in properties));
293
+ for (const key of inner.declared) declared.add(key);
294
+ }
295
+ }
296
+ return {
297
+ required,
298
+ declared
299
+ };
300
+ }
301
+ function collectAllOfRequired(schemas, context) {
302
+ return collectAllOfRequiredWithDeclared(schemas, context, /* @__PURE__ */ new Set()).required;
303
+ }
304
+ //#endregion
268
305
  //#region src/faker/compatible-v9.ts
269
306
  const getFakerPackageVersion = (packageJson) => {
270
307
  return packageJson.resolvedVersions?.["@faker-js/faker"] ?? packageJson.dependencies?.["@faker-js/faker"] ?? packageJson.devDependencies?.["@faker-js/faker"] ?? packageJson.peerDependencies?.["@faker-js/faker"];
@@ -316,6 +353,20 @@ function getReferenceName$1(ref, context) {
316
353
  if (!ref) return "";
317
354
  return getRefInfo(ref, context).name;
318
355
  }
356
+ function isNullableRefTarget(ref, context) {
357
+ return isNullableSchema(resolveRefTarget(ref, context));
358
+ }
359
+ function reExpansionWouldCollapse(ref, context, existingReferencedProperties, nonNullable) {
360
+ const target = resolveRefTarget(ref, context);
361
+ const targetProperties = target?.properties;
362
+ const targetRequired = target?.required;
363
+ if (!targetProperties || !Array.isArray(targetRequired)) return false;
364
+ return Object.entries(targetProperties).some(([key, property]) => {
365
+ if (!targetRequired.includes(key) || !isReference(property)) return false;
366
+ if (!existingReferencedProperties.includes(getReferenceName$1(property.$ref, context))) return false;
367
+ return nonNullable || !isNullableRefTarget(property.$ref, context);
368
+ });
369
+ }
319
370
  function getMockObject({ item, mockOptions, operationId, tags, combine, context, imports, existingReferencedProperties, existingReferencedAllOfRefs = [], splitMockImplementations, allowOverride = false }) {
320
371
  if (isReference(item)) return resolveMockValue({
321
372
  schema: {
@@ -421,9 +472,16 @@ function getMockObject({ item, mockOptions, operationId, tags, combine, context,
421
472
  if (combine?.includedProperties.includes(key)) return;
422
473
  const isRequired = mockOptions?.required ?? (Array.isArray(itemRequired) ? itemRequired : []).includes(key);
423
474
  const hasNullable = "nullable" in prop && prop.nullable === true;
424
- if (isReference(prop) && existingReferencedProperties.includes(getReferenceName$1(prop.$ref, context))) {
425
- if (isRequired) return `${getKey(key)}: null`;
426
- return;
475
+ const refName = isReference(prop) ? getReferenceName$1(prop.$ref, context) : "";
476
+ const isRecursiveRef = !!refName && existingReferencedProperties.includes(refName);
477
+ if (isRecursiveRef) {
478
+ if (!isRequired) return;
479
+ const keyDefinition = getKey(key);
480
+ if (!mockOptions?.nonNullable && (hasNullable || isReference(prop) && isNullableRefTarget(prop.$ref, context))) return `${keyDefinition}: null`;
481
+ if (new Set(existingReferencedProperties).size !== existingReferencedProperties.length || isReference(prop) && reExpansionWouldCollapse(prop.$ref, context, existingReferencedProperties, mockOptions?.nonNullable)) {
482
+ imports.push({ name: refName });
483
+ return `${keyDefinition}: {} as unknown as ${refName}`;
484
+ }
427
485
  }
428
486
  const importsBefore = imports.length;
429
487
  const resolvedValue = resolveMockValue({
@@ -445,6 +503,14 @@ function getMockObject({ item, mockOptions, operationId, tags, combine, context,
445
503
  mergeReturnedMockImports(imports, importsBefore, resolvedValue.imports);
446
504
  includedProperties.push(key);
447
505
  const keyDefinition = getKey(key);
506
+ if (isRequired && refName && resolvedValue.value === "undefined") {
507
+ imports.push({ name: refName });
508
+ return `${keyDefinition}: undefined as unknown as ${refName}`;
509
+ }
510
+ if (isRequired && isRecursiveRef && resolvedValue.value.includes(" as unknown as ")) {
511
+ imports.push({ name: refName });
512
+ return `${keyDefinition}: {} as unknown as ${refName}`;
513
+ }
448
514
  const hasDefault = "default" in prop && prop.default !== void 0;
449
515
  if (!isRequired && !resolvedValue.overrided && !hasDefault) {
450
516
  const omitValue = mockOptions?.nonNullable || !hasNullable ? "undefined" : "null";
@@ -521,14 +587,16 @@ function getMockObject({ item, mockOptions, operationId, tags, combine, context,
521
587
  //#region src/faker/getters/array-item-factory.ts
522
588
  /**
523
589
  * Scope key for file-level array-item factory dedup. Must match how writers
524
- * group mock output: one bucket per tag file in tags modes, otherwise one
525
- * bucket for the whole target.
590
+ * group mock output: one bucket per tag file in tags modes, one bucket per
591
+ * operation file in the tags-operations modes (each operation gets its own
592
+ * mock file), otherwise one bucket for the whole target.
526
593
  */
527
- function getArrayItemMockFileScope(context, tags) {
594
+ function getArrayItemMockFileScope(context, tags, operationId) {
528
595
  const mode = context.output.mode;
529
596
  const mockType = context.activeMockOutputType ?? OutputMockType.MSW;
530
597
  let base;
531
- if (mode === OutputMode.TAGS || mode === OutputMode.TAGS_SPLIT) base = `tag:${getOperationTagKey({ tags })}`;
598
+ if (mode === OutputMode.TAGS_OPERATIONS || mode === OutputMode.TAGS_OPERATIONS_SPLIT) base = `operation:${operationId ?? ""}`;
599
+ else if (mode === OutputMode.TAGS || mode === OutputMode.TAGS_SPLIT) base = `tag:${getOperationTagKey({ tags })}`;
532
600
  else if (mode === OutputMode.SPLIT) base = "split";
533
601
  else base = "single";
534
602
  return `${base}:${mockType}`;
@@ -657,7 +725,7 @@ function extractArrayItemMock({ items, propertyName, parentName, operationId, ta
657
725
  });
658
726
  if (!names) return;
659
727
  const { factoryName, typeName } = names;
660
- const fileLevelFactories = getFileLevelExtractedFactories(context, getArrayItemMockFileScope(context, tags));
728
+ const fileLevelFactories = getFileLevelExtractedFactories(context, getArrayItemMockFileScope(context, tags, operationId));
661
729
  const mockOptions = context.output.override.mock;
662
730
  if (!(fileLevelFactories.has(factoryName) || splitMockImplementations.some((f) => f.includes(`export const ${factoryName}`)))) {
663
731
  const { param, returnType, returnCast } = getMockFactorySignatureParts(typeName, mockOptions, {
@@ -1071,17 +1139,19 @@ function getItemType(item) {
1071
1139
  function getEnum(item, imports, context, existingReferencedProperties, type) {
1072
1140
  if (!item.enum) return "";
1073
1141
  let enumValue = `[${item.enum.filter((e) => e !== null).map((e) => type === "string" || type === void 0 && isString(e) ? `'${jsStringLiteralEscape(e)}'` : e).join(",")}]`;
1074
- if (context.output.override.enumGenerationType === EnumGeneration.ENUM) if (item.isRef || existingReferencedProperties.length === 0) {
1075
- enumValue += ` as ${item.name}${item.name.endsWith("[]") ? "" : "[]"}`;
1076
- imports.push({ name: item.name });
1077
- } else {
1078
- const parentReference = existingReferencedProperties.at(-1);
1079
- if (!parentReference) return "";
1080
- enumValue += ` as ${parentReference}['${item.name}']`;
1081
- if (!item.path?.endsWith("[]")) enumValue += "[]";
1082
- imports.push({ name: parentReference });
1083
- }
1084
- else enumValue += " as const";
1142
+ if (context.output.override.enumGenerationType === EnumGeneration.ENUM) {
1143
+ const isRootSchema = !item.parentName && existingReferencedProperties.at(-1) === item.name;
1144
+ if (item.isRef || existingReferencedProperties.length === 0 || isRootSchema) {
1145
+ enumValue += ` as ${item.name}${item.name.endsWith("[]") ? "" : "[]"}`;
1146
+ imports.push({ name: item.name });
1147
+ } else {
1148
+ const parentReference = existingReferencedProperties.at(-1);
1149
+ if (!parentReference) return "";
1150
+ enumValue += ` as ${parentReference}['${item.name}']`;
1151
+ if (!item.path?.endsWith("[]")) enumValue += "[]";
1152
+ imports.push({ name: parentReference });
1153
+ }
1154
+ } else enumValue += " as const";
1085
1155
  if (item.isRef && type === "string" && context.output.override.enumGenerationType !== EnumGeneration.UNION) {
1086
1156
  enumValue = `Object.values(${item.name})`;
1087
1157
  imports.push({
@@ -1120,6 +1190,15 @@ function resolveMockOverride(properties = {}, item, nonNullableOption) {
1120
1190
  overrided: true
1121
1191
  };
1122
1192
  }
1193
+ /** Resolves a `$ref` string to its schema in the loaded spec, if any. */
1194
+ function resolveRefTarget(ref, context) {
1195
+ if (typeof ref !== "string") return void 0;
1196
+ const [, fragment] = ref.split("#");
1197
+ if (!fragment) return void 0;
1198
+ const { refPaths } = getRefInfo(ref, context);
1199
+ if (!Array.isArray(refPaths)) return void 0;
1200
+ return prop(context.spec, ...refPaths);
1201
+ }
1123
1202
  /** OpenAPI 3.0 `nullable: true` or 3.1 `type` unions that include `null`. */
1124
1203
  function isNullableSchema(schema) {
1125
1204
  if (!schema || typeof schema !== "object") return false;
@@ -1186,8 +1265,9 @@ function hasOverrideTouchingSchema(schemaProperties, mockOptions, operationId, t
1186
1265
  function resolveMockValue({ schema, mockOptions, operationId, tags, combine, context, imports, existingReferencedProperties, existingReferencedAllOfRefs = [], splitMockImplementations, allowOverride }) {
1187
1266
  if (isReference(schema)) {
1188
1267
  const schemaReference = schema;
1189
- const { name, refPaths } = getRefInfo(typeof schema.$ref === "string" ? schema.$ref : "", context);
1190
- const schemaRef = Array.isArray(refPaths) ? prop(context.spec, ...refPaths) : void 0;
1268
+ const schemaRefPath = typeof schema.$ref === "string" ? schema.$ref : "";
1269
+ const { name, refPaths } = getRefInfo(schemaRefPath, context);
1270
+ const schemaRef = resolveRefTarget(schemaRefPath, context);
1191
1271
  const newSchema = {
1192
1272
  ...schemaRef,
1193
1273
  name,
@@ -1217,7 +1297,9 @@ function resolveMockValue({ schema, mockOptions, operationId, tags, combine, con
1217
1297
  }
1218
1298
  }
1219
1299
  const newSeparator = newSchema.allOf ? "allOf" : newSchema.oneOf ? "oneOf" : "anyOf";
1220
- if (shouldDelegateToSchemaFactories(context) && isComponentsSchemaRef(refPaths) && !hasOverrideTouchingSchema(schemaRef?.properties, mockOptions, operationId, tags, schemaReference.path)) {
1300
+ const targetEffective = schemaRef ? collectAllOfRequiredWithDeclared([schemaRef], context, /* @__PURE__ */ new Set()) : void 0;
1301
+ const delegationDropsRequired = (schemaReference.required ?? []).some((requiredName) => targetEffective?.declared.has(requiredName) && !targetEffective.required.includes(requiredName));
1302
+ if (shouldDelegateToSchemaFactories(context) && isComponentsSchemaRef(refPaths) && !existingReferencedProperties.includes(name) && !delegationDropsRequired && !hasOverrideTouchingSchema(schemaRef?.properties, mockOptions, operationId, tags, schemaReference.path)) {
1221
1303
  const factoryName = `get${pascal(name)}Mock`;
1222
1304
  const factoryImport = {
1223
1305
  name: factoryName,
@@ -1313,8 +1395,7 @@ function resolvesToObjectLike(schema, context, seen = /* @__PURE__ */ new Set())
1313
1395
  if (isReference(schema)) {
1314
1396
  if (typeof schema.$ref !== "string" || seen.has(schema.$ref)) return false;
1315
1397
  seen = new Set(seen).add(schema.$ref);
1316
- const { refPaths } = getRefInfo(schema.$ref, context);
1317
- resolved = Array.isArray(refPaths) ? prop(context.spec, ...refPaths) : void 0;
1398
+ resolved = resolveRefTarget(schema.$ref, context);
1318
1399
  } else resolved = schema;
1319
1400
  if (!resolved) return false;
1320
1401
  if (resolved.type === "object" || resolved.properties || resolved.additionalProperties || resolved.allOf) return true;
@@ -1353,8 +1434,18 @@ function combineSchemasMock({ item, separator, mockOptions, operationId, tags, c
1353
1434
  }
1354
1435
  }
1355
1436
  const hasResolvableProperties = itemEntriesForResolve.some(([key]) => key === "properties");
1437
+ const allRequiredFields = [];
1438
+ if (separator === "allOf") {
1439
+ if (itemRequired) allRequiredFields.push(...itemRequired);
1440
+ allRequiredFields.push(...collectAllOfRequired(separatorItems, context));
1441
+ }
1442
+ const itemSchemaForResolve = Object.fromEntries(itemEntriesForResolve);
1443
+ if (separator === "allOf" && allRequiredFields.length > 0) {
1444
+ const itemResolveRequired = itemSchemaForResolve.required;
1445
+ itemSchemaForResolve.required = [...new Set([...allRequiredFields, ...itemResolveRequired ?? []])];
1446
+ }
1356
1447
  const itemResolvedValue = isRefAndNotExisting || hasResolvableProperties ? resolveMockValue({
1357
- schema: Object.fromEntries(itemEntriesForResolve),
1448
+ schema: itemSchemaForResolve,
1358
1449
  combine: {
1359
1450
  separator: "allOf",
1360
1451
  includedProperties: []
@@ -1371,11 +1462,6 @@ function combineSchemasMock({ item, separator, mockOptions, operationId, tags, c
1371
1462
  includedProperties.push(...itemResolvedValue?.includedProperties ?? []);
1372
1463
  combineImports.push(...itemResolvedValue?.imports ?? []);
1373
1464
  let containsOnlyPrimitiveValues = true;
1374
- const allRequiredFields = [];
1375
- if (separator === "allOf") {
1376
- if (itemRequired) allRequiredFields.push(...itemRequired);
1377
- for (const val of separatorItems) if (isSchema(val) && val.required) allRequiredFields.push(...val.required);
1378
- }
1379
1465
  let value = separator === "allOf" ? "" : "faker.helpers.arrayElement([";
1380
1466
  for (const val of separatorItems) {
1381
1467
  const refName = isReference(val) ? getReferenceName(val.$ref, context) : "";
@@ -1444,34 +1530,7 @@ function combineSchemasMock({ item, separator, mockOptions, operationId, tags, c
1444
1530
  }
1445
1531
  //#endregion
1446
1532
  //#region src/faker/getters/route.ts
1447
- const hasParam = (path) => /[^{]*{[\w*_-]*}.*/.test(path);
1448
- const getRoutePath = (path) => {
1449
- const matches = /([^{]*){?([\w*_-]*)}?(.*)/.exec(path);
1450
- if (!matches?.length) return path;
1451
- const prev = matches[1];
1452
- const param = sanitize(camel(matches[2]), {
1453
- es5keyword: true,
1454
- underscore: true,
1455
- dash: true,
1456
- dot: true
1457
- });
1458
- const next = hasParam(matches[3]) ? getRoutePath(matches[3]) : matches[3];
1459
- return hasParam(path) ? `${prev}:${param}${next}` : `${prev}${param}${next}`;
1460
- };
1461
- const getRouteMSW = (route, baseUrl = "*") => {
1462
- route = route.replaceAll(":", String.raw`\\:`);
1463
- const splittedRoute = route.split("/");
1464
- let resolvedRoute = baseUrl;
1465
- for (const [index, path] of splittedRoute.entries()) {
1466
- if (!path && !index) continue;
1467
- if (!path.includes("{")) {
1468
- resolvedRoute = `${resolvedRoute}/${path}`;
1469
- continue;
1470
- }
1471
- resolvedRoute = `${resolvedRoute}/${getRoutePath(path)}`;
1472
- }
1473
- return resolvedRoute;
1474
- };
1533
+ const getRouteMSW = (route, baseUrl = "*") => `${baseUrl}${toColonRoutePath(route.replaceAll(":", String.raw`\\:`), camelPathParamName)}`;
1475
1534
  //#endregion
1476
1535
  //#region src/msw/mocks.ts
1477
1536
  function getMockPropertiesWithoutFunc(properties, spec) {
@@ -1749,6 +1808,10 @@ function generateDefinition(name, route, getResponseMockFunctionNameBase, handle
1749
1808
  const binaryContentType = (preferredContentTypeMatch && isBinaryLikeContentType(preferredContentTypeMatch) ? preferredContentTypeMatch : contentTypes.find((ct) => isBinaryLikeContentType(ct))) ?? "application/octet-stream";
1750
1809
  const firstTextCt = isExactlyStringReturnType && !!preferredContentTypeMatch && !isTextLikeContentType(preferredContentTypeMatch) && hasTextLikeContentType ? contentTypes.find((ct) => isTextLikeContentType(ct)) : contentTypesByPreference.find((ct) => isTextLikeContentType(ct));
1751
1810
  const textHelper = firstTextCt === "application/xml" || firstTextCt?.endsWith("+xml") ? "xml" : firstTextCt === "text/html" ? "html" : "text";
1811
+ const firstJsonCt = contentTypesByPreference.find((ct) => ct.includes("json"));
1812
+ const textHelperDefaultContentType = textHelper === "xml" ? "text/xml" : textHelper === "html" ? "text/html" : "text/plain";
1813
+ const jsonCtHeaderSuffix = firstJsonCt && firstJsonCt !== "application/json" ? `, headers: { 'Content-Type': '${firstJsonCt}' }` : "";
1814
+ const textCtHeaderSuffix = firstTextCt && firstTextCt !== textHelperDefaultContentType ? `, headers: { 'Content-Type': '${firstTextCt}' }` : "";
1752
1815
  let responseBody;
1753
1816
  let responsePrelude = "";
1754
1817
  if (isReturnHttpResponse) {
@@ -1770,23 +1833,23 @@ function generateDefinition(name, route, getResponseMockFunctionNameBase, handle
1770
1833
  else if (isVoidUnionType) {
1771
1834
  let nonVoidBody;
1772
1835
  if (needsRuntimeContentTypeSwitch) nonVoidBody = `typeof resolvedBody === 'string'
1773
- ? HttpResponse.${textHelper}(resolvedBody, { status: ${statusCode} })
1774
- : HttpResponse.json(resolvedBody, { status: ${statusCode} })`;
1836
+ ? HttpResponse.${textHelper}(resolvedBody, { status: ${statusCode}${textCtHeaderSuffix} })
1837
+ : HttpResponse.json(resolvedBody, { status: ${statusCode}${jsonCtHeaderSuffix} })`;
1775
1838
  else if (isTextResponse && !shouldPreferJsonResponse) nonVoidBody = `HttpResponse.${textHelper}(
1776
1839
  typeof resolvedBody === 'string' ? resolvedBody : JSON.stringify(resolvedBody ?? null),
1777
- { status: ${statusCode} })`;
1778
- else nonVoidBody = `HttpResponse.json(resolvedBody, { status: ${statusCode} })`;
1840
+ { status: ${statusCode}${textCtHeaderSuffix} })`;
1841
+ else nonVoidBody = `HttpResponse.json(resolvedBody, { status: ${statusCode}${jsonCtHeaderSuffix} })`;
1779
1842
  responseBody = `resolvedBody === undefined
1780
1843
  ? new HttpResponse(null, { status: ${noContentStatusCode} })
1781
1844
  : ${nonVoidBody}`;
1782
1845
  } else if (needsRuntimeContentTypeSwitch) responseBody = `typeof resolvedBody === 'string'
1783
- ? HttpResponse.${textHelper}(resolvedBody, { status: ${statusCode} })
1784
- : HttpResponse.json(resolvedBody, { status: ${statusCode} })`;
1846
+ ? HttpResponse.${textHelper}(resolvedBody, { status: ${statusCode}${textCtHeaderSuffix} })
1847
+ : HttpResponse.json(resolvedBody, { status: ${statusCode}${jsonCtHeaderSuffix} })`;
1785
1848
  else if (isTextResponse && !shouldPreferJsonResponse) responseBody = `HttpResponse.${textHelper}(textBody,
1786
- { status: ${statusCode}
1849
+ { status: ${statusCode}${textCtHeaderSuffix}
1787
1850
  })`;
1788
1851
  else responseBody = `HttpResponse.json(${resolvedResponseExpr},
1789
- { status: ${statusCode}
1852
+ { status: ${statusCode}${jsonCtHeaderSuffix}
1790
1853
  })`;
1791
1854
  const infoType = `Parameters<Parameters<typeof http.${verb}>[1]>[0]`;
1792
1855
  const handlerImplementation = `