@orval/query 8.9.1 → 8.10.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
@@ -10,7 +10,10 @@ const normalizeQueryOptions = (queryOptions = {}, outputWorkspace) => {
10
10
  ...queryOptions.useInvalidate ? { useInvalidate: true } : {},
11
11
  ...queryOptions.useSetQueryData ? { useSetQueryData: true } : {},
12
12
  ...queryOptions.useGetQueryData ? { useGetQueryData: true } : {},
13
- ...queryOptions.useQuery ? { useQuery: true } : {},
13
+ ...queryOptions.useQuery === void 0 ? {} : { useQuery: queryOptions.useQuery },
14
+ ...queryOptions.useMutation === void 0 ? {} : { useMutation: queryOptions.useMutation },
15
+ ...queryOptions.useSuspenseQuery ? { useSuspenseQuery: true } : {},
16
+ ...queryOptions.useSuspenseInfiniteQuery ? { useSuspenseInfiniteQuery: true } : {},
14
17
  ...queryOptions.useInfinite ? { useInfinite: true } : {},
15
18
  ...queryOptions.useInfiniteQueryParam ? { useInfiniteQueryParam: queryOptions.useInfiniteQueryParam } : {},
16
19
  ...queryOptions.options ? { options: queryOptions.options } : {},
@@ -901,7 +904,7 @@ const generateQueryOptions = ({ params, options, type, adapter }) => {
901
904
  if (options) return `${queryConfig} ...queryOptions`;
902
905
  return "...queryOptions";
903
906
  }
904
- return `${adapter ? adapter.generateEnabledOption(params, options) : !isObject(options) || !Object.hasOwn(options, "enabled") ? `enabled: !!(${params.map(({ name }) => name).join(" && ")}),` : ""}${queryConfig} ...queryOptions`;
907
+ return `${adapter ? adapter.generateEnabledOption(params, options) : !isObject(options) || !Object.hasOwn(options, "enabled") ? `enabled: ${params.map(({ name }) => `${name} != null`).join(" && ")},` : ""}${queryConfig} ...queryOptions`;
905
908
  };
906
909
  const isSuspenseQuery = (type) => {
907
910
  return [QueryType.SUSPENSE_INFINITE, QueryType.SUSPENSE_QUERY].includes(type);
@@ -1328,7 +1331,8 @@ const createVueAdapter = ({ hasVueQueryV4, hasQueryV5, hasQueryV5WithDataTagErro
1328
1331
  return vueUnRefParams(props.filter((prop) => prop.type === GetterPropType.NAMED_PATH_PARAMS));
1329
1332
  },
1330
1333
  generateEnabledOption(params, options) {
1331
- if (!isObject(options) || !Object.hasOwn(options, "enabled")) return `enabled: computed(() => !!(${params.map(({ name }) => `unref(${name})`).join(" && ")})),`;
1334
+ if (params.length === 0) return "";
1335
+ if (!isObject(options) || !Object.hasOwn(options, "enabled")) return `enabled: computed(() => ${params.map(({ name }) => `unref(${name}) !== null && unref(${name}) !== undefined`).join(" && ")}),`;
1332
1336
  return "";
1333
1337
  },
1334
1338
  getQueryKeyPrefix() {
@@ -1378,7 +1382,8 @@ const withDefaults = (adapter) => ({
1378
1382
  return `\`${route}\``;
1379
1383
  },
1380
1384
  generateEnabledOption(params, options) {
1381
- if (!isObject(options) || !Object.hasOwn(options, "enabled")) return `enabled: !!(${params.map(({ name }) => name).join(" && ")}),`;
1385
+ if (params.length === 0) return "";
1386
+ if (!isObject(options) || !Object.hasOwn(options, "enabled")) return `enabled: ${params.map(({ name }) => `${name} !== null && ${name} !== undefined`).join(" && ")},`;
1382
1387
  return "";
1383
1388
  },
1384
1389
  getQueryPropertyForProp(prop, body) {
@@ -1545,10 +1550,12 @@ const findOperationInfo = (spec, operationName) => {
1545
1550
  if (opId !== operationName && camel(opId) !== operationName) continue;
1546
1551
  if (!routePath.includes("{")) return {
1547
1552
  route: routePath,
1553
+ method,
1548
1554
  hasRequiredPathParams: false
1549
1555
  };
1550
1556
  return {
1551
1557
  route: routePath,
1558
+ method,
1552
1559
  hasRequiredPathParams: [...Array.isArray(pathItem.parameters) ? pathItem.parameters : [], ...Array.isArray(operation.parameters) ? operation.parameters : []].filter((p) => p.in === "path").some((p) => p.schema?.default === void 0 && p.default === void 0)
1553
1560
  };
1554
1561
  }
@@ -1597,7 +1604,7 @@ const generateParamArgs = (params) => {
1597
1604
  * Create a generateInvalidateCall function that has access to the OpenAPI spec
1598
1605
  * for intelligent route-based invalidation when params are not specified.
1599
1606
  */
1600
- const createGenerateInvalidateCall = (spec, shouldSplitQueryKey) => {
1607
+ const createGenerateInvalidateCall = (spec, shouldSplitQueryKey, useOperationIdAsQueryKey) => {
1601
1608
  return (target) => {
1602
1609
  const method = target.invalidateMode === "reset" ? "resetQueries" : "invalidateQueries";
1603
1610
  const queryKeyFn = camel(`get-${target.query}-query-key`);
@@ -1606,7 +1613,15 @@ const createGenerateInvalidateCall = (spec, shouldSplitQueryKey) => {
1606
1613
  if (info?.hasRequiredPathParams) {
1607
1614
  const prefix = getStaticRoutePrefix(info.route);
1608
1615
  if (prefix !== void 0) {
1609
- if (shouldSplitQueryKey) return ` queryClient.${method}({ queryKey: [${prefix.split("/").filter((s) => s !== "").map((s) => `'${s}'`).join(", ")}] });`;
1616
+ const verbPrefix = getQueryKeyVerbPrefix({
1617
+ verb: info.method,
1618
+ useOperationIdAsQueryKey
1619
+ });
1620
+ if (shouldSplitQueryKey) {
1621
+ const segments = prefix.split("/").filter((s) => s !== "").map((s) => `'${s}'`).join(", ");
1622
+ return ` queryClient.${method}({ queryKey: ${verbPrefix ? `['${verbPrefix}', ${segments}]` : `[${segments}]`} });`;
1623
+ }
1624
+ if (verbPrefix) return ` queryClient.${method}({ predicate: (query) => query.queryKey[0] === '${verbPrefix}' && typeof query.queryKey[1] === 'string' && query.queryKey[1].startsWith('${prefix}') });`;
1610
1625
  return ` queryClient.${method}({ predicate: (query) => typeof query.queryKey[0] === 'string' && query.queryKey[0].startsWith('${prefix}') });`;
1611
1626
  }
1612
1627
  }
@@ -1688,7 +1703,7 @@ ${hasInvalidation ? adapter.generateMutationOnSuccess({
1688
1703
  operationName,
1689
1704
  definitions,
1690
1705
  isRequestOptions,
1691
- generateInvalidateCall: createGenerateInvalidateCall(context.spec, !!query.shouldSplitQueryKey),
1706
+ generateInvalidateCall: createGenerateInvalidateCall(context.spec, !!query.shouldSplitQueryKey, !!query.useOperationIdAsQueryKey),
1692
1707
  uniqueInvalidates
1693
1708
  }) : ""}
1694
1709
 
@@ -1757,6 +1772,54 @@ const getMutationInvalidatesConflictWarning = ({ operationName, isMutation, isQu
1757
1772
  if (!mutationInvalidates.find((rule) => rule.onMutations.includes(operationName))) return void 0;
1758
1773
  return `mutationInvalidates rule references '${operationName}', but that operation is generated as a ${isQuery ? "Query hook" : "plain function (no hook)"}, not a Mutation. The invalidation will not fire. Either remove '${operationName}' from the rule's onMutations list, or configure '${operationName}' so that it is generated as a Mutation hook.`;
1759
1774
  };
1775
+ const escapeRegExpMetaChars = (value) => value.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`);
1776
+ /**
1777
+ * Wraps the body parameter's type in a property string with the mutator's
1778
+ * `BodyType<T>` envelope so that user-facing Query helpers (hook signature,
1779
+ * `getXxxQueryOptions`, `getXxxQueryKey`, prefetch / invalidate / set+get
1780
+ * QueryData) match the request function's signature, which is already
1781
+ * wrapped by `client.ts`. Without this, callers that pass a plain body to
1782
+ * a non-GET Query hook (possible after #2376 routes non-GET verbs to
1783
+ * Query hooks) would hit a type mismatch against the underlying request
1784
+ * function.
1785
+ *
1786
+ * The pattern handles three prop shapes that the various
1787
+ * `toObjectString(props, ...)` callers can emit:
1788
+ * - `name: T` — required body
1789
+ * - `name?: T` — optional body
1790
+ * - `name: undefined | T` — `definedInitialData` overload transform
1791
+ *
1792
+ * `body.definition` is fully regex-escaped so types containing metachars
1793
+ * (e.g. `Pet[]`, `Foo | Bar`, anonymous object types) are matched
1794
+ * verbatim rather than reinterpreted as regex syntax.
1795
+ *
1796
+ * No-op when the operation has no body or the mutator does not export a
1797
+ * `BodyType<T>` wrapper, so existing GET-only Query keys are unchanged.
1798
+ */
1799
+ const wrapPropsBodyWithMutatorBodyType = ({ propsString, body, mutator }) => {
1800
+ if (!mutator?.bodyTypeName || !body.definition) return propsString;
1801
+ const bodyDefinitionPattern = escapeRegExpMetaChars(body.definition);
1802
+ return propsString.replace(new RegExp(String.raw`(\w+\??:\s*(?:undefined\s*\|\s*)?)${bodyDefinitionPattern}`), `$1${mutator.bodyTypeName}<${body.definition}>`);
1803
+ };
1804
+ /**
1805
+ * Computes a verb prefix segment for query keys when a non-GET operation is
1806
+ * routed to a Query hook. Without this prefix, two operations sharing a path
1807
+ * (e.g. `GET /pets` and `POST /pets`) would generate cache keys that both
1808
+ * begin with `'/pets'`, so TanStack Query would mix their cached data and
1809
+ * `invalidateQueries({ queryKey: ['/pets'] })` would match both.
1810
+ *
1811
+ * Skipped for GET (preserves existing keys) and when
1812
+ * `useOperationIdAsQueryKey` is enabled (operation IDs are already unique
1813
+ * across verb + path, so the prefix would be redundant).
1814
+ *
1815
+ * Returns the uppercased verb when a prefix should be inserted, or
1816
+ * `undefined` when no prefix is needed.
1817
+ */
1818
+ const getQueryKeyVerbPrefix = ({ verb, useOperationIdAsQueryKey }) => {
1819
+ if (useOperationIdAsQueryKey) return void 0;
1820
+ if (verb === Verbs.GET) return void 0;
1821
+ return verb.toUpperCase();
1822
+ };
1760
1823
  const getQueryFnArguments = ({ hasQueryParam, hasSignal, hasSignalParam = false }) => {
1761
1824
  if (!hasQueryParam && !hasSignal) return "";
1762
1825
  const signalDestructure = hasSignalParam ? "signal: querySignal" : "signal";
@@ -1787,20 +1850,32 @@ const generatePrefetch = ({ usePrefetch, type, useQuery, useInfinite, operationN
1787
1850
  return queryClient;
1788
1851
  }\n`;
1789
1852
  };
1790
- const generateQueryImplementation = ({ queryOption: { name, queryParam, options, type, queryKeyFnName }, operationName, queryProperties, queryKeyProperties, queryParams, params, props, mutator, queryOptionsMutator, queryKeyMutator, isRequestOptions, response, httpClient, isExactOptionalPropertyTypes, hasSignal, useRuntimeFetcher, route, doc, usePrefetch, useQuery, useInfinite, useInvalidate, useSetQueryData, useGetQueryData, adapter }) => {
1853
+ const generateQueryImplementation = ({ queryOption: { name, queryParam, options, type, queryKeyFnName }, operationName, queryProperties, queryKeyProperties, queryParams, params, props, body, mutator, queryOptionsMutator, queryKeyMutator, isRequestOptions, response, httpClient, isExactOptionalPropertyTypes, hasSignal, useRuntimeFetcher, route, doc, usePrefetch, useQuery, useInfinite, useInvalidate, useSetQueryData, useGetQueryData, adapter }) => {
1791
1854
  const { hasQueryV5, hasQueryV5WithDataTagError, hasQueryV5WithInfiniteQueryOptionsError } = adapter;
1792
1855
  const hasSignalParam = props.some((prop) => prop.name === "signal");
1793
- const queryPropDefinitions = toObjectString(props, "definition");
1794
- const definedInitialDataQueryPropsDefinitions = toObjectString(props.map((prop) => {
1795
- const regex = new RegExp(String.raw`^${prop.name}\s*\?:`);
1796
- if (!regex.test(prop.definition)) return prop;
1797
- const definitionWithUndefined = prop.definition.replace(regex, `${prop.name}: undefined | `);
1798
- return {
1799
- ...prop,
1800
- definition: definitionWithUndefined
1801
- };
1802
- }), "definition");
1803
- const queryProps = toObjectString(props, "implementation");
1856
+ const queryPropDefinitions = wrapPropsBodyWithMutatorBodyType({
1857
+ propsString: toObjectString(props, "definition"),
1858
+ body,
1859
+ mutator
1860
+ });
1861
+ const definedInitialDataQueryPropsDefinitions = wrapPropsBodyWithMutatorBodyType({
1862
+ propsString: toObjectString(props.map((prop) => {
1863
+ const regex = new RegExp(String.raw`^${prop.name}\s*\?:`);
1864
+ if (!regex.test(prop.definition)) return prop;
1865
+ const definitionWithUndefined = prop.definition.replace(regex, `${prop.name}: undefined | `);
1866
+ return {
1867
+ ...prop,
1868
+ definition: definitionWithUndefined
1869
+ };
1870
+ }), "definition"),
1871
+ body,
1872
+ mutator
1873
+ });
1874
+ const queryProps = wrapPropsBodyWithMutatorBodyType({
1875
+ propsString: toObjectString(props, "implementation"),
1876
+ body,
1877
+ mutator
1878
+ });
1804
1879
  const hasInfiniteQueryParam = queryParam && queryParams?.schema.name;
1805
1880
  const httpFunctionProps = queryParam ? adapter.getInfiniteQueryHttpProps(props, queryParam, !!mutator) : adapter.getHttpFunctionQueryProps(queryProperties, httpClient, !!mutator);
1806
1881
  const definedInitialDataReturnType = adapter.getQueryReturnType({
@@ -1962,7 +2037,11 @@ export function ${queryHookName}<TData = ${TData}, TError = ${errorType}>(\n ${q
1962
2037
  const isReactQuery = adapter.outputClient === OutputClient.REACT_QUERY;
1963
2038
  const setQueryDataFnName = isReactQuery ? camel(`use-set-${name}-query-data`) : camel(`set-${name}-query-data`);
1964
2039
  const setQueryDataKeyExpr = buildBaseQueryKeyExpr();
1965
- const setQueryDataProps = toObjectString(props.filter((prop) => prop.type !== GetterPropType.HEADER), "implementation").replaceAll("?:", ":");
2040
+ const setQueryDataProps = wrapPropsBodyWithMutatorBodyType({
2041
+ propsString: toObjectString(props.filter((prop) => prop.type !== GetterPropType.HEADER), "implementation").replaceAll("?:", ":"),
2042
+ body,
2043
+ mutator
2044
+ });
1966
2045
  const shouldGenerateGetQueryData = useGetQueryData && isPrimaryQueryType;
1967
2046
  const getQueryDataFnName = isReactQuery ? camel(`use-get-${name}-query-data`) : camel(`get-${name}-query-data`);
1968
2047
  const getQueryDataKeyExpr = setQueryDataKeyExpr;
@@ -1993,7 +2072,11 @@ export type ${pascal(name)}QueryError = ${errorType}
1993
2072
 
1994
2073
  ${adapter.shouldGenerateOverrideTypes() ? overrideTypes : ""}
1995
2074
  ${doc}
1996
- export function ${queryHookName}<TData = ${TData}, TError = ${errorType}>(\n ${adapter.getHookPropsDefinitions(props)} ${queryArguments} ${optionalQueryClientArgument} \n ): ${returnType} {
2075
+ export function ${queryHookName}<TData = ${TData}, TError = ${errorType}>(\n ${wrapPropsBodyWithMutatorBodyType({
2076
+ propsString: adapter.getHookPropsDefinitions(props),
2077
+ body,
2078
+ mutator
2079
+ })} ${queryArguments} ${optionalQueryClientArgument} \n ): ${returnType} {
1997
2080
 
1998
2081
  ${queryInit}
1999
2082
 
@@ -2045,20 +2128,14 @@ const generateQueryHook = async (verbOptions, options, outputClient, adapter) =>
2045
2128
  });
2046
2129
  let implementation = "";
2047
2130
  let mutators;
2048
- const hasOperationQueryOption = [
2049
- operationQueryOptions?.useQuery,
2050
- operationQueryOptions?.useSuspenseQuery,
2051
- operationQueryOptions?.useInfinite,
2052
- operationQueryOptions?.useSuspenseInfiniteQuery
2053
- ].some(Boolean);
2054
- let isQuery = Verbs.GET === verb && [
2055
- override.query.useQuery,
2056
- override.query.useSuspenseQuery,
2057
- override.query.useInfinite,
2058
- override.query.useSuspenseInfiniteQuery
2059
- ].some(Boolean) || hasOperationQueryOption;
2060
- let isMutation = override.query.useMutation && verb !== Verbs.GET;
2061
- if (operationQueryOptions?.useMutation !== void 0) isMutation = operationQueryOptions.useMutation;
2131
+ const effectiveUseQuery = operationQueryOptions?.useQuery ?? override.query.useQuery ?? verb === Verbs.GET;
2132
+ const effectiveUseMutation = operationQueryOptions?.useMutation ?? override.query.useMutation ?? verb !== Verbs.GET;
2133
+ const globalSuspenseOrInfiniteOnlyForGet = (flag) => flag === true && verb === Verbs.GET;
2134
+ const effectiveUseSuspenseQuery = operationQueryOptions?.useSuspenseQuery ?? globalSuspenseOrInfiniteOnlyForGet(override.query.useSuspenseQuery);
2135
+ const effectiveUseInfinite = operationQueryOptions?.useInfinite ?? globalSuspenseOrInfiniteOnlyForGet(override.query.useInfinite);
2136
+ const effectiveUseSuspenseInfiniteQuery = operationQueryOptions?.useSuspenseInfiniteQuery ?? globalSuspenseOrInfiniteOnlyForGet(override.query.useSuspenseInfiniteQuery);
2137
+ let isQuery = effectiveUseQuery || effectiveUseSuspenseQuery || effectiveUseInfinite || effectiveUseSuspenseInfiniteQuery;
2138
+ let isMutation = effectiveUseMutation && verb !== Verbs.GET;
2062
2139
  if (verb !== Verbs.GET && isQuery) isMutation = false;
2063
2140
  if (verb === Verbs.GET && isMutation) isQuery = false;
2064
2141
  const conflictWarning = getMutationInvalidatesConflictWarning({
@@ -2090,26 +2167,26 @@ const generateQueryHook = async (verbOptions, options, outputClient, adapter) =>
2090
2167
  return adapter.getQueryPropertyForProp(param, body);
2091
2168
  }).join(",");
2092
2169
  const queries = [
2093
- ...query.useInfinite || operationQueryOptions?.useInfinite ? [{
2170
+ ...effectiveUseInfinite ? [{
2094
2171
  name: camel(`${operationName}-infinite`),
2095
2172
  options: query.options,
2096
2173
  type: QueryType.INFINITE,
2097
2174
  queryParam: query.useInfiniteQueryParam,
2098
2175
  queryKeyFnName: camel(`get-${operationName}-infinite-query-key`)
2099
2176
  }] : [],
2100
- ...query.useQuery || operationQueryOptions?.useQuery ? [{
2177
+ ...effectiveUseQuery ? [{
2101
2178
  name: operationName,
2102
2179
  options: query.options,
2103
2180
  type: QueryType.QUERY,
2104
2181
  queryKeyFnName: camel(`get-${operationName}-query-key`)
2105
2182
  }] : [],
2106
- ...query.useSuspenseQuery || operationQueryOptions?.useSuspenseQuery ? [{
2183
+ ...effectiveUseSuspenseQuery ? [{
2107
2184
  name: camel(`${operationName}-suspense`),
2108
2185
  options: query.options,
2109
2186
  type: QueryType.SUSPENSE_QUERY,
2110
2187
  queryKeyFnName: camel(`get-${operationName}-query-key`)
2111
2188
  }] : [],
2112
- ...query.useSuspenseInfiniteQuery || operationQueryOptions?.useSuspenseInfiniteQuery ? [{
2189
+ ...effectiveUseSuspenseInfiniteQuery ? [{
2113
2190
  name: camel(`${operationName}-suspense-infinite`),
2114
2191
  options: query.options,
2115
2192
  type: QueryType.SUSPENSE_INFINITE,
@@ -2124,18 +2201,27 @@ const generateQueryHook = async (verbOptions, options, outputClient, adapter) =>
2124
2201
  if (impl.includes("=")) return impl;
2125
2202
  return impl.replace(/^(\w+):\s*/, "$1?: ");
2126
2203
  };
2127
- const queryKeyProps = toObjectString(props.filter((prop) => prop.type !== GetterPropType.HEADER).map((prop) => ({
2128
- ...prop,
2129
- implementation: prop.type === GetterPropType.PARAM || prop.type === GetterPropType.NAMED_PATH_PARAMS ? prop.implementation : makeOptionalParam(prop.implementation)
2130
- })), "implementation");
2204
+ const queryKeyProps = wrapPropsBodyWithMutatorBodyType({
2205
+ propsString: toObjectString(props.filter((prop) => prop.type !== GetterPropType.HEADER).map((prop) => ({
2206
+ ...prop,
2207
+ implementation: prop.type === GetterPropType.PARAM || prop.type === GetterPropType.NAMED_PATH_PARAMS ? prop.implementation : makeOptionalParam(prop.implementation)
2208
+ })), "implementation"),
2209
+ body,
2210
+ mutator
2211
+ });
2131
2212
  const routeString = adapter.getQueryKeyRouteString(route, !!override.query.shouldSplitQueryKey);
2132
2213
  const queryKeyIdentifier = override.query.useOperationIdAsQueryKey ? `"${operationName}"` : routeString;
2133
2214
  const queryKeyParams = props.filter((p) => override.query.useOperationIdAsQueryKey ? true : p.type === GetterPropType.QUERY_PARAM).toSorted((a) => a.required ? -1 : 1).map((p) => `...(${p.name} ? [${p.name}] : [])`).join(", ");
2215
+ const verbPrefix = getQueryKeyVerbPrefix({
2216
+ verb,
2217
+ useOperationIdAsQueryKey: override.query.useOperationIdAsQueryKey
2218
+ });
2134
2219
  queryKeyFns += `
2135
2220
  ${override.query.shouldExportQueryKey ? "export " : ""}const ${queryOption.queryKeyFnName} = (${queryKeyProps}) => {
2136
2221
  return [
2137
2222
  ${[
2138
2223
  queryOption.type === QueryType.INFINITE || queryOption.type === QueryType.SUSPENSE_INFINITE ? `'infinite'` : "",
2224
+ verbPrefix ? `'${verbPrefix}'` : "",
2139
2225
  queryKeyIdentifier,
2140
2226
  queryKeyParams,
2141
2227
  body.implementation
@@ -2154,6 +2240,7 @@ ${queryKeyFns}`;
2154
2240
  queryKeyProperties,
2155
2241
  params,
2156
2242
  props,
2243
+ body,
2157
2244
  mutator,
2158
2245
  isRequestOptions,
2159
2246
  queryParams,
@@ -2167,8 +2254,8 @@ ${queryKeyFns}`;
2167
2254
  route,
2168
2255
  doc,
2169
2256
  usePrefetch: query.usePrefetch,
2170
- useQuery: query.useQuery,
2171
- useInfinite: query.useInfinite,
2257
+ useQuery: effectiveUseQuery,
2258
+ useInfinite: effectiveUseInfinite,
2172
2259
  useInvalidate: query.useInvalidate,
2173
2260
  useSetQueryData: operationQueryOptions?.useSetQueryData ?? query.useSetQueryData,
2174
2261
  useGetQueryData: operationQueryOptions?.useGetQueryData ?? query.useGetQueryData,