@typokit/client-react-query 0.1.4

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.
@@ -0,0 +1,59 @@
1
+ import type { RouteContract } from "@typokit/types";
2
+ import type { RouteMap, TypeSafeClient } from "@typokit/client";
3
+ import type { UseQueryResult, UseMutationOptions, UseMutationResult } from "@tanstack/react-query";
4
+ /** Build a React Query cache key from route path, params, and query */
5
+ export declare function buildQueryKey(path: string, params?: Record<string, string>, query?: Record<string, unknown>): readonly unknown[];
6
+ /** Extract GET route paths from a RouteMap */
7
+ type GetPaths<TRoutes extends RouteMap> = {
8
+ [P in keyof TRoutes]: "GET" extends keyof TRoutes[P] ? P : never;
9
+ }[keyof TRoutes] & string;
10
+ type MutationMethod = "POST" | "PUT" | "PATCH" | "DELETE";
11
+ /** Extract paths that have a given mutation method */
12
+ type MutationPaths<TRoutes extends RouteMap, M extends MutationMethod> = {
13
+ [P in keyof TRoutes]: M extends keyof TRoutes[P] ? P : never;
14
+ }[keyof TRoutes] & string;
15
+ /** Resolve the RouteContract for a given path and method */
16
+ type ContractFor<TRoutes extends RouteMap, P extends string, M extends string> = P extends keyof TRoutes ? M extends keyof TRoutes[P] ? TRoutes[P][M] extends RouteContract ? TRoutes[P][M] : RouteContract : RouteContract : RouteContract;
17
+ /** Options for useGet: route-specific params/query + React Query config */
18
+ export interface UseGetOptions<C extends RouteContract> {
19
+ params?: C["params"] extends void ? undefined : C["params"] & Record<string, string>;
20
+ query?: C["query"] extends void ? undefined : C["query"];
21
+ enabled?: boolean;
22
+ staleTime?: number;
23
+ refetchInterval?: number | false;
24
+ }
25
+ /** Variables passed to mutation hooks */
26
+ export interface MutationVariables<C extends RouteContract> {
27
+ params?: C["params"] extends void ? undefined : C["params"] & Record<string, string>;
28
+ body?: C["body"] extends void ? undefined : C["body"];
29
+ }
30
+ /** Mutation lifecycle callbacks */
31
+ type MutationCallbacks<TData, TVars> = Pick<UseMutationOptions<TData, Error, TVars, unknown>, "onSuccess" | "onError" | "onSettled" | "onMutate">;
32
+ /** Type-safe React Query hooks generated from a RouteMap */
33
+ export interface QueryHooks<TRoutes extends RouteMap> {
34
+ /** useQuery wrapper for GET routes */
35
+ useGet<P extends GetPaths<TRoutes>>(path: P, options?: UseGetOptions<ContractFor<TRoutes, P, "GET">>): UseQueryResult<ContractFor<TRoutes, P, "GET">["response"], Error>;
36
+ /** useMutation wrapper for POST routes */
37
+ usePost<P extends MutationPaths<TRoutes, "POST">>(path: P, options?: MutationCallbacks<ContractFor<TRoutes, P, "POST">["response"], MutationVariables<ContractFor<TRoutes, P, "POST">>>): UseMutationResult<ContractFor<TRoutes, P, "POST">["response"], Error, MutationVariables<ContractFor<TRoutes, P, "POST">>>;
38
+ /** useMutation wrapper for PUT routes */
39
+ usePut<P extends MutationPaths<TRoutes, "PUT">>(path: P, options?: MutationCallbacks<ContractFor<TRoutes, P, "PUT">["response"], MutationVariables<ContractFor<TRoutes, P, "PUT">>>): UseMutationResult<ContractFor<TRoutes, P, "PUT">["response"], Error, MutationVariables<ContractFor<TRoutes, P, "PUT">>>;
40
+ /** useMutation wrapper for PATCH routes */
41
+ usePatch<P extends MutationPaths<TRoutes, "PATCH">>(path: P, options?: MutationCallbacks<ContractFor<TRoutes, P, "PATCH">["response"], MutationVariables<ContractFor<TRoutes, P, "PATCH">>>): UseMutationResult<ContractFor<TRoutes, P, "PATCH">["response"], Error, MutationVariables<ContractFor<TRoutes, P, "PATCH">>>;
42
+ /** useMutation wrapper for DELETE routes */
43
+ useDelete<P extends MutationPaths<TRoutes, "DELETE">>(path: P, options?: MutationCallbacks<ContractFor<TRoutes, P, "DELETE">["response"], MutationVariables<ContractFor<TRoutes, P, "DELETE">>>): UseMutationResult<ContractFor<TRoutes, P, "DELETE">["response"], Error, MutationVariables<ContractFor<TRoutes, P, "DELETE">>>;
44
+ }
45
+ /**
46
+ * Create type-safe React Query hooks from a TypoKit client.
47
+ *
48
+ * @example
49
+ * ```ts
50
+ * const hooks = createQueryHooks<MyRoutes>(client);
51
+ * // In a React component:
52
+ * const { data, isLoading } = hooks.useGet("/users", { query: { page: 1 } });
53
+ * const createUser = hooks.usePost("/users");
54
+ * createUser.mutate({ body: { name: "Alice" } });
55
+ * ```
56
+ */
57
+ export declare function createQueryHooks<TRoutes extends RouteMap>(client: TypeSafeClient<TRoutes>): QueryHooks<TRoutes>;
58
+ export {};
59
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AACpD,OAAO,KAAK,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAChE,OAAO,KAAK,EACV,cAAc,EACd,kBAAkB,EAClB,iBAAiB,EAClB,MAAM,uBAAuB,CAAC;AAK/B,uEAAuE;AACvE,wBAAgB,aAAa,CAC3B,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC/B,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC9B,SAAS,OAAO,EAAE,CASpB;AAID,8CAA8C;AAC9C,KAAK,QAAQ,CAAC,OAAO,SAAS,QAAQ,IAAI;KACvC,CAAC,IAAI,MAAM,OAAO,GAAG,KAAK,SAAS,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,KAAK;CACjE,CAAC,MAAM,OAAO,CAAC,GACd,MAAM,CAAC;AAET,KAAK,cAAc,GAAG,MAAM,GAAG,KAAK,GAAG,OAAO,GAAG,QAAQ,CAAC;AAE1D,sDAAsD;AACtD,KAAK,aAAa,CAAC,OAAO,SAAS,QAAQ,EAAE,CAAC,SAAS,cAAc,IAAI;KACtE,CAAC,IAAI,MAAM,OAAO,GAAG,CAAC,SAAS,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,KAAK;CAC7D,CAAC,MAAM,OAAO,CAAC,GACd,MAAM,CAAC;AAET,4DAA4D;AAC5D,KAAK,WAAW,CACd,OAAO,SAAS,QAAQ,EACxB,CAAC,SAAS,MAAM,EAChB,CAAC,SAAS,MAAM,IACd,CAAC,SAAS,MAAM,OAAO,GACvB,CAAC,SAAS,MAAM,OAAO,CAAC,CAAC,CAAC,GACxB,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,aAAa,GACjC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GACb,aAAa,GACf,aAAa,GACf,aAAa,CAAC;AAIlB,2EAA2E;AAC3E,MAAM,WAAW,aAAa,CAAC,CAAC,SAAS,aAAa;IACpD,MAAM,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,SAAS,IAAI,GAC7B,SAAS,GACT,CAAC,CAAC,QAAQ,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACzC,KAAK,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC,SAAS,IAAI,GAAG,SAAS,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC;IACzD,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;CAClC;AAED,yCAAyC;AACzC,MAAM,WAAW,iBAAiB,CAAC,CAAC,SAAS,aAAa;IACxD,MAAM,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,SAAS,IAAI,GAC7B,SAAS,GACT,CAAC,CAAC,QAAQ,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACzC,IAAI,CAAC,EAAE,CAAC,CAAC,MAAM,CAAC,SAAS,IAAI,GAAG,SAAS,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC;CACvD;AAED,mCAAmC;AACnC,KAAK,iBAAiB,CAAC,KAAK,EAAE,KAAK,IAAI,IAAI,CACzC,kBAAkB,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,CAAC,EAChD,WAAW,GAAG,SAAS,GAAG,WAAW,GAAG,UAAU,CACnD,CAAC;AAIF,4DAA4D;AAC5D,MAAM,WAAW,UAAU,CAAC,OAAO,SAAS,QAAQ;IAClD,sCAAsC;IACtC,MAAM,CAAC,CAAC,SAAS,QAAQ,CAAC,OAAO,CAAC,EAChC,IAAI,EAAE,CAAC,EACP,OAAO,CAAC,EAAE,aAAa,CAAC,WAAW,CAAC,OAAO,EAAE,CAAC,EAAE,KAAK,CAAC,CAAC,GACtD,cAAc,CAAC,WAAW,CAAC,OAAO,EAAE,CAAC,EAAE,KAAK,CAAC,CAAC,UAAU,CAAC,EAAE,KAAK,CAAC,CAAC;IAErE,0CAA0C;IAC1C,OAAO,CAAC,CAAC,SAAS,aAAa,CAAC,OAAO,EAAE,MAAM,CAAC,EAC9C,IAAI,EAAE,CAAC,EACP,OAAO,CAAC,EAAE,iBAAiB,CACzB,WAAW,CAAC,OAAO,EAAE,CAAC,EAAE,MAAM,CAAC,CAAC,UAAU,CAAC,EAC3C,iBAAiB,CAAC,WAAW,CAAC,OAAO,EAAE,CAAC,EAAE,MAAM,CAAC,CAAC,CACnD,GACA,iBAAiB,CAClB,WAAW,CAAC,OAAO,EAAE,CAAC,EAAE,MAAM,CAAC,CAAC,UAAU,CAAC,EAC3C,KAAK,EACL,iBAAiB,CAAC,WAAW,CAAC,OAAO,EAAE,CAAC,EAAE,MAAM,CAAC,CAAC,CACnD,CAAC;IAEF,yCAAyC;IACzC,MAAM,CAAC,CAAC,SAAS,aAAa,CAAC,OAAO,EAAE,KAAK,CAAC,EAC5C,IAAI,EAAE,CAAC,EACP,OAAO,CAAC,EAAE,iBAAiB,CACzB,WAAW,CAAC,OAAO,EAAE,CAAC,EAAE,KAAK,CAAC,CAAC,UAAU,CAAC,EAC1C,iBAAiB,CAAC,WAAW,CAAC,OAAO,EAAE,CAAC,EAAE,KAAK,CAAC,CAAC,CAClD,GACA,iBAAiB,CAClB,WAAW,CAAC,OAAO,EAAE,CAAC,EAAE,KAAK,CAAC,CAAC,UAAU,CAAC,EAC1C,KAAK,EACL,iBAAiB,CAAC,WAAW,CAAC,OAAO,EAAE,CAAC,EAAE,KAAK,CAAC,CAAC,CAClD,CAAC;IAEF,2CAA2C;IAC3C,QAAQ,CAAC,CAAC,SAAS,aAAa,CAAC,OAAO,EAAE,OAAO,CAAC,EAChD,IAAI,EAAE,CAAC,EACP,OAAO,CAAC,EAAE,iBAAiB,CACzB,WAAW,CAAC,OAAO,EAAE,CAAC,EAAE,OAAO,CAAC,CAAC,UAAU,CAAC,EAC5C,iBAAiB,CAAC,WAAW,CAAC,OAAO,EAAE,CAAC,EAAE,OAAO,CAAC,CAAC,CACpD,GACA,iBAAiB,CAClB,WAAW,CAAC,OAAO,EAAE,CAAC,EAAE,OAAO,CAAC,CAAC,UAAU,CAAC,EAC5C,KAAK,EACL,iBAAiB,CAAC,WAAW,CAAC,OAAO,EAAE,CAAC,EAAE,OAAO,CAAC,CAAC,CACpD,CAAC;IAEF,4CAA4C;IAC5C,SAAS,CAAC,CAAC,SAAS,aAAa,CAAC,OAAO,EAAE,QAAQ,CAAC,EAClD,IAAI,EAAE,CAAC,EACP,OAAO,CAAC,EAAE,iBAAiB,CACzB,WAAW,CAAC,OAAO,EAAE,CAAC,EAAE,QAAQ,CAAC,CAAC,UAAU,CAAC,EAC7C,iBAAiB,CAAC,WAAW,CAAC,OAAO,EAAE,CAAC,EAAE,QAAQ,CAAC,CAAC,CACrD,GACA,iBAAiB,CAClB,WAAW,CAAC,OAAO,EAAE,CAAC,EAAE,QAAQ,CAAC,CAAC,UAAU,CAAC,EAC7C,KAAK,EACL,iBAAiB,CAAC,WAAW,CAAC,OAAO,EAAE,CAAC,EAAE,QAAQ,CAAC,CAAC,CACrD,CAAC;CACH;AAcD;;;;;;;;;;;GAWG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,SAAS,QAAQ,EACvD,MAAM,EAAE,cAAc,CAAC,OAAO,CAAC,GAC9B,UAAU,CAAC,OAAO,CAAC,CA0DrB"}
package/dist/index.js ADDED
@@ -0,0 +1,58 @@
1
+ // @typokit/client-react-query — React Query Hooks
2
+ import { useQuery, useMutation } from "@tanstack/react-query";
3
+ // ─── Query Key Builder ──────────────────────────────────────
4
+ /** Build a React Query cache key from route path, params, and query */
5
+ export function buildQueryKey(path, params, query) {
6
+ const key = [path];
7
+ if (params && Object.keys(params).length > 0) {
8
+ key.push(params);
9
+ }
10
+ if (query && Object.keys(query).length > 0) {
11
+ key.push(query);
12
+ }
13
+ return key;
14
+ }
15
+ // ─── Factory ────────────────────────────────────────────────
16
+ /**
17
+ * Create type-safe React Query hooks from a TypoKit client.
18
+ *
19
+ * @example
20
+ * ```ts
21
+ * const hooks = createQueryHooks<MyRoutes>(client);
22
+ * // In a React component:
23
+ * const { data, isLoading } = hooks.useGet("/users", { query: { page: 1 } });
24
+ * const createUser = hooks.usePost("/users");
25
+ * createUser.mutate({ body: { name: "Alice" } });
26
+ * ```
27
+ */
28
+ export function createQueryHooks(client) {
29
+ const c = client;
30
+ function makeGetHook(path, options) {
31
+ const queryKey = buildQueryKey(path, options?.params, options?.query);
32
+ return useQuery({
33
+ queryKey,
34
+ queryFn: () => c.get(path, { params: options?.params, query: options?.query }),
35
+ enabled: options?.enabled,
36
+ staleTime: options?.staleTime,
37
+ });
38
+ }
39
+ function makeMutationHook(method, path, options) {
40
+ const { onSuccess, onError, onSettled, onMutate, ...rest } = options ?? {};
41
+ return useMutation({
42
+ mutationFn: (variables) => c[method](path, { params: variables.params, body: variables.body }),
43
+ onSuccess: onSuccess,
44
+ onError: onError,
45
+ onSettled: onSettled,
46
+ onMutate: onMutate,
47
+ ...rest,
48
+ });
49
+ }
50
+ return {
51
+ useGet: (path, options) => makeGetHook(path, options),
52
+ usePost: (path, options) => makeMutationHook("post", path, options),
53
+ usePut: (path, options) => makeMutationHook("put", path, options),
54
+ usePatch: (path, options) => makeMutationHook("patch", path, options),
55
+ useDelete: (path, options) => makeMutationHook("delete", path, options),
56
+ };
57
+ }
58
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,kDAAkD;AASlD,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAE9D,+DAA+D;AAE/D,uEAAuE;AACvE,MAAM,UAAU,aAAa,CAC3B,IAAY,EACZ,MAA+B,EAC/B,KAA+B;IAE/B,MAAM,GAAG,GAAc,CAAC,IAAI,CAAC,CAAC;IAC9B,IAAI,MAAM,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC7C,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACnB,CAAC;IACD,IAAI,KAAK,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC3C,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAClB,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAmID,+DAA+D;AAE/D;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,gBAAgB,CAC9B,MAA+B;IAE/B,MAAM,CAAC,GAAG,MAAkC,CAAC;IAE7C,SAAS,WAAW,CAClB,IAAY,EACZ,OAMC;QAED,MAAM,QAAQ,GAAG,aAAa,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC;QACtE,OAAO,QAAQ,CAAC;YACd,QAAQ;YACR,OAAO,EAAE,GAAG,EAAE,CACZ,CAAC,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;YACjE,OAAO,EAAE,OAAO,EAAE,OAAO;YACzB,SAAS,EAAE,OAAO,EAAE,SAAS;SAC9B,CAAC,CAAC;IACL,CAAC;IAED,SAAS,gBAAgB,CACvB,MAA2C,EAC3C,IAAY,EACZ,OAAiC;QAMjC,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,GAAG,IAAI,EAAE,GAAG,OAAO,IAAI,EAAE,CAAC;QAC3E,OAAO,WAAW,CAAC;YACjB,UAAU,EAAE,CAAC,SAGZ,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,SAAS,CAAC,IAAI,EAAE,CAAC;YACzE,SAAS,EAAE,SAAsB;YACjC,OAAO,EAAE,OAAoB;YAC7B,SAAS,EAAE,SAAsB;YACjC,QAAQ,EAAE,QAAqB;YAC/B,GAAG,IAAI;SACR,CAAC,CAAC;IACL,CAAC;IAED,OAAO;QACL,MAAM,EAAE,CAAC,IAAY,EAAE,OAAiC,EAAE,EAAE,CAC1D,WAAW,CAAC,IAAI,EAAE,OAAkC,CAAC;QACvD,OAAO,EAAE,CAAC,IAAY,EAAE,OAAiC,EAAE,EAAE,CAC3D,gBAAgB,CAAC,MAAM,EAAE,IAAI,EAAE,OAAO,CAAC;QACzC,MAAM,EAAE,CAAC,IAAY,EAAE,OAAiC,EAAE,EAAE,CAC1D,gBAAgB,CAAC,KAAK,EAAE,IAAI,EAAE,OAAO,CAAC;QACxC,QAAQ,EAAE,CAAC,IAAY,EAAE,OAAiC,EAAE,EAAE,CAC5D,gBAAgB,CAAC,OAAO,EAAE,IAAI,EAAE,OAAO,CAAC;QAC1C,SAAS,EAAE,CAAC,IAAY,EAAE,OAAiC,EAAE,EAAE,CAC7D,gBAAgB,CAAC,QAAQ,EAAE,IAAI,EAAE,OAAO,CAAC;KACrB,CAAC;AAC3B,CAAC"}
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@typokit/client-react-query",
3
+ "exports": {
4
+ ".": {
5
+ "import": "./dist/index.js",
6
+ "types": "./dist/index.d.ts"
7
+ }
8
+ },
9
+ "version": "0.1.4",
10
+ "type": "module",
11
+ "files": [
12
+ "dist",
13
+ "src"
14
+ ],
15
+ "main": "./dist/index.js",
16
+ "types": "./dist/index.d.ts",
17
+ "dependencies": {
18
+ "@typokit/client": "0.1.4",
19
+ "@typokit/types": "0.1.4"
20
+ },
21
+ "peerDependencies": {
22
+ "@tanstack/react-query": ">=5.0.0",
23
+ "react": ">=18.0.0"
24
+ },
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "https://github.com/KyleBastien/typokit",
28
+ "directory": "packages/client-react-query"
29
+ },
30
+ "scripts": {
31
+ "test": "rstest run --passWithNoTests"
32
+ }
33
+ }
package/src/env.d.ts ADDED
@@ -0,0 +1,95 @@
1
+ // Ambient type declarations for @tanstack/react-query (peer dependency)
2
+ // Consumers must install @tanstack/react-query >= 5.0.0
3
+
4
+ declare module "@tanstack/react-query" {
5
+ export type QueryKey = readonly unknown[];
6
+
7
+ export interface UseQueryOptions<
8
+ TQueryFnData = unknown,
9
+ _TError = Error,
10
+ TData = TQueryFnData,
11
+ TQueryKey extends QueryKey = QueryKey,
12
+ > {
13
+ queryKey: TQueryKey;
14
+ queryFn: (context: { queryKey: TQueryKey }) => Promise<TQueryFnData>;
15
+ enabled?: boolean;
16
+ staleTime?: number;
17
+ refetchInterval?: number | false;
18
+ select?: (data: TQueryFnData) => TData;
19
+ }
20
+
21
+ export interface UseQueryResult<TData = unknown, TError = Error> {
22
+ data: TData | undefined;
23
+ error: TError | null;
24
+ isLoading: boolean;
25
+ isPending: boolean;
26
+ isError: boolean;
27
+ isSuccess: boolean;
28
+ isFetching: boolean;
29
+ refetch: () => Promise<UseQueryResult<TData, TError>>;
30
+ status: "pending" | "error" | "success";
31
+ }
32
+
33
+ export interface UseMutationOptions<
34
+ TData = unknown,
35
+ TError = Error,
36
+ TVariables = void,
37
+ TContext = unknown,
38
+ > {
39
+ mutationFn: (variables: TVariables) => Promise<TData>;
40
+ onSuccess?: (
41
+ data: TData,
42
+ variables: TVariables,
43
+ context: TContext | undefined,
44
+ ) => void;
45
+ onError?: (
46
+ error: TError,
47
+ variables: TVariables,
48
+ context: TContext | undefined,
49
+ ) => void;
50
+ onSettled?: (
51
+ data: TData | undefined,
52
+ error: TError | null,
53
+ variables: TVariables,
54
+ context: TContext | undefined,
55
+ ) => void;
56
+ onMutate?: (
57
+ variables: TVariables,
58
+ ) => TContext | Promise<TContext | undefined>;
59
+ }
60
+
61
+ export interface UseMutationResult<
62
+ TData = unknown,
63
+ TError = Error,
64
+ TVariables = void,
65
+ > {
66
+ mutate: (variables: TVariables) => void;
67
+ mutateAsync: (variables: TVariables) => Promise<TData>;
68
+ data: TData | undefined;
69
+ error: TError | null;
70
+ isIdle: boolean;
71
+ isPending: boolean;
72
+ isError: boolean;
73
+ isSuccess: boolean;
74
+ reset: () => void;
75
+ status: "idle" | "pending" | "error" | "success";
76
+ }
77
+
78
+ export function useQuery<
79
+ TQueryFnData = unknown,
80
+ TError = Error,
81
+ TData = TQueryFnData,
82
+ TQueryKey extends QueryKey = QueryKey,
83
+ >(
84
+ options: UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
85
+ ): UseQueryResult<TData, TError>;
86
+
87
+ export function useMutation<
88
+ TData = unknown,
89
+ TError = Error,
90
+ TVariables = void,
91
+ TContext = unknown,
92
+ >(
93
+ options: UseMutationOptions<TData, TError, TVariables, TContext>,
94
+ ): UseMutationResult<TData, TError, TVariables>;
95
+ }
@@ -0,0 +1,60 @@
1
+ // @typokit/client-react-query — Tests
2
+
3
+ import { describe, it, expect } from "@rstest/core";
4
+ import { buildQueryKey } from "./index.js";
5
+
6
+ // ─── buildQueryKey Tests ────────────────────────────────────
7
+
8
+ describe("buildQueryKey", () => {
9
+ it("returns path-only key when no params or query", () => {
10
+ const key = buildQueryKey("/users");
11
+ expect(key).toEqual(["/users"]);
12
+ });
13
+
14
+ it("includes params in key when provided", () => {
15
+ const key = buildQueryKey("/users/:id", { id: "123" });
16
+ expect(key).toEqual(["/users/:id", { id: "123" }]);
17
+ });
18
+
19
+ it("includes query in key when provided", () => {
20
+ const key = buildQueryKey("/users", undefined, { page: 1, limit: 10 });
21
+ expect(key).toEqual(["/users", { page: 1, limit: 10 }]);
22
+ });
23
+
24
+ it("includes both params and query when provided", () => {
25
+ const key = buildQueryKey(
26
+ "/users/:id/posts",
27
+ { id: "42" },
28
+ { sort: "date" },
29
+ );
30
+ expect(key).toEqual(["/users/:id/posts", { id: "42" }, { sort: "date" }]);
31
+ });
32
+
33
+ it("omits empty params object", () => {
34
+ const key = buildQueryKey("/users", {});
35
+ expect(key).toEqual(["/users"]);
36
+ });
37
+
38
+ it("omits empty query object", () => {
39
+ const key = buildQueryKey("/users", undefined, {});
40
+ expect(key).toEqual(["/users"]);
41
+ });
42
+
43
+ it("produces different keys for different params", () => {
44
+ const key1 = buildQueryKey("/users/:id", { id: "1" });
45
+ const key2 = buildQueryKey("/users/:id", { id: "2" });
46
+ expect(key1).not.toEqual(key2);
47
+ });
48
+
49
+ it("produces different keys for different queries", () => {
50
+ const key1 = buildQueryKey("/users", undefined, { page: 1 });
51
+ const key2 = buildQueryKey("/users", undefined, { page: 2 });
52
+ expect(key1).not.toEqual(key2);
53
+ });
54
+
55
+ it("produces different keys for different paths", () => {
56
+ const key1 = buildQueryKey("/users");
57
+ const key2 = buildQueryKey("/posts");
58
+ expect(key1).not.toEqual(key2);
59
+ });
60
+ });
package/src/index.ts ADDED
@@ -0,0 +1,233 @@
1
+ // @typokit/client-react-query — React Query Hooks
2
+
3
+ import type { RouteContract } from "@typokit/types";
4
+ import type { RouteMap, TypeSafeClient } from "@typokit/client";
5
+ import type {
6
+ UseQueryResult,
7
+ UseMutationOptions,
8
+ UseMutationResult,
9
+ } from "@tanstack/react-query";
10
+ import { useQuery, useMutation } from "@tanstack/react-query";
11
+
12
+ // ─── Query Key Builder ──────────────────────────────────────
13
+
14
+ /** Build a React Query cache key from route path, params, and query */
15
+ export function buildQueryKey(
16
+ path: string,
17
+ params?: Record<string, string>,
18
+ query?: Record<string, unknown>,
19
+ ): readonly unknown[] {
20
+ const key: unknown[] = [path];
21
+ if (params && Object.keys(params).length > 0) {
22
+ key.push(params);
23
+ }
24
+ if (query && Object.keys(query).length > 0) {
25
+ key.push(query);
26
+ }
27
+ return key;
28
+ }
29
+
30
+ // ─── Type-Level Utilities ───────────────────────────────────
31
+
32
+ /** Extract GET route paths from a RouteMap */
33
+ type GetPaths<TRoutes extends RouteMap> = {
34
+ [P in keyof TRoutes]: "GET" extends keyof TRoutes[P] ? P : never;
35
+ }[keyof TRoutes] &
36
+ string;
37
+
38
+ type MutationMethod = "POST" | "PUT" | "PATCH" | "DELETE";
39
+
40
+ /** Extract paths that have a given mutation method */
41
+ type MutationPaths<TRoutes extends RouteMap, M extends MutationMethod> = {
42
+ [P in keyof TRoutes]: M extends keyof TRoutes[P] ? P : never;
43
+ }[keyof TRoutes] &
44
+ string;
45
+
46
+ /** Resolve the RouteContract for a given path and method */
47
+ type ContractFor<
48
+ TRoutes extends RouteMap,
49
+ P extends string,
50
+ M extends string,
51
+ > = P extends keyof TRoutes
52
+ ? M extends keyof TRoutes[P]
53
+ ? TRoutes[P][M] extends RouteContract
54
+ ? TRoutes[P][M]
55
+ : RouteContract
56
+ : RouteContract
57
+ : RouteContract;
58
+
59
+ // ─── Hook Option / Variable Types ───────────────────────────
60
+
61
+ /** Options for useGet: route-specific params/query + React Query config */
62
+ export interface UseGetOptions<C extends RouteContract> {
63
+ params?: C["params"] extends void
64
+ ? undefined
65
+ : C["params"] & Record<string, string>;
66
+ query?: C["query"] extends void ? undefined : C["query"];
67
+ enabled?: boolean;
68
+ staleTime?: number;
69
+ refetchInterval?: number | false;
70
+ }
71
+
72
+ /** Variables passed to mutation hooks */
73
+ export interface MutationVariables<C extends RouteContract> {
74
+ params?: C["params"] extends void
75
+ ? undefined
76
+ : C["params"] & Record<string, string>;
77
+ body?: C["body"] extends void ? undefined : C["body"];
78
+ }
79
+
80
+ /** Mutation lifecycle callbacks */
81
+ type MutationCallbacks<TData, TVars> = Pick<
82
+ UseMutationOptions<TData, Error, TVars, unknown>,
83
+ "onSuccess" | "onError" | "onSettled" | "onMutate"
84
+ >;
85
+
86
+ // ─── QueryHooks Interface ───────────────────────────────────
87
+
88
+ /** Type-safe React Query hooks generated from a RouteMap */
89
+ export interface QueryHooks<TRoutes extends RouteMap> {
90
+ /** useQuery wrapper for GET routes */
91
+ useGet<P extends GetPaths<TRoutes>>(
92
+ path: P,
93
+ options?: UseGetOptions<ContractFor<TRoutes, P, "GET">>,
94
+ ): UseQueryResult<ContractFor<TRoutes, P, "GET">["response"], Error>;
95
+
96
+ /** useMutation wrapper for POST routes */
97
+ usePost<P extends MutationPaths<TRoutes, "POST">>(
98
+ path: P,
99
+ options?: MutationCallbacks<
100
+ ContractFor<TRoutes, P, "POST">["response"],
101
+ MutationVariables<ContractFor<TRoutes, P, "POST">>
102
+ >,
103
+ ): UseMutationResult<
104
+ ContractFor<TRoutes, P, "POST">["response"],
105
+ Error,
106
+ MutationVariables<ContractFor<TRoutes, P, "POST">>
107
+ >;
108
+
109
+ /** useMutation wrapper for PUT routes */
110
+ usePut<P extends MutationPaths<TRoutes, "PUT">>(
111
+ path: P,
112
+ options?: MutationCallbacks<
113
+ ContractFor<TRoutes, P, "PUT">["response"],
114
+ MutationVariables<ContractFor<TRoutes, P, "PUT">>
115
+ >,
116
+ ): UseMutationResult<
117
+ ContractFor<TRoutes, P, "PUT">["response"],
118
+ Error,
119
+ MutationVariables<ContractFor<TRoutes, P, "PUT">>
120
+ >;
121
+
122
+ /** useMutation wrapper for PATCH routes */
123
+ usePatch<P extends MutationPaths<TRoutes, "PATCH">>(
124
+ path: P,
125
+ options?: MutationCallbacks<
126
+ ContractFor<TRoutes, P, "PATCH">["response"],
127
+ MutationVariables<ContractFor<TRoutes, P, "PATCH">>
128
+ >,
129
+ ): UseMutationResult<
130
+ ContractFor<TRoutes, P, "PATCH">["response"],
131
+ Error,
132
+ MutationVariables<ContractFor<TRoutes, P, "PATCH">>
133
+ >;
134
+
135
+ /** useMutation wrapper for DELETE routes */
136
+ useDelete<P extends MutationPaths<TRoutes, "DELETE">>(
137
+ path: P,
138
+ options?: MutationCallbacks<
139
+ ContractFor<TRoutes, P, "DELETE">["response"],
140
+ MutationVariables<ContractFor<TRoutes, P, "DELETE">>
141
+ >,
142
+ ): UseMutationResult<
143
+ ContractFor<TRoutes, P, "DELETE">["response"],
144
+ Error,
145
+ MutationVariables<ContractFor<TRoutes, P, "DELETE">>
146
+ >;
147
+ }
148
+
149
+ // ─── Untyped client for internal use ────────────────────────
150
+
151
+ interface UntypedClient {
152
+ get(path: string, options?: unknown): Promise<unknown>;
153
+ post(path: string, options?: unknown): Promise<unknown>;
154
+ put(path: string, options?: unknown): Promise<unknown>;
155
+ patch(path: string, options?: unknown): Promise<unknown>;
156
+ delete(path: string, options?: unknown): Promise<unknown>;
157
+ }
158
+
159
+ // ─── Factory ────────────────────────────────────────────────
160
+
161
+ /**
162
+ * Create type-safe React Query hooks from a TypoKit client.
163
+ *
164
+ * @example
165
+ * ```ts
166
+ * const hooks = createQueryHooks<MyRoutes>(client);
167
+ * // In a React component:
168
+ * const { data, isLoading } = hooks.useGet("/users", { query: { page: 1 } });
169
+ * const createUser = hooks.usePost("/users");
170
+ * createUser.mutate({ body: { name: "Alice" } });
171
+ * ```
172
+ */
173
+ export function createQueryHooks<TRoutes extends RouteMap>(
174
+ client: TypeSafeClient<TRoutes>,
175
+ ): QueryHooks<TRoutes> {
176
+ const c = client as unknown as UntypedClient;
177
+
178
+ function makeGetHook(
179
+ path: string,
180
+ options?: {
181
+ params?: Record<string, string>;
182
+ query?: Record<string, unknown>;
183
+ enabled?: boolean;
184
+ staleTime?: number;
185
+ refetchInterval?: number | false;
186
+ },
187
+ ): UseQueryResult<unknown, Error> {
188
+ const queryKey = buildQueryKey(path, options?.params, options?.query);
189
+ return useQuery({
190
+ queryKey,
191
+ queryFn: () =>
192
+ c.get(path, { params: options?.params, query: options?.query }),
193
+ enabled: options?.enabled,
194
+ staleTime: options?.staleTime,
195
+ });
196
+ }
197
+
198
+ function makeMutationHook(
199
+ method: "post" | "put" | "patch" | "delete",
200
+ path: string,
201
+ options?: Record<string, unknown>,
202
+ ): UseMutationResult<
203
+ unknown,
204
+ Error,
205
+ { params?: Record<string, string>; body?: unknown }
206
+ > {
207
+ const { onSuccess, onError, onSettled, onMutate, ...rest } = options ?? {};
208
+ return useMutation({
209
+ mutationFn: (variables: {
210
+ params?: Record<string, string>;
211
+ body?: unknown;
212
+ }) => c[method](path, { params: variables.params, body: variables.body }),
213
+ onSuccess: onSuccess as undefined,
214
+ onError: onError as undefined,
215
+ onSettled: onSettled as undefined,
216
+ onMutate: onMutate as undefined,
217
+ ...rest,
218
+ });
219
+ }
220
+
221
+ return {
222
+ useGet: (path: string, options?: Record<string, unknown>) =>
223
+ makeGetHook(path, options as Record<string, unknown>),
224
+ usePost: (path: string, options?: Record<string, unknown>) =>
225
+ makeMutationHook("post", path, options),
226
+ usePut: (path: string, options?: Record<string, unknown>) =>
227
+ makeMutationHook("put", path, options),
228
+ usePatch: (path: string, options?: Record<string, unknown>) =>
229
+ makeMutationHook("patch", path, options),
230
+ useDelete: (path: string, options?: Record<string, unknown>) =>
231
+ makeMutationHook("delete", path, options),
232
+ } as QueryHooks<TRoutes>;
233
+ }