All files definitions.ts

100% Statements 32/32
100% Branches 16/16
100% Functions 2/2
100% Lines 32/32

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 1281x   1x             1x             1x                     14x 14x 16x 16x 16x 16x 16x 14x                                                                                                                                               1x 2x 2x 2x 2x 2x   2x 2x 11x 11x 11x 14x 14x 14x 11x 11x 11x 2x 2x 2x  
import { stringify } from "query-string";
import type { ActualSchema, Empty, ExpectedSchema, Options } from "./types";
import { replacePathParams } from "./utilities";
 
/**
 * Utility used to define parameter types in parameter schemas
 * @example
 * { id: type as string }
 */
export const type: unknown = undefined;
 
/**
 * Utility used when no parameters are required for an endpoint method
 * @example
 * { get: empty }
 */
export const empty: Empty = undefined;
 
/**
 * Higher-order function that returns a URL generator for an endpoint, given its parameters
 * @param endpoint - The endpoint path, which can include path parameters (e.g., "/users/[id]")
 * @param baseUrl - The base URL to prepend to the generated URL (optional)
 * @example
 * const getUrl = method("/users/[id]", "https://example.com/api");
 * getUrl({ params: { id: "1" }, queries: { flag: true } });
 * // => "https://example.com/api/users/1?flag=true"
 */
function method(endpoint: string, baseUrl = "") {
  return (options?: Options) => {
    const path = replacePathParams(endpoint, options?.params);
    const queries = options?.queries ? `?${stringify(options.queries)}` : "";
    const hash = options?.hash ? `#${encodeURIComponent(options.hash)}` : "";
    return `${baseUrl}${path}${queries}${hash}`;
  };
}
 
/**
 * Takes a routes schema and returns an object with type-safe URL generators
 * Provides autocomplete for URL path parameters during definition, and type checking at usage
 *
 * @param schema - Routes schema, which can include path parameters (e.g., "/users/[id]") and method definitions (e.g., { get: { params: { id: type as string } } })
 * @returns An object with type-safe URL generators for each route and method
 * @example
 * import { routes, type, empty } from "routopia";
 *
 * const routes = routes({
 *   // Example for the /users route
 *   "/users": {
 *     get: {
 *       // Define query parameters
 *       queries: {
 *         required: type as string,
 *         optional: type as string | undefined,
 *       },
 *     },
 *     // No parameters needed
 *     post: empty
 *   },
 *   "/users/[id]": {
 *     get: {
 *       // Define path parameters
 *       params: {
 *         id: type as string,
 *       },
 *     },
 *   },
 * });
 */
export function routes<Schema extends ExpectedSchema<Schema>>(schema: Schema): ActualSchema<Schema, "">;
/**
 * Takes a base URL and a routes schema, and returns an object with type-safe URL generators
 * Provides autocomplete for URL path parameters during definition, and type checking at usage
 *
 * @param baseUrl - The base URL to prepend to the generated URLs
 * @param schema - Routes schema, which can include path parameters (e.g., "/users/[id]") and method definitions (e.g., { get: { params: { id: type as string } } })
 * @returns An object with type-safe URL generators for each route and method
 * @example
 * import { routes, type, empty } from "routopia";
 *
 * const routes = routes("https://example.com/api", {
 *   // Example for the /users route
 *   "/users": {
 *     get: {
 *       // Define query parameters
 *       queries: {
 *         required: type as string,
 *         optional: type as string | undefined,
 *       },
 *     },
 *     // No parameters needed
 *     post: empty,
 *   },
 *   "/users/[id]": {
 *     get: {
 *       // Define path parameters
 *       params: {
 *         id: type as string,
 *       },
 *     },
 *   },
 * });
 */
export function routes<BaseUrl extends string, Schema extends ExpectedSchema<Schema>>(
  baseUrl: BaseUrl,
  schema: Schema,
): ActualSchema<Schema, BaseUrl>;
export function routes<Schema extends ExpectedSchema<Schema>, BaseUrl extends string>(
  ...args: [Schema] | [BaseUrl, Schema]
) {
  const hasBaseUrl = args.length === 2;
  const baseUrl = hasBaseUrl ? args[0] : "";
  const schema = hasBaseUrl ? args[1] : args[0];
 
  return Object.entries<object>(schema).reduce(
    (acc, [endpoint, methods]) =>
      Object.assign(acc, {
        [endpoint]: Object.keys(methods).reduce(
          (acc, methodKey) =>
            Object.assign(acc, {
              [methodKey]: method(endpoint, baseUrl),
            }),
          {},
        ),
      }),
    {},
  ) as ActualSchema<Schema, BaseUrl>;
}