1import type { Api, ApiContext, Module, ModuleName } from './apiTypes'
2import type { BaseQueryArg, BaseQueryFn } from './baseQueryTypes'
3import type { SerializeQueryArgs } from './defaultSerializeQueryArgs'
4import { defaultSerializeQueryArgs } from './defaultSerializeQueryArgs'
5import type {
6  EndpointBuilder,
7  EndpointDefinitions,
8} from './endpointDefinitions'
9import { DefinitionType } from './endpointDefinitions'
10import { nanoid } from '@reduxjs/toolkit'
11
12export interface CreateApiOptions<
13  BaseQuery extends BaseQueryFn,
14  Definitions extends EndpointDefinitions,
15  ReducerPath extends string = 'api',
16  TagTypes extends string = never
17> {
18  /**
19   * The base query used by each endpoint if no `queryFn` option is specified. RTK Query exports a utility called [fetchBaseQuery](./fetchBaseQuery) as a lightweight wrapper around `fetch` for common use-cases. See [Customizing Queries](../../rtk-query/usage/customizing-queries) if `fetchBaseQuery` does not handle your requirements.
20   *
21   * @example
22   *
23   * ```ts
24   * import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query'
25   *
26   * const api = createApi({
27   *   // highlight-start
28   *   baseQuery: fetchBaseQuery({ baseUrl: '/' }),
29   *   // highlight-end
30   *   endpoints: (build) => ({
31   *     // ...endpoints
32   *   }),
33   * })
34   * ```
35   */
36  baseQuery: BaseQuery
37  /**
38   * An array of string tag type names. Specifying tag types is optional, but you should define them so that they can be used for caching and invalidation. When defining an tag type, you will be able to [provide](../../rtk-query/usage/automated-refetching#providing-tags) them with `provides` and [invalidate](../../rtk-query/usage/automated-refetching#invalidating-tags) them with `invalidates` when configuring [endpoints](#endpoints).
39   *
40   * @example
41   *
42   * ```ts
43   * import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query'
44   *
45   * const api = createApi({
46   *   baseQuery: fetchBaseQuery({ baseUrl: '/' }),
47   *   // highlight-start
48   *   tagTypes: ['Post', 'User'],
49   *   // highlight-end
50   *   endpoints: (build) => ({
51   *     // ...endpoints
52   *   }),
53   * })
54   * ```
55   */
56  tagTypes?: readonly TagTypes[]
57  /**
58   * The `reducerPath` is a _unique_ key that your service will be mounted to in your store. If you call `createApi` more than once in your application, you will need to provide a unique value each time. Defaults to `'api'`.
59   *
60   * @example
61   *
62   * ```ts
63   * // codeblock-meta title="apis.js"
64   * import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query';
65   *
66   * const apiOne = createApi({
67   *   // highlight-start
68   *   reducerPath: 'apiOne',
69   *   // highlight-end
70   *   baseQuery: fetchBaseQuery({ baseUrl: '/' }),
71   *   endpoints: (builder) => ({
72   *     // ...endpoints
73   *   }),
74   * });
75   *
76   * const apiTwo = createApi({
77   *   // highlight-start
78   *   reducerPath: 'apiTwo',
79   *   // highlight-end
80   *   baseQuery: fetchBaseQuery({ baseUrl: '/' }),
81   *   endpoints: (builder) => ({
82   *     // ...endpoints
83   *   }),
84   * });
85   * ```
86   */
87  reducerPath?: ReducerPath
88  /**
89   * Accepts a custom function if you have a need to change the creation of cache keys for any reason.
90   */
91  serializeQueryArgs?: SerializeQueryArgs<BaseQueryArg<BaseQuery>>
92  /**
93   * Endpoints are just a set of operations that you want to perform against your server. You define them as an object using the builder syntax. There are two basic endpoint types: [`query`](../../rtk-query/usage/queries) and [`mutation`](../../rtk-query/usage/mutations).
94   */
95  endpoints(
96    build: EndpointBuilder<BaseQuery, TagTypes, ReducerPath>
97  ): Definitions
98  /**
99   * Defaults to `60` _(this value is in seconds)_. This is how long RTK Query will keep your data cached for **after** the last component unsubscribes. For example, if you query an endpoint, then unmount the component, then mount another component that makes the same request within the given time frame, the most recent value will be served from the cache.
100   *
101   * ```ts
102   * // codeblock-meta title="keepUnusedDataFor example"
103   *
104   * import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
105   * interface Post {
106   *   id: number
107   *   name: string
108   * }
109   * type PostsResponse = Post[]
110   *
111   * const api = createApi({
112   *   baseQuery: fetchBaseQuery({ baseUrl: '/' }),
113   *   endpoints: (build) => ({
114   *     getPosts: build.query<PostsResponse, void>({
115   *       query: () => 'posts',
116   *       // highlight-start
117   *       keepUnusedDataFor: 5
118   *       // highlight-end
119   *     })
120   *   })
121   * })
122   * ```
123   */
124  keepUnusedDataFor?: number
125  /**
126   * Defaults to `false`. This setting allows you to control whether if a cached result is already available RTK Query will only serve a cached result, or if it should `refetch` when set to `true` or if an adequate amount of time has passed since the last successful query result.
127   * - `false` - Will not cause a query to be performed _unless_ it does not exist yet.
128   * - `true` - Will always refetch when a new subscriber to a query is added. Behaves the same as calling the `refetch` callback or passing `forceRefetch: true` in the action creator.
129   * - `number` - **Value is in seconds**. If a number is provided and there is an existing query in the cache, it will compare the current time vs the last fulfilled timestamp, and only refetch if enough time has elapsed.
130   *
131   * If you specify this option alongside `skip: true`, this **will not be evaluated** until `skip` is false.
132   */
133  refetchOnMountOrArgChange?: boolean | number
134  /**
135   * Defaults to `false`. This setting allows you to control whether RTK Query will try to refetch all subscribed queries after the application window regains focus.
136   *
137   * If you specify this option alongside `skip: true`, this **will not be evaluated** until `skip` is false.
138   *
139   * Note: requires [`setupListeners`](./setupListeners) to have been called.
140   */
141  refetchOnFocus?: boolean
142  /**
143   * Defaults to `false`. This setting allows you to control whether RTK Query will try to refetch all subscribed queries after regaining a network connection.
144   *
145   * If you specify this option alongside `skip: true`, this **will not be evaluated** until `skip` is false.
146   *
147   * Note: requires [`setupListeners`](./setupListeners) to have been called.
148   */
149  refetchOnReconnect?: boolean
150}
151
152export type CreateApi<Modules extends ModuleName> = {
153  /**
154   * Creates a service to use in your application. Contains only the basic redux logic (the core module).
155   *
156   * @link https://rtk-query-docs.netlify.app/api/createApi
157   */
158  <
159    BaseQuery extends BaseQueryFn,
160    Definitions extends EndpointDefinitions,
161    ReducerPath extends string = 'api',
162    TagTypes extends string = never
163  >(
164    options: CreateApiOptions<BaseQuery, Definitions, ReducerPath, TagTypes>
165  ): Api<BaseQuery, Definitions, ReducerPath, TagTypes, Modules>
166}
167
168/**
169 * Builds a `createApi` method based on the provided `modules`.
170 *
171 * @link https://rtk-query-docs.netlify.app/concepts/customizing-create-api
172 *
173 * @example
174 * ```ts
175 * const MyContext = React.createContext<ReactReduxContextValue>(null as any);
176 * const customCreateApi = buildCreateApi(
177 *   coreModule(),
178 *   reactHooksModule({ useDispatch: createDispatchHook(MyContext) })
179 * );
180 * ```
181 *
182 * @param modules - A variable number of modules that customize how the `createApi` method handles endpoints
183 * @returns A `createApi` method using the provided `modules`.
184 */
185export function buildCreateApi<Modules extends [Module<any>, ...Module<any>[]]>(
186  ...modules: Modules
187): CreateApi<Modules[number]['name']> {
188  return function baseCreateApi(options) {
189    const optionsWithDefaults = {
190      reducerPath: 'api',
191      serializeQueryArgs: defaultSerializeQueryArgs,
192      keepUnusedDataFor: 60,
193      refetchOnMountOrArgChange: false,
194      refetchOnFocus: false,
195      refetchOnReconnect: false,
196      ...options,
197      tagTypes: [...(options.tagTypes || [])],
198    }
199
200    const context: ApiContext<EndpointDefinitions> = {
201      endpointDefinitions: {},
202      batch(fn) {
203        // placeholder "batch" method to be overridden by plugins, for example with React.unstable_batchedUpdate
204        fn()
205      },
206      apiUid: nanoid(),
207    }
208
209    const api = {
210      injectEndpoints,
211      enhanceEndpoints({ addTagTypes, endpoints }) {
212        if (addTagTypes) {
213          for (const eT of addTagTypes) {
214            if (!optionsWithDefaults.tagTypes.includes(eT as any)) {
215              optionsWithDefaults.tagTypes.push(eT as any)
216            }
217          }
218        }
219        if (endpoints) {
220          for (const [endpointName, partialDefinition] of Object.entries(
221            endpoints
222          )) {
223            if (typeof partialDefinition === 'function') {
224              partialDefinition(context.endpointDefinitions[endpointName])
225            }
226            Object.assign(
227              context.endpointDefinitions[endpointName] || {},
228              partialDefinition
229            )
230          }
231        }
232        return api
233      },
234    } as Api<BaseQueryFn, {}, string, string, Modules[number]['name']>
235
236    const initializedModules = modules.map((m) =>
237      m.init(api as any, optionsWithDefaults, context)
238    )
239
240    function injectEndpoints(
241      inject: Parameters<typeof api.injectEndpoints>[0]
242    ) {
243      const evaluatedEndpoints = inject.endpoints({
244        query: (x) => ({ ...x, type: DefinitionType.query } as any),
245        mutation: (x) => ({ ...x, type: DefinitionType.mutation } as any),
246      })
247
248      for (const [endpointName, definition] of Object.entries(
249        evaluatedEndpoints
250      )) {
251        if (
252          !inject.overrideExisting &&
253          endpointName in context.endpointDefinitions
254        ) {
255          if (
256            typeof process !== 'undefined' &&
257            process.env.NODE_ENV === 'development'
258          ) {
259            console.error(
260              `called \`injectEndpoints\` to override already-existing endpointName ${endpointName} without specifying \`overrideExisting: true\``
261            )
262          }
263
264          continue
265        }
266        context.endpointDefinitions[endpointName] = definition
267        for (const m of initializedModules) {
268          m.injectEndpoint(endpointName, definition)
269        }
270      }
271
272      return api as any
273    }
274
275    return api.injectEndpoints({ endpoints: options.endpoints as any })
276  }
277}
278