openapi-ts icon indicating copy to clipboard operation
openapi-ts copied to clipboard

skip-token support

Open nandorojo opened this issue 3 months ago • 3 comments

Description

Would be nice to be able to do this:

useQuery({ 
  ...heyApiOptions(!deploymentId ? skipToken : { path: { deploymentId } }) 
})

nandorojo avatar Oct 16 '25 14:10 nandorojo

I am also looking forward to a feature like that! I currently solve that with enabled and ! operator but that isn't really ts friendly for me and my setup.

jofflin avatar Oct 28 '25 09:10 jofflin

@jofflin one limitation I pointed out is it needs to be figured out how to make this feature work with SDK calls such as foo(id, query, body), i.e. multiple arguments as opposed to the single argument they support today. As such, this feature is blocked by that to avoid back and forth

mrlubos avatar Oct 28 '25 09:10 mrlubos

@jofflin one limitation I pointed out is it needs to be figured out how to make this feature work with SDK calls such as foo(id, query, body), i.e. multiple arguments as opposed to the single argument they support today. As such, this feature is blocked by that to avoid back and forth

Makes sense. I am not too deep into the topic but a solution i could think of is something like:

  1. Provide configuration Option like automaticSkipToken
  2. This converts the queryOptions(options: Options<GetUserOrganizationsData>) => return queryOptions(...) queryOptions(options: Options<Partial<GetUserOrganizationsData>>) => return queryOptions(...) inside the queryOptions there is then the comparison if all mandatory fields are set, if not it uses skipToken in the react-query.gen.ts directly.

I created this helper for now that does exactly that

type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends object
    ? DeepPartial<T[P]> | null | undefined
    : T[P] | null | undefined;
};

type OptionalOptions<TData extends TDataShape> = Partial<
  Omit<Options<TData>, 'path' | 'query' | 'body'>
> & {
  path?: DeepPartial<TData['path']>;
  query?: DeepPartial<TData['query']>;
  body?: DeepPartial<TData['body']>;
};

function hasAllRequiredValues(obj: unknown): boolean {
  if (obj === null || obj === undefined) {
    return false;
  }

  if (typeof obj !== 'object') {
    return true;
  }

  if (Array.isArray(obj)) {
    return obj.every((item) => hasAllRequiredValues(item));
  }

  // For objects, check all values recursively
  return Object.values(obj).every((value) => {
    if (value === null || value === undefined) {
      return false;
    }
    if (typeof value === 'object') {
      return hasAllRequiredValues(value);
    }
    return true;
  });
}


export function createSafeQueryOptions<
  TData extends TDataShape,
  TQueryOptionsResult,
>(
  queryOptionsFn: (options: Options<TData>) => TQueryOptionsResult,
  options?: OptionalOptions<TData>
): TQueryOptionsResult {
  // If no options provided or options is empty, use skipToken
  if (!options || Object.keys(options).length === 0) {
    return { ...queryOptionsFn(options as Options<TData>), queryFn: skipToken };
  }

  // Check if all required values in path, query, and body are defined
  const pathValid = !options.path || hasAllRequiredValues(options.path);
  const queryValid = !options.query || hasAllRequiredValues(options.query);
  const bodyValid = !options.body || hasAllRequiredValues(options.body);

  // If any required parameter is missing, use skipToken
  if (!pathValid || !queryValid || !bodyValid) {
    return { ...queryOptionsFn(options as Options<TData>), queryFn: skipToken };
  }

  // All required parameters are present, call the queryOptions function
  // TypeScript knows this is safe because we've validated all values
  return queryOptionsFn(options as Options<TData>);
}

jofflin avatar Oct 28 '25 10:10 jofflin