@void-snippets/react 0.1.7 → 0.2.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.d.mts +40 -26
- package/dist/index.d.ts +40 -26
- package/dist/index.js +7 -6
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +7 -6
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
package/dist/index.d.mts
CHANGED
|
@@ -1,43 +1,57 @@
|
|
|
1
1
|
import * as _tanstack_react_query from '@tanstack/react-query';
|
|
2
|
-
import { ResourceService } from '@void-snippets/client';
|
|
3
2
|
import { ResourceAdapters, TQueryParams, TPagination, ResourceListResult } from '@void-snippets/core';
|
|
4
3
|
|
|
5
|
-
type
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
4
|
+
type CapitalizeStr<S extends string> = S extends `${infer F}${infer Rest}` ? `${Uppercase<F>}${Rest}` : S;
|
|
5
|
+
interface WithResourceTypes {
|
|
6
|
+
readonly __types: {
|
|
7
|
+
id: unknown;
|
|
8
|
+
base: unknown;
|
|
9
|
+
detail: unknown;
|
|
10
|
+
create: unknown;
|
|
11
|
+
update: unknown;
|
|
12
|
+
listRaw: unknown;
|
|
13
|
+
singleRaw: unknown;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
type Id<S extends WithResourceTypes> = S["__types"]["id"];
|
|
17
|
+
type Base<S extends WithResourceTypes> = S["__types"]["base"];
|
|
18
|
+
type Detail<S extends WithResourceTypes> = S["__types"]["detail"];
|
|
19
|
+
type Create<S extends WithResourceTypes> = S["__types"]["create"];
|
|
20
|
+
type Update<S extends WithResourceTypes> = S["__types"]["update"];
|
|
21
|
+
type ListRaw<S extends WithResourceTypes> = S["__types"]["listRaw"];
|
|
22
|
+
type SingleRaw<S extends WithResourceTypes> = S["__types"]["singleRaw"];
|
|
14
23
|
type UseListReturn<K extends string, TBase> = {
|
|
15
24
|
[P in K]: TBase[];
|
|
16
25
|
} & {
|
|
17
26
|
pagination: TPagination;
|
|
18
27
|
} & {
|
|
19
|
-
[P in `is${
|
|
28
|
+
[P in `is${CapitalizeStr<K>}Loading`]: boolean;
|
|
20
29
|
} & {
|
|
21
30
|
[P in `${K}Error`]: Error | null;
|
|
22
31
|
} & {
|
|
23
|
-
[P in `invalidate${
|
|
32
|
+
[P in `invalidate${CapitalizeStr<K>}`]: () => void;
|
|
24
33
|
};
|
|
25
34
|
interface CreateResourceHooksOptions<TListRaw, TBase, TSingleRaw, TDetail> {
|
|
26
35
|
/**
|
|
27
|
-
* Adapters
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
36
|
+
* Adapters map your API's raw response shapes to the library's internal
|
|
37
|
+
* format. Omit this entirely if your API matches the default shape:
|
|
38
|
+
* List: { data: { items, page, limit, totalPages, totalDocuments } }
|
|
39
|
+
* Single: { data: <item> }
|
|
31
40
|
*
|
|
32
41
|
* @example
|
|
33
42
|
* createResourceHooks("contacts", ContactsApis, {
|
|
34
43
|
* adapters: {
|
|
35
44
|
* fromList: (raw) => ({
|
|
36
45
|
* items: raw.results,
|
|
37
|
-
* pagination: {
|
|
46
|
+
* pagination: {
|
|
47
|
+
* page: raw.meta.page,
|
|
48
|
+
* limit: raw.meta.perPage,
|
|
49
|
+
* totalPages: raw.meta.lastPage,
|
|
50
|
+
* totalDocuments: raw.meta.total,
|
|
51
|
+
* },
|
|
38
52
|
* }),
|
|
39
53
|
* fromSingle: (raw) => raw.payload,
|
|
40
|
-
* }
|
|
54
|
+
* },
|
|
41
55
|
* })
|
|
42
56
|
*/
|
|
43
57
|
adapters?: ResourceAdapters<TListRaw, TBase, TSingleRaw, TDetail>;
|
|
@@ -49,11 +63,11 @@ interface CreateResourceHooksOptions<TListRaw, TBase, TSingleRaw, TDetail> {
|
|
|
49
63
|
}
|
|
50
64
|
/**
|
|
51
65
|
* Creates a set of TanStack Query hooks for a resource.
|
|
52
|
-
* All types are fully inferred from the `apiService` instance — no
|
|
53
|
-
*
|
|
66
|
+
* All types are fully inferred from the `apiService` instance — no generics
|
|
67
|
+
* need to be passed manually.
|
|
54
68
|
*
|
|
55
|
-
* @param queryKeyPrefix -
|
|
56
|
-
*
|
|
69
|
+
* @param queryKeyPrefix - TanStack Query cache key prefix and the base name
|
|
70
|
+
* for the returned hook properties.
|
|
57
71
|
* e.g. "contacts" → { contacts, isContactsLoading, ... }
|
|
58
72
|
* @param apiService - An instance of ResourceService (or a subclass).
|
|
59
73
|
* @param options - Optional adapters and default params.
|
|
@@ -63,15 +77,15 @@ interface CreateResourceHooksOptions<TListRaw, TBase, TSingleRaw, TDetail> {
|
|
|
63
77
|
* import { createResourceHooks } from '@void-snippets/react';
|
|
64
78
|
* import { ContactsApis } from './contacts.api';
|
|
65
79
|
*
|
|
66
|
-
* // No generics needed —
|
|
80
|
+
* // No generics needed — all types are inferred from ContactsApis
|
|
67
81
|
* export const contactHooks = createResourceHooks('contacts', ContactsApis);
|
|
68
82
|
*
|
|
69
83
|
* // In a component:
|
|
70
84
|
* const { contacts, isContactsLoading } = contactHooks.useList();
|
|
71
|
-
* const { data } = contactHooks.useGet(id); // data
|
|
72
|
-
* const { create, update, delete:
|
|
85
|
+
* const { data } = contactHooks.useGet(id); // data: Contact.WithCreatedBy
|
|
86
|
+
* const { create, update, delete: remove } = contactHooks.useMutations();
|
|
73
87
|
*/
|
|
74
|
-
declare function createResourceHooks<K extends string, S extends
|
|
88
|
+
declare function createResourceHooks<K extends string, S extends WithResourceTypes>(queryKeyPrefix: K, apiService: S, options?: CreateResourceHooksOptions<ListRaw<S>, Base<S>, SingleRaw<S>, Detail<S>>): {
|
|
75
89
|
useList: (params?: TQueryParams) => UseListReturn<K, Base<S>>;
|
|
76
90
|
useGet: (id: Id<S>, staleTime?: number) => {
|
|
77
91
|
data: _tanstack_react_query.NoInfer<Detail<S>> | undefined;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,43 +1,57 @@
|
|
|
1
1
|
import * as _tanstack_react_query from '@tanstack/react-query';
|
|
2
|
-
import { ResourceService } from '@void-snippets/client';
|
|
3
2
|
import { ResourceAdapters, TQueryParams, TPagination, ResourceListResult } from '@void-snippets/core';
|
|
4
3
|
|
|
5
|
-
type
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
4
|
+
type CapitalizeStr<S extends string> = S extends `${infer F}${infer Rest}` ? `${Uppercase<F>}${Rest}` : S;
|
|
5
|
+
interface WithResourceTypes {
|
|
6
|
+
readonly __types: {
|
|
7
|
+
id: unknown;
|
|
8
|
+
base: unknown;
|
|
9
|
+
detail: unknown;
|
|
10
|
+
create: unknown;
|
|
11
|
+
update: unknown;
|
|
12
|
+
listRaw: unknown;
|
|
13
|
+
singleRaw: unknown;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
type Id<S extends WithResourceTypes> = S["__types"]["id"];
|
|
17
|
+
type Base<S extends WithResourceTypes> = S["__types"]["base"];
|
|
18
|
+
type Detail<S extends WithResourceTypes> = S["__types"]["detail"];
|
|
19
|
+
type Create<S extends WithResourceTypes> = S["__types"]["create"];
|
|
20
|
+
type Update<S extends WithResourceTypes> = S["__types"]["update"];
|
|
21
|
+
type ListRaw<S extends WithResourceTypes> = S["__types"]["listRaw"];
|
|
22
|
+
type SingleRaw<S extends WithResourceTypes> = S["__types"]["singleRaw"];
|
|
14
23
|
type UseListReturn<K extends string, TBase> = {
|
|
15
24
|
[P in K]: TBase[];
|
|
16
25
|
} & {
|
|
17
26
|
pagination: TPagination;
|
|
18
27
|
} & {
|
|
19
|
-
[P in `is${
|
|
28
|
+
[P in `is${CapitalizeStr<K>}Loading`]: boolean;
|
|
20
29
|
} & {
|
|
21
30
|
[P in `${K}Error`]: Error | null;
|
|
22
31
|
} & {
|
|
23
|
-
[P in `invalidate${
|
|
32
|
+
[P in `invalidate${CapitalizeStr<K>}`]: () => void;
|
|
24
33
|
};
|
|
25
34
|
interface CreateResourceHooksOptions<TListRaw, TBase, TSingleRaw, TDetail> {
|
|
26
35
|
/**
|
|
27
|
-
* Adapters
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
36
|
+
* Adapters map your API's raw response shapes to the library's internal
|
|
37
|
+
* format. Omit this entirely if your API matches the default shape:
|
|
38
|
+
* List: { data: { items, page, limit, totalPages, totalDocuments } }
|
|
39
|
+
* Single: { data: <item> }
|
|
31
40
|
*
|
|
32
41
|
* @example
|
|
33
42
|
* createResourceHooks("contacts", ContactsApis, {
|
|
34
43
|
* adapters: {
|
|
35
44
|
* fromList: (raw) => ({
|
|
36
45
|
* items: raw.results,
|
|
37
|
-
* pagination: {
|
|
46
|
+
* pagination: {
|
|
47
|
+
* page: raw.meta.page,
|
|
48
|
+
* limit: raw.meta.perPage,
|
|
49
|
+
* totalPages: raw.meta.lastPage,
|
|
50
|
+
* totalDocuments: raw.meta.total,
|
|
51
|
+
* },
|
|
38
52
|
* }),
|
|
39
53
|
* fromSingle: (raw) => raw.payload,
|
|
40
|
-
* }
|
|
54
|
+
* },
|
|
41
55
|
* })
|
|
42
56
|
*/
|
|
43
57
|
adapters?: ResourceAdapters<TListRaw, TBase, TSingleRaw, TDetail>;
|
|
@@ -49,11 +63,11 @@ interface CreateResourceHooksOptions<TListRaw, TBase, TSingleRaw, TDetail> {
|
|
|
49
63
|
}
|
|
50
64
|
/**
|
|
51
65
|
* Creates a set of TanStack Query hooks for a resource.
|
|
52
|
-
* All types are fully inferred from the `apiService` instance — no
|
|
53
|
-
*
|
|
66
|
+
* All types are fully inferred from the `apiService` instance — no generics
|
|
67
|
+
* need to be passed manually.
|
|
54
68
|
*
|
|
55
|
-
* @param queryKeyPrefix -
|
|
56
|
-
*
|
|
69
|
+
* @param queryKeyPrefix - TanStack Query cache key prefix and the base name
|
|
70
|
+
* for the returned hook properties.
|
|
57
71
|
* e.g. "contacts" → { contacts, isContactsLoading, ... }
|
|
58
72
|
* @param apiService - An instance of ResourceService (or a subclass).
|
|
59
73
|
* @param options - Optional adapters and default params.
|
|
@@ -63,15 +77,15 @@ interface CreateResourceHooksOptions<TListRaw, TBase, TSingleRaw, TDetail> {
|
|
|
63
77
|
* import { createResourceHooks } from '@void-snippets/react';
|
|
64
78
|
* import { ContactsApis } from './contacts.api';
|
|
65
79
|
*
|
|
66
|
-
* // No generics needed —
|
|
80
|
+
* // No generics needed — all types are inferred from ContactsApis
|
|
67
81
|
* export const contactHooks = createResourceHooks('contacts', ContactsApis);
|
|
68
82
|
*
|
|
69
83
|
* // In a component:
|
|
70
84
|
* const { contacts, isContactsLoading } = contactHooks.useList();
|
|
71
|
-
* const { data } = contactHooks.useGet(id); // data
|
|
72
|
-
* const { create, update, delete:
|
|
85
|
+
* const { data } = contactHooks.useGet(id); // data: Contact.WithCreatedBy
|
|
86
|
+
* const { create, update, delete: remove } = contactHooks.useMutations();
|
|
73
87
|
*/
|
|
74
|
-
declare function createResourceHooks<K extends string, S extends
|
|
88
|
+
declare function createResourceHooks<K extends string, S extends WithResourceTypes>(queryKeyPrefix: K, apiService: S, options?: CreateResourceHooksOptions<ListRaw<S>, Base<S>, SingleRaw<S>, Detail<S>>): {
|
|
75
89
|
useList: (params?: TQueryParams) => UseListReturn<K, Base<S>>;
|
|
76
90
|
useGet: (id: Id<S>, staleTime?: number) => {
|
|
77
91
|
data: _tanstack_react_query.NoInfer<Detail<S>> | undefined;
|
package/dist/index.js
CHANGED
|
@@ -32,6 +32,7 @@ var DEFAULT_PAGINATION = {
|
|
|
32
32
|
limit: 10
|
|
33
33
|
};
|
|
34
34
|
function createResourceHooks(queryKeyPrefix, apiService, options = {}) {
|
|
35
|
+
const service = apiService;
|
|
35
36
|
const {
|
|
36
37
|
adapters = (0, import_core.createDefaultAdapters)(),
|
|
37
38
|
defaultParams = DEFAULT_PAGINATION
|
|
@@ -49,7 +50,7 @@ function createResourceHooks(queryKeyPrefix, apiService, options = {}) {
|
|
|
49
50
|
const query = (0, import_react_query.useQuery)({
|
|
50
51
|
queryKey,
|
|
51
52
|
queryFn: async () => {
|
|
52
|
-
const raw = await
|
|
53
|
+
const raw = await service.list(params);
|
|
53
54
|
return adapters.fromList(raw);
|
|
54
55
|
}
|
|
55
56
|
});
|
|
@@ -75,7 +76,7 @@ function createResourceHooks(queryKeyPrefix, apiService, options = {}) {
|
|
|
75
76
|
const query = (0, import_react_query.useQuery)({
|
|
76
77
|
queryKey: [queryKeyPrefix, id],
|
|
77
78
|
queryFn: async () => {
|
|
78
|
-
const raw = await
|
|
79
|
+
const raw = await service.get(id);
|
|
79
80
|
return adapters.fromSingle(raw);
|
|
80
81
|
},
|
|
81
82
|
enabled: id !== void 0 && id !== null && id !== "",
|
|
@@ -96,21 +97,21 @@ function createResourceHooks(queryKeyPrefix, apiService, options = {}) {
|
|
|
96
97
|
const invalidate = () => queryClient.invalidateQueries({ queryKey: [queryKeyPrefix] });
|
|
97
98
|
const createMutation = (0, import_react_query.useMutation)({
|
|
98
99
|
mutationFn: async (payload) => {
|
|
99
|
-
const raw = await
|
|
100
|
+
const raw = await service.create(payload);
|
|
100
101
|
return adapters.fromSingle(raw);
|
|
101
102
|
},
|
|
102
103
|
onSuccess: invalidate
|
|
103
104
|
});
|
|
104
105
|
const updateMutation = (0, import_react_query.useMutation)({
|
|
105
106
|
mutationFn: async ({ _id, payload }) => {
|
|
106
|
-
const raw = await
|
|
107
|
+
const raw = await service.update(_id, payload);
|
|
107
108
|
return adapters.fromSingle(raw);
|
|
108
109
|
},
|
|
109
110
|
onSuccess: invalidate
|
|
110
111
|
});
|
|
111
112
|
const deleteMutation = (0, import_react_query.useMutation)({
|
|
112
113
|
mutationFn: async (_id) => {
|
|
113
|
-
const raw = await
|
|
114
|
+
const raw = await service.delete(_id);
|
|
114
115
|
return adapters.fromSingle(raw);
|
|
115
116
|
},
|
|
116
117
|
onSuccess: invalidate
|
|
@@ -129,7 +130,7 @@ function createResourceHooks(queryKeyPrefix, apiService, options = {}) {
|
|
|
129
130
|
queryKey: [queryKeyPrefix, "INFINITE", params],
|
|
130
131
|
queryFn: async ({ pageParam }) => {
|
|
131
132
|
var _a;
|
|
132
|
-
const raw = await
|
|
133
|
+
const raw = await service.list({
|
|
133
134
|
...params,
|
|
134
135
|
page: pageParam,
|
|
135
136
|
limit: (_a = params.limit) != null ? _a : 20
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/hooks/createResourceHooks.ts"],"sourcesContent":["export { createResourceHooks } from \"./hooks/createResourceHooks\";\nexport type {\n UseListReturn,\n CreateResourceHooksOptions,\n} from \"./hooks/createResourceHooks\";\n","import {\n useInfiniteQuery,\n useMutation,\n useQuery,\n useQueryClient,\n} from \"@tanstack/react-query\";\nimport type { ResourceService } from \"@void-snippets/client\";\nimport type {\n ResourceAdapters,\n ResourceListResult,\n TPagination,\n TQueryParams,\n TDefaultPaginatedResponse,\n TDefaultSingleResponse,\n} from \"@void-snippets/core\";\nimport { createDefaultAdapters } from \"@void-snippets/core\";\n\n// ============================================================================\n// TYPE HELPERS\n// ============================================================================\n\ntype Capitalize<S extends string> = S extends `${infer F}${infer Rest}`\n ? `${Uppercase<F>}${Rest}`\n : S;\n\nconst DEFAULT_PAGINATION: Required<TQueryParams> = {\n page: 1,\n limit: 10,\n};\n\n// ============================================================================\n// SERVICE GENERIC EXTRACTOR\n// Instead of listing all 7 generics explicitly in createResourceHooks,\n// we capture the whole service as S and extract its generics here.\n// This is what enables full inference across package boundaries — TypeScript\n// only needs to match one type (S) rather than 7 simultaneously.\n//\n// AnyResourceService uses `unknown` (not `any`) as the upper bound —\n// unknown forces proper type narrowing downstream while remaining safe.\n// ============================================================================\n\ntype AnyResourceService = ResourceService<\n never,\n never,\n never,\n never,\n never,\n never,\n never\n>;\n\ntype Id<S extends AnyResourceService> = S[\"__types\"][\"id\"];\ntype Base<S extends AnyResourceService> = S[\"__types\"][\"base\"];\ntype Detail<S extends AnyResourceService> = S[\"__types\"][\"detail\"];\ntype Create<S extends AnyResourceService> = S[\"__types\"][\"create\"];\ntype Update<S extends AnyResourceService> = S[\"__types\"][\"update\"];\ntype ListRaw<S extends AnyResourceService> = S[\"__types\"][\"listRaw\"];\ntype SingleRaw<S extends AnyResourceService> = S[\"__types\"][\"singleRaw\"];\n\n// ============================================================================\n// RETURN TYPE — useList\n// Dynamically named keys based on the queryKeyPrefix (e.g. \"contacts\"):\n// { contacts: TBase[], isContactsLoading: boolean, contactsError: Error | null, invalidateContacts: () => void }\n// ============================================================================\n\nexport type UseListReturn<K extends string, TBase> = {\n [P in K]: TBase[];\n} & {\n pagination: TPagination;\n} & {\n [P in `is${Capitalize<K>}Loading`]: boolean;\n} & {\n [P in `${K}Error`]: Error | null;\n} & {\n [P in `invalidate${Capitalize<K>}`]: () => void;\n};\n\n// ============================================================================\n// OPTIONS\n// ============================================================================\n\nexport interface CreateResourceHooksOptions<\n TListRaw,\n TBase,\n TSingleRaw,\n TDetail,\n> {\n /**\n * Adapters let you map your API's raw response shapes to the library's\n * internal format. If your API matches the default shape\n * ({ data: { items, page, limit, totalPages, totalDocuments } })\n * you can omit this entirely.\n *\n * @example\n * createResourceHooks(\"contacts\", ContactsApis, {\n * adapters: {\n * fromList: (raw) => ({\n * items: raw.results,\n * pagination: { page: raw.page, limit: raw.perPage, totalPages: raw.pages, totalDocuments: raw.total },\n * }),\n * fromSingle: (raw) => raw.payload,\n * }\n * })\n */\n adapters?: ResourceAdapters<TListRaw, TBase, TSingleRaw, TDetail>;\n\n /**\n * Default params passed to useList and useInfinite when none are provided.\n * @default { page: 1, limit: 10 }\n */\n defaultParams?: TQueryParams;\n}\n\n// ============================================================================\n// FACTORY\n// ============================================================================\n\n/**\n * Creates a set of TanStack Query hooks for a resource.\n * All types are fully inferred from the `apiService` instance — no need to\n * pass generics manually.\n *\n * @param queryKeyPrefix - Used as the TanStack Query cache key prefix AND\n * to name the returned hook properties dynamically.\n * e.g. \"contacts\" → { contacts, isContactsLoading, ... }\n * @param apiService - An instance of ResourceService (or a subclass).\n * @param options - Optional adapters and default params.\n *\n * @example\n * // contacts.hooks.ts\n * import { createResourceHooks } from '@void-snippets/react';\n * import { ContactsApis } from './contacts.api';\n *\n * // No generics needed — everything is inferred from ContactsApis\n * export const contactHooks = createResourceHooks('contacts', ContactsApis);\n *\n * // In a component:\n * const { contacts, isContactsLoading } = contactHooks.useList();\n * const { data } = contactHooks.useGet(id); // data typed as Contact.WithCreatedBy\n * const { create, update, delete: deleteContact } = contactHooks.useMutations();\n */\nexport function createResourceHooks<\n K extends string,\n S extends AnyResourceService,\n>(\n queryKeyPrefix: K,\n apiService: S,\n options: CreateResourceHooksOptions<\n ListRaw<S>,\n Base<S>,\n SingleRaw<S>,\n Detail<S>\n > = {},\n) {\n const {\n adapters = createDefaultAdapters<Base<S>, Detail<S>>() as ResourceAdapters<\n TDefaultPaginatedResponse<Base<S>>,\n Base<S>,\n TDefaultSingleResponse<Detail<S>>,\n Detail<S>\n > as ResourceAdapters<ListRaw<S>, Base<S>, SingleRaw<S>, Detail<S>>,\n defaultParams = DEFAULT_PAGINATION,\n } = options;\n\n const capitalize = (str: string) =>\n str.charAt(0).toUpperCase() + str.slice(1);\n const capPrefix = capitalize(queryKeyPrefix);\n\n return {\n // -------------------------------------------------------------------------\n // useList — paginated list with invalidation helper\n // -------------------------------------------------------------------------\n useList: (\n params: TQueryParams = defaultParams,\n ): UseListReturn<K, Base<S>> => {\n const queryClient = useQueryClient();\n const queryKey = [queryKeyPrefix, params];\n\n const query = useQuery<ResourceListResult<Base<S>>, Error>({\n queryKey,\n queryFn: async () => {\n const raw = await apiService.list(params);\n return adapters.fromList(raw as ListRaw<S>);\n },\n });\n\n const items = query.data?.items ?? [];\n const pagination: TPagination = query.data?.pagination ?? {\n page: 1,\n limit: defaultParams.limit ?? 10,\n totalPages: 0,\n totalDocuments: 0,\n };\n\n return {\n [queryKeyPrefix]: items,\n pagination,\n [`is${capPrefix}Loading`]: query.isLoading || query.isFetching,\n [`${queryKeyPrefix}Error`]: query.error,\n [`invalidate${capPrefix}`]: () =>\n queryClient.invalidateQueries({ queryKey: [queryKeyPrefix] }),\n } as UseListReturn<K, Base<S>>;\n },\n\n // -------------------------------------------------------------------------\n // useGet — single item by id\n // -------------------------------------------------------------------------\n useGet: (id: Id<S>, staleTime = 30_000) => {\n const query = useQuery<Detail<S>, Error>({\n queryKey: [queryKeyPrefix, id],\n queryFn: async () => {\n const raw = await apiService.get(id);\n return adapters.fromSingle(raw as SingleRaw<S>);\n },\n enabled: id !== undefined && id !== null && id !== \"\",\n staleTime,\n });\n\n return {\n data: query.data,\n isLoading: query.isLoading || query.isFetching,\n error: query.error,\n refetch: query.refetch,\n };\n },\n\n // -------------------------------------------------------------------------\n // useMutations — create / update / delete with auto-invalidation\n // -------------------------------------------------------------------------\n useMutations: () => {\n const queryClient = useQueryClient();\n const invalidate = () =>\n queryClient.invalidateQueries({ queryKey: [queryKeyPrefix] });\n\n const createMutation = useMutation<Detail<S>, Error, Create<S>>({\n mutationFn: async (payload) => {\n const raw = await apiService.create(payload);\n return adapters.fromSingle(raw as SingleRaw<S>);\n },\n onSuccess: invalidate,\n });\n\n const updateMutation = useMutation<\n Detail<S>,\n Error,\n { _id: Id<S>; payload: Update<S> }\n >({\n mutationFn: async ({ _id, payload }) => {\n const raw = await apiService.update(_id, payload);\n return adapters.fromSingle(raw as SingleRaw<S>);\n },\n onSuccess: invalidate,\n });\n\n const deleteMutation = useMutation<Detail<S>, Error, Id<S>>({\n mutationFn: async (_id) => {\n const raw = await apiService.delete(_id);\n return adapters.fromSingle(raw as SingleRaw<S>);\n },\n onSuccess: invalidate,\n });\n\n return {\n create: createMutation,\n update: updateMutation,\n delete: deleteMutation,\n };\n },\n\n // -------------------------------------------------------------------------\n // useInfinite — infinite scroll / load more\n // -------------------------------------------------------------------------\n useInfinite: (params: TQueryParams = defaultParams) => {\n return useInfiniteQuery<ResourceListResult<Base<S>>, Error>({\n queryKey: [queryKeyPrefix, \"INFINITE\", params],\n queryFn: async ({ pageParam }) => {\n const raw = await apiService.list({\n ...params,\n page: pageParam as number,\n limit: params.limit ?? 20,\n });\n return adapters.fromList(raw as ListRaw<S>);\n },\n getNextPageParam: (lastPage) => {\n const { page, totalPages } = lastPage.pagination;\n return page < totalPages ? page + 1 : undefined;\n },\n initialPageParam: 1,\n });\n },\n };\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/hooks/createResourceHooks.ts"],"sourcesContent":["export { createResourceHooks } from \"./hooks/createResourceHooks\";\nexport type {\n UseListReturn,\n CreateResourceHooksOptions,\n} from \"./hooks/createResourceHooks\";\n","import {\n useInfiniteQuery,\n useMutation,\n useQuery,\n useQueryClient,\n} from \"@tanstack/react-query\";\nimport type { ResourceService } from \"@void-snippets/client\";\nimport type {\n ResourceAdapters,\n ResourceListResult,\n TPagination,\n TQueryParams,\n TDefaultPaginatedResponse,\n TDefaultSingleResponse,\n} from \"@void-snippets/core\";\nimport { createDefaultAdapters } from \"@void-snippets/core\";\n\n// ============================================================================\n// TYPE HELPERS\n// ============================================================================\n\ntype CapitalizeStr<S extends string> = S extends `${infer F}${infer Rest}`\n ? `${Uppercase<F>}${Rest}`\n : S;\n\nconst DEFAULT_PAGINATION: TQueryParams = {\n page: 1,\n limit: 10,\n};\n\n// ============================================================================\n// PHANTOM TYPE CONSTRAINT\n//\n// We constrain S only on the __types phantom property (which is readonly,\n// i.e. covariant) so that ResourceService<string, ...> is assignable to\n// WithResourceTypes. This avoids the variance issue that arises when\n// constraining against method parameter types (contravariant positions).\n//\n// Inside the function we cast apiService once to a fully-typed ResourceService\n// using the phantom generics — this is safe because S IS a ResourceService.\n// ============================================================================\n\ninterface WithResourceTypes {\n readonly __types: {\n id: unknown;\n base: unknown;\n detail: unknown;\n create: unknown;\n update: unknown;\n listRaw: unknown;\n singleRaw: unknown;\n };\n}\n\n// Aliases for reading phantom types from S — keeps the function body readable\ntype Id<S extends WithResourceTypes> = S[\"__types\"][\"id\"];\ntype Base<S extends WithResourceTypes> = S[\"__types\"][\"base\"];\ntype Detail<S extends WithResourceTypes> = S[\"__types\"][\"detail\"];\ntype Create<S extends WithResourceTypes> = S[\"__types\"][\"create\"];\ntype Update<S extends WithResourceTypes> = S[\"__types\"][\"update\"];\ntype ListRaw<S extends WithResourceTypes> = S[\"__types\"][\"listRaw\"];\ntype SingleRaw<S extends WithResourceTypes> = S[\"__types\"][\"singleRaw\"];\n\n// ============================================================================\n// RETURN TYPE — useList\n// Dynamically named keys based on the queryKeyPrefix (e.g. \"contacts\"):\n// { contacts: TBase[], isContactsLoading: boolean, contactsError: Error | null, invalidateContacts: () => void }\n// ============================================================================\n\nexport type UseListReturn<K extends string, TBase> = {\n [P in K]: TBase[];\n} & {\n pagination: TPagination;\n} & {\n [P in `is${CapitalizeStr<K>}Loading`]: boolean;\n} & {\n [P in `${K}Error`]: Error | null;\n} & {\n [P in `invalidate${CapitalizeStr<K>}`]: () => void;\n};\n\n// ============================================================================\n// OPTIONS\n// ============================================================================\n\nexport interface CreateResourceHooksOptions<\n TListRaw,\n TBase,\n TSingleRaw,\n TDetail,\n> {\n /**\n * Adapters map your API's raw response shapes to the library's internal\n * format. Omit this entirely if your API matches the default shape:\n * List: { data: { items, page, limit, totalPages, totalDocuments } }\n * Single: { data: <item> }\n *\n * @example\n * createResourceHooks(\"contacts\", ContactsApis, {\n * adapters: {\n * fromList: (raw) => ({\n * items: raw.results,\n * pagination: {\n * page: raw.meta.page,\n * limit: raw.meta.perPage,\n * totalPages: raw.meta.lastPage,\n * totalDocuments: raw.meta.total,\n * },\n * }),\n * fromSingle: (raw) => raw.payload,\n * },\n * })\n */\n adapters?: ResourceAdapters<TListRaw, TBase, TSingleRaw, TDetail>;\n\n /**\n * Default params passed to useList and useInfinite when none are provided.\n * @default { page: 1, limit: 10 }\n */\n defaultParams?: TQueryParams;\n}\n\n// ============================================================================\n// FACTORY\n// ============================================================================\n\n/**\n * Creates a set of TanStack Query hooks for a resource.\n * All types are fully inferred from the `apiService` instance — no generics\n * need to be passed manually.\n *\n * @param queryKeyPrefix - TanStack Query cache key prefix and the base name\n * for the returned hook properties.\n * e.g. \"contacts\" → { contacts, isContactsLoading, ... }\n * @param apiService - An instance of ResourceService (or a subclass).\n * @param options - Optional adapters and default params.\n *\n * @example\n * // contacts.hooks.ts\n * import { createResourceHooks } from '@void-snippets/react';\n * import { ContactsApis } from './contacts.api';\n *\n * // No generics needed — all types are inferred from ContactsApis\n * export const contactHooks = createResourceHooks('contacts', ContactsApis);\n *\n * // In a component:\n * const { contacts, isContactsLoading } = contactHooks.useList();\n * const { data } = contactHooks.useGet(id); // data: Contact.WithCreatedBy\n * const { create, update, delete: remove } = contactHooks.useMutations();\n */\nexport function createResourceHooks<\n K extends string,\n S extends WithResourceTypes,\n>(\n queryKeyPrefix: K,\n apiService: S,\n options: CreateResourceHooksOptions<\n ListRaw<S>,\n Base<S>,\n SingleRaw<S>,\n Detail<S>\n > = {},\n) {\n // One safe cast: S is guaranteed to be a ResourceService subclass because\n // __types is only present on ResourceService. Casting here lets us call\n // the service methods with the correct types extracted from the phantom.\n const service = apiService as unknown as ResourceService<\n Id<S>,\n Base<S>,\n Detail<S>,\n Create<S>,\n Update<S>,\n ListRaw<S>,\n SingleRaw<S>\n >;\n\n const {\n adapters = createDefaultAdapters<Base<S>, Detail<S>>() as ResourceAdapters<\n TDefaultPaginatedResponse<Base<S>>,\n Base<S>,\n TDefaultSingleResponse<Detail<S>>,\n Detail<S>\n > as ResourceAdapters<ListRaw<S>, Base<S>, SingleRaw<S>, Detail<S>>,\n defaultParams = DEFAULT_PAGINATION,\n } = options;\n\n const capitalize = (str: string) =>\n str.charAt(0).toUpperCase() + str.slice(1);\n const capPrefix = capitalize(queryKeyPrefix);\n\n return {\n // -------------------------------------------------------------------------\n // useList — paginated list with invalidation helper\n // -------------------------------------------------------------------------\n useList: (\n params: TQueryParams = defaultParams,\n ): UseListReturn<K, Base<S>> => {\n const queryClient = useQueryClient();\n const queryKey = [queryKeyPrefix, params];\n\n const query = useQuery<ResourceListResult<Base<S>>, Error>({\n queryKey,\n queryFn: async () => {\n const raw = await service.list(params);\n return adapters.fromList(raw as ListRaw<S>);\n },\n });\n\n const items = query.data?.items ?? [];\n const pagination: TPagination = query.data?.pagination ?? {\n page: 1,\n limit: defaultParams.limit ?? 10,\n totalPages: 0,\n totalDocuments: 0,\n };\n\n return {\n [queryKeyPrefix]: items,\n pagination,\n [`is${capPrefix}Loading`]: query.isLoading || query.isFetching,\n [`${queryKeyPrefix}Error`]: query.error,\n [`invalidate${capPrefix}`]: () =>\n queryClient.invalidateQueries({ queryKey: [queryKeyPrefix] }),\n } as UseListReturn<K, Base<S>>;\n },\n\n // -------------------------------------------------------------------------\n // useGet — single item by id\n // -------------------------------------------------------------------------\n useGet: (id: Id<S>, staleTime = 30_000) => {\n const query = useQuery<Detail<S>, Error>({\n queryKey: [queryKeyPrefix, id],\n queryFn: async () => {\n const raw = await service.get(id);\n return adapters.fromSingle(raw as SingleRaw<S>);\n },\n enabled: id !== undefined && id !== null && id !== \"\",\n staleTime,\n });\n\n return {\n data: query.data,\n isLoading: query.isLoading || query.isFetching,\n error: query.error,\n refetch: query.refetch,\n };\n },\n\n // -------------------------------------------------------------------------\n // useMutations — create / update / delete with auto-invalidation\n // -------------------------------------------------------------------------\n useMutations: () => {\n const queryClient = useQueryClient();\n const invalidate = () =>\n queryClient.invalidateQueries({ queryKey: [queryKeyPrefix] });\n\n const createMutation = useMutation<Detail<S>, Error, Create<S>>({\n mutationFn: async (payload) => {\n const raw = await service.create(payload);\n return adapters.fromSingle(raw as SingleRaw<S>);\n },\n onSuccess: invalidate,\n });\n\n const updateMutation = useMutation<\n Detail<S>,\n Error,\n { _id: Id<S>; payload: Update<S> }\n >({\n mutationFn: async ({ _id, payload }) => {\n const raw = await service.update(_id, payload);\n return adapters.fromSingle(raw as SingleRaw<S>);\n },\n onSuccess: invalidate,\n });\n\n const deleteMutation = useMutation<Detail<S>, Error, Id<S>>({\n mutationFn: async (_id) => {\n const raw = await service.delete(_id);\n return adapters.fromSingle(raw as SingleRaw<S>);\n },\n onSuccess: invalidate,\n });\n\n return {\n create: createMutation,\n update: updateMutation,\n delete: deleteMutation,\n };\n },\n\n // -------------------------------------------------------------------------\n // useInfinite — infinite scroll / load more\n // -------------------------------------------------------------------------\n useInfinite: (params: TQueryParams = defaultParams) => {\n return useInfiniteQuery<ResourceListResult<Base<S>>, Error>({\n queryKey: [queryKeyPrefix, \"INFINITE\", params],\n queryFn: async ({ pageParam }) => {\n const raw = await service.list({\n ...params,\n page: pageParam as number,\n limit: params.limit ?? 20,\n });\n return adapters.fromList(raw as ListRaw<S>);\n },\n getNextPageParam: (lastPage) => {\n const { page, totalPages } = lastPage.pagination;\n return page < totalPages ? page + 1 : undefined;\n },\n initialPageParam: 1,\n });\n },\n };\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.mjs
CHANGED
|
@@ -11,6 +11,7 @@ var DEFAULT_PAGINATION = {
|
|
|
11
11
|
limit: 10
|
|
12
12
|
};
|
|
13
13
|
function createResourceHooks(queryKeyPrefix, apiService, options = {}) {
|
|
14
|
+
const service = apiService;
|
|
14
15
|
const {
|
|
15
16
|
adapters = createDefaultAdapters(),
|
|
16
17
|
defaultParams = DEFAULT_PAGINATION
|
|
@@ -28,7 +29,7 @@ function createResourceHooks(queryKeyPrefix, apiService, options = {}) {
|
|
|
28
29
|
const query = useQuery({
|
|
29
30
|
queryKey,
|
|
30
31
|
queryFn: async () => {
|
|
31
|
-
const raw = await
|
|
32
|
+
const raw = await service.list(params);
|
|
32
33
|
return adapters.fromList(raw);
|
|
33
34
|
}
|
|
34
35
|
});
|
|
@@ -54,7 +55,7 @@ function createResourceHooks(queryKeyPrefix, apiService, options = {}) {
|
|
|
54
55
|
const query = useQuery({
|
|
55
56
|
queryKey: [queryKeyPrefix, id],
|
|
56
57
|
queryFn: async () => {
|
|
57
|
-
const raw = await
|
|
58
|
+
const raw = await service.get(id);
|
|
58
59
|
return adapters.fromSingle(raw);
|
|
59
60
|
},
|
|
60
61
|
enabled: id !== void 0 && id !== null && id !== "",
|
|
@@ -75,21 +76,21 @@ function createResourceHooks(queryKeyPrefix, apiService, options = {}) {
|
|
|
75
76
|
const invalidate = () => queryClient.invalidateQueries({ queryKey: [queryKeyPrefix] });
|
|
76
77
|
const createMutation = useMutation({
|
|
77
78
|
mutationFn: async (payload) => {
|
|
78
|
-
const raw = await
|
|
79
|
+
const raw = await service.create(payload);
|
|
79
80
|
return adapters.fromSingle(raw);
|
|
80
81
|
},
|
|
81
82
|
onSuccess: invalidate
|
|
82
83
|
});
|
|
83
84
|
const updateMutation = useMutation({
|
|
84
85
|
mutationFn: async ({ _id, payload }) => {
|
|
85
|
-
const raw = await
|
|
86
|
+
const raw = await service.update(_id, payload);
|
|
86
87
|
return adapters.fromSingle(raw);
|
|
87
88
|
},
|
|
88
89
|
onSuccess: invalidate
|
|
89
90
|
});
|
|
90
91
|
const deleteMutation = useMutation({
|
|
91
92
|
mutationFn: async (_id) => {
|
|
92
|
-
const raw = await
|
|
93
|
+
const raw = await service.delete(_id);
|
|
93
94
|
return adapters.fromSingle(raw);
|
|
94
95
|
},
|
|
95
96
|
onSuccess: invalidate
|
|
@@ -108,7 +109,7 @@ function createResourceHooks(queryKeyPrefix, apiService, options = {}) {
|
|
|
108
109
|
queryKey: [queryKeyPrefix, "INFINITE", params],
|
|
109
110
|
queryFn: async ({ pageParam }) => {
|
|
110
111
|
var _a;
|
|
111
|
-
const raw = await
|
|
112
|
+
const raw = await service.list({
|
|
112
113
|
...params,
|
|
113
114
|
page: pageParam,
|
|
114
115
|
limit: (_a = params.limit) != null ? _a : 20
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/hooks/createResourceHooks.ts"],"sourcesContent":["import {\n useInfiniteQuery,\n useMutation,\n useQuery,\n useQueryClient,\n} from \"@tanstack/react-query\";\nimport type { ResourceService } from \"@void-snippets/client\";\nimport type {\n ResourceAdapters,\n ResourceListResult,\n TPagination,\n TQueryParams,\n TDefaultPaginatedResponse,\n TDefaultSingleResponse,\n} from \"@void-snippets/core\";\nimport { createDefaultAdapters } from \"@void-snippets/core\";\n\n// ============================================================================\n// TYPE HELPERS\n// ============================================================================\n\ntype Capitalize<S extends string> = S extends `${infer F}${infer Rest}`\n ? `${Uppercase<F>}${Rest}`\n : S;\n\nconst DEFAULT_PAGINATION: Required<TQueryParams> = {\n page: 1,\n limit: 10,\n};\n\n// ============================================================================\n// SERVICE GENERIC EXTRACTOR\n// Instead of listing all 7 generics explicitly in createResourceHooks,\n// we capture the whole service as S and extract its generics here.\n// This is what enables full inference across package boundaries — TypeScript\n// only needs to match one type (S) rather than 7 simultaneously.\n//\n// AnyResourceService uses `unknown` (not `any`) as the upper bound —\n// unknown forces proper type narrowing downstream while remaining safe.\n// ============================================================================\n\ntype AnyResourceService = ResourceService<\n never,\n never,\n never,\n never,\n never,\n never,\n never\n>;\n\ntype Id<S extends AnyResourceService> = S[\"__types\"][\"id\"];\ntype Base<S extends AnyResourceService> = S[\"__types\"][\"base\"];\ntype Detail<S extends AnyResourceService> = S[\"__types\"][\"detail\"];\ntype Create<S extends AnyResourceService> = S[\"__types\"][\"create\"];\ntype Update<S extends AnyResourceService> = S[\"__types\"][\"update\"];\ntype ListRaw<S extends AnyResourceService> = S[\"__types\"][\"listRaw\"];\ntype SingleRaw<S extends AnyResourceService> = S[\"__types\"][\"singleRaw\"];\n\n// ============================================================================\n// RETURN TYPE — useList\n// Dynamically named keys based on the queryKeyPrefix (e.g. \"contacts\"):\n// { contacts: TBase[], isContactsLoading: boolean, contactsError: Error | null, invalidateContacts: () => void }\n// ============================================================================\n\nexport type UseListReturn<K extends string, TBase> = {\n [P in K]: TBase[];\n} & {\n pagination: TPagination;\n} & {\n [P in `is${Capitalize<K>}Loading`]: boolean;\n} & {\n [P in `${K}Error`]: Error | null;\n} & {\n [P in `invalidate${Capitalize<K>}`]: () => void;\n};\n\n// ============================================================================\n// OPTIONS\n// ============================================================================\n\nexport interface CreateResourceHooksOptions<\n TListRaw,\n TBase,\n TSingleRaw,\n TDetail,\n> {\n /**\n * Adapters let you map your API's raw response shapes to the library's\n * internal format. If your API matches the default shape\n * ({ data: { items, page, limit, totalPages, totalDocuments } })\n * you can omit this entirely.\n *\n * @example\n * createResourceHooks(\"contacts\", ContactsApis, {\n * adapters: {\n * fromList: (raw) => ({\n * items: raw.results,\n * pagination: { page: raw.page, limit: raw.perPage, totalPages: raw.pages, totalDocuments: raw.total },\n * }),\n * fromSingle: (raw) => raw.payload,\n * }\n * })\n */\n adapters?: ResourceAdapters<TListRaw, TBase, TSingleRaw, TDetail>;\n\n /**\n * Default params passed to useList and useInfinite when none are provided.\n * @default { page: 1, limit: 10 }\n */\n defaultParams?: TQueryParams;\n}\n\n// ============================================================================\n// FACTORY\n// ============================================================================\n\n/**\n * Creates a set of TanStack Query hooks for a resource.\n * All types are fully inferred from the `apiService` instance — no need to\n * pass generics manually.\n *\n * @param queryKeyPrefix - Used as the TanStack Query cache key prefix AND\n * to name the returned hook properties dynamically.\n * e.g. \"contacts\" → { contacts, isContactsLoading, ... }\n * @param apiService - An instance of ResourceService (or a subclass).\n * @param options - Optional adapters and default params.\n *\n * @example\n * // contacts.hooks.ts\n * import { createResourceHooks } from '@void-snippets/react';\n * import { ContactsApis } from './contacts.api';\n *\n * // No generics needed — everything is inferred from ContactsApis\n * export const contactHooks = createResourceHooks('contacts', ContactsApis);\n *\n * // In a component:\n * const { contacts, isContactsLoading } = contactHooks.useList();\n * const { data } = contactHooks.useGet(id); // data typed as Contact.WithCreatedBy\n * const { create, update, delete: deleteContact } = contactHooks.useMutations();\n */\nexport function createResourceHooks<\n K extends string,\n S extends AnyResourceService,\n>(\n queryKeyPrefix: K,\n apiService: S,\n options: CreateResourceHooksOptions<\n ListRaw<S>,\n Base<S>,\n SingleRaw<S>,\n Detail<S>\n > = {},\n) {\n const {\n adapters = createDefaultAdapters<Base<S>, Detail<S>>() as ResourceAdapters<\n TDefaultPaginatedResponse<Base<S>>,\n Base<S>,\n TDefaultSingleResponse<Detail<S>>,\n Detail<S>\n > as ResourceAdapters<ListRaw<S>, Base<S>, SingleRaw<S>, Detail<S>>,\n defaultParams = DEFAULT_PAGINATION,\n } = options;\n\n const capitalize = (str: string) =>\n str.charAt(0).toUpperCase() + str.slice(1);\n const capPrefix = capitalize(queryKeyPrefix);\n\n return {\n // -------------------------------------------------------------------------\n // useList — paginated list with invalidation helper\n // -------------------------------------------------------------------------\n useList: (\n params: TQueryParams = defaultParams,\n ): UseListReturn<K, Base<S>> => {\n const queryClient = useQueryClient();\n const queryKey = [queryKeyPrefix, params];\n\n const query = useQuery<ResourceListResult<Base<S>>, Error>({\n queryKey,\n queryFn: async () => {\n const raw = await apiService.list(params);\n return adapters.fromList(raw as ListRaw<S>);\n },\n });\n\n const items = query.data?.items ?? [];\n const pagination: TPagination = query.data?.pagination ?? {\n page: 1,\n limit: defaultParams.limit ?? 10,\n totalPages: 0,\n totalDocuments: 0,\n };\n\n return {\n [queryKeyPrefix]: items,\n pagination,\n [`is${capPrefix}Loading`]: query.isLoading || query.isFetching,\n [`${queryKeyPrefix}Error`]: query.error,\n [`invalidate${capPrefix}`]: () =>\n queryClient.invalidateQueries({ queryKey: [queryKeyPrefix] }),\n } as UseListReturn<K, Base<S>>;\n },\n\n // -------------------------------------------------------------------------\n // useGet — single item by id\n // -------------------------------------------------------------------------\n useGet: (id: Id<S>, staleTime = 30_000) => {\n const query = useQuery<Detail<S>, Error>({\n queryKey: [queryKeyPrefix, id],\n queryFn: async () => {\n const raw = await apiService.get(id);\n return adapters.fromSingle(raw as SingleRaw<S>);\n },\n enabled: id !== undefined && id !== null && id !== \"\",\n staleTime,\n });\n\n return {\n data: query.data,\n isLoading: query.isLoading || query.isFetching,\n error: query.error,\n refetch: query.refetch,\n };\n },\n\n // -------------------------------------------------------------------------\n // useMutations — create / update / delete with auto-invalidation\n // -------------------------------------------------------------------------\n useMutations: () => {\n const queryClient = useQueryClient();\n const invalidate = () =>\n queryClient.invalidateQueries({ queryKey: [queryKeyPrefix] });\n\n const createMutation = useMutation<Detail<S>, Error, Create<S>>({\n mutationFn: async (payload) => {\n const raw = await apiService.create(payload);\n return adapters.fromSingle(raw as SingleRaw<S>);\n },\n onSuccess: invalidate,\n });\n\n const updateMutation = useMutation<\n Detail<S>,\n Error,\n { _id: Id<S>; payload: Update<S> }\n >({\n mutationFn: async ({ _id, payload }) => {\n const raw = await apiService.update(_id, payload);\n return adapters.fromSingle(raw as SingleRaw<S>);\n },\n onSuccess: invalidate,\n });\n\n const deleteMutation = useMutation<Detail<S>, Error, Id<S>>({\n mutationFn: async (_id) => {\n const raw = await apiService.delete(_id);\n return adapters.fromSingle(raw as SingleRaw<S>);\n },\n onSuccess: invalidate,\n });\n\n return {\n create: createMutation,\n update: updateMutation,\n delete: deleteMutation,\n };\n },\n\n // -------------------------------------------------------------------------\n // useInfinite — infinite scroll / load more\n // -------------------------------------------------------------------------\n useInfinite: (params: TQueryParams = defaultParams) => {\n return useInfiniteQuery<ResourceListResult<Base<S>>, Error>({\n queryKey: [queryKeyPrefix, \"INFINITE\", params],\n queryFn: async ({ pageParam }) => {\n const raw = await apiService.list({\n ...params,\n page: pageParam as number,\n limit: params.limit ?? 20,\n });\n return adapters.fromList(raw as ListRaw<S>);\n },\n getNextPageParam: (lastPage) => {\n const { page, totalPages } = lastPage.pagination;\n return page < totalPages ? page + 1 : undefined;\n },\n initialPageParam: 1,\n });\n },\n };\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/hooks/createResourceHooks.ts"],"sourcesContent":["import {\n useInfiniteQuery,\n useMutation,\n useQuery,\n useQueryClient,\n} from \"@tanstack/react-query\";\nimport type { ResourceService } from \"@void-snippets/client\";\nimport type {\n ResourceAdapters,\n ResourceListResult,\n TPagination,\n TQueryParams,\n TDefaultPaginatedResponse,\n TDefaultSingleResponse,\n} from \"@void-snippets/core\";\nimport { createDefaultAdapters } from \"@void-snippets/core\";\n\n// ============================================================================\n// TYPE HELPERS\n// ============================================================================\n\ntype CapitalizeStr<S extends string> = S extends `${infer F}${infer Rest}`\n ? `${Uppercase<F>}${Rest}`\n : S;\n\nconst DEFAULT_PAGINATION: TQueryParams = {\n page: 1,\n limit: 10,\n};\n\n// ============================================================================\n// PHANTOM TYPE CONSTRAINT\n//\n// We constrain S only on the __types phantom property (which is readonly,\n// i.e. covariant) so that ResourceService<string, ...> is assignable to\n// WithResourceTypes. This avoids the variance issue that arises when\n// constraining against method parameter types (contravariant positions).\n//\n// Inside the function we cast apiService once to a fully-typed ResourceService\n// using the phantom generics — this is safe because S IS a ResourceService.\n// ============================================================================\n\ninterface WithResourceTypes {\n readonly __types: {\n id: unknown;\n base: unknown;\n detail: unknown;\n create: unknown;\n update: unknown;\n listRaw: unknown;\n singleRaw: unknown;\n };\n}\n\n// Aliases for reading phantom types from S — keeps the function body readable\ntype Id<S extends WithResourceTypes> = S[\"__types\"][\"id\"];\ntype Base<S extends WithResourceTypes> = S[\"__types\"][\"base\"];\ntype Detail<S extends WithResourceTypes> = S[\"__types\"][\"detail\"];\ntype Create<S extends WithResourceTypes> = S[\"__types\"][\"create\"];\ntype Update<S extends WithResourceTypes> = S[\"__types\"][\"update\"];\ntype ListRaw<S extends WithResourceTypes> = S[\"__types\"][\"listRaw\"];\ntype SingleRaw<S extends WithResourceTypes> = S[\"__types\"][\"singleRaw\"];\n\n// ============================================================================\n// RETURN TYPE — useList\n// Dynamically named keys based on the queryKeyPrefix (e.g. \"contacts\"):\n// { contacts: TBase[], isContactsLoading: boolean, contactsError: Error | null, invalidateContacts: () => void }\n// ============================================================================\n\nexport type UseListReturn<K extends string, TBase> = {\n [P in K]: TBase[];\n} & {\n pagination: TPagination;\n} & {\n [P in `is${CapitalizeStr<K>}Loading`]: boolean;\n} & {\n [P in `${K}Error`]: Error | null;\n} & {\n [P in `invalidate${CapitalizeStr<K>}`]: () => void;\n};\n\n// ============================================================================\n// OPTIONS\n// ============================================================================\n\nexport interface CreateResourceHooksOptions<\n TListRaw,\n TBase,\n TSingleRaw,\n TDetail,\n> {\n /**\n * Adapters map your API's raw response shapes to the library's internal\n * format. Omit this entirely if your API matches the default shape:\n * List: { data: { items, page, limit, totalPages, totalDocuments } }\n * Single: { data: <item> }\n *\n * @example\n * createResourceHooks(\"contacts\", ContactsApis, {\n * adapters: {\n * fromList: (raw) => ({\n * items: raw.results,\n * pagination: {\n * page: raw.meta.page,\n * limit: raw.meta.perPage,\n * totalPages: raw.meta.lastPage,\n * totalDocuments: raw.meta.total,\n * },\n * }),\n * fromSingle: (raw) => raw.payload,\n * },\n * })\n */\n adapters?: ResourceAdapters<TListRaw, TBase, TSingleRaw, TDetail>;\n\n /**\n * Default params passed to useList and useInfinite when none are provided.\n * @default { page: 1, limit: 10 }\n */\n defaultParams?: TQueryParams;\n}\n\n// ============================================================================\n// FACTORY\n// ============================================================================\n\n/**\n * Creates a set of TanStack Query hooks for a resource.\n * All types are fully inferred from the `apiService` instance — no generics\n * need to be passed manually.\n *\n * @param queryKeyPrefix - TanStack Query cache key prefix and the base name\n * for the returned hook properties.\n * e.g. \"contacts\" → { contacts, isContactsLoading, ... }\n * @param apiService - An instance of ResourceService (or a subclass).\n * @param options - Optional adapters and default params.\n *\n * @example\n * // contacts.hooks.ts\n * import { createResourceHooks } from '@void-snippets/react';\n * import { ContactsApis } from './contacts.api';\n *\n * // No generics needed — all types are inferred from ContactsApis\n * export const contactHooks = createResourceHooks('contacts', ContactsApis);\n *\n * // In a component:\n * const { contacts, isContactsLoading } = contactHooks.useList();\n * const { data } = contactHooks.useGet(id); // data: Contact.WithCreatedBy\n * const { create, update, delete: remove } = contactHooks.useMutations();\n */\nexport function createResourceHooks<\n K extends string,\n S extends WithResourceTypes,\n>(\n queryKeyPrefix: K,\n apiService: S,\n options: CreateResourceHooksOptions<\n ListRaw<S>,\n Base<S>,\n SingleRaw<S>,\n Detail<S>\n > = {},\n) {\n // One safe cast: S is guaranteed to be a ResourceService subclass because\n // __types is only present on ResourceService. Casting here lets us call\n // the service methods with the correct types extracted from the phantom.\n const service = apiService as unknown as ResourceService<\n Id<S>,\n Base<S>,\n Detail<S>,\n Create<S>,\n Update<S>,\n ListRaw<S>,\n SingleRaw<S>\n >;\n\n const {\n adapters = createDefaultAdapters<Base<S>, Detail<S>>() as ResourceAdapters<\n TDefaultPaginatedResponse<Base<S>>,\n Base<S>,\n TDefaultSingleResponse<Detail<S>>,\n Detail<S>\n > as ResourceAdapters<ListRaw<S>, Base<S>, SingleRaw<S>, Detail<S>>,\n defaultParams = DEFAULT_PAGINATION,\n } = options;\n\n const capitalize = (str: string) =>\n str.charAt(0).toUpperCase() + str.slice(1);\n const capPrefix = capitalize(queryKeyPrefix);\n\n return {\n // -------------------------------------------------------------------------\n // useList — paginated list with invalidation helper\n // -------------------------------------------------------------------------\n useList: (\n params: TQueryParams = defaultParams,\n ): UseListReturn<K, Base<S>> => {\n const queryClient = useQueryClient();\n const queryKey = [queryKeyPrefix, params];\n\n const query = useQuery<ResourceListResult<Base<S>>, Error>({\n queryKey,\n queryFn: async () => {\n const raw = await service.list(params);\n return adapters.fromList(raw as ListRaw<S>);\n },\n });\n\n const items = query.data?.items ?? [];\n const pagination: TPagination = query.data?.pagination ?? {\n page: 1,\n limit: defaultParams.limit ?? 10,\n totalPages: 0,\n totalDocuments: 0,\n };\n\n return {\n [queryKeyPrefix]: items,\n pagination,\n [`is${capPrefix}Loading`]: query.isLoading || query.isFetching,\n [`${queryKeyPrefix}Error`]: query.error,\n [`invalidate${capPrefix}`]: () =>\n queryClient.invalidateQueries({ queryKey: [queryKeyPrefix] }),\n } as UseListReturn<K, Base<S>>;\n },\n\n // -------------------------------------------------------------------------\n // useGet — single item by id\n // -------------------------------------------------------------------------\n useGet: (id: Id<S>, staleTime = 30_000) => {\n const query = useQuery<Detail<S>, Error>({\n queryKey: [queryKeyPrefix, id],\n queryFn: async () => {\n const raw = await service.get(id);\n return adapters.fromSingle(raw as SingleRaw<S>);\n },\n enabled: id !== undefined && id !== null && id !== \"\",\n staleTime,\n });\n\n return {\n data: query.data,\n isLoading: query.isLoading || query.isFetching,\n error: query.error,\n refetch: query.refetch,\n };\n },\n\n // -------------------------------------------------------------------------\n // useMutations — create / update / delete with auto-invalidation\n // -------------------------------------------------------------------------\n useMutations: () => {\n const queryClient = useQueryClient();\n const invalidate = () =>\n queryClient.invalidateQueries({ queryKey: [queryKeyPrefix] });\n\n const createMutation = useMutation<Detail<S>, Error, Create<S>>({\n mutationFn: async (payload) => {\n const raw = await service.create(payload);\n return adapters.fromSingle(raw as SingleRaw<S>);\n },\n onSuccess: invalidate,\n });\n\n const updateMutation = useMutation<\n Detail<S>,\n Error,\n { _id: Id<S>; payload: Update<S> }\n >({\n mutationFn: async ({ _id, payload }) => {\n const raw = await service.update(_id, payload);\n return adapters.fromSingle(raw as SingleRaw<S>);\n },\n onSuccess: invalidate,\n });\n\n const deleteMutation = useMutation<Detail<S>, Error, Id<S>>({\n mutationFn: async (_id) => {\n const raw = await service.delete(_id);\n return adapters.fromSingle(raw as SingleRaw<S>);\n },\n onSuccess: invalidate,\n });\n\n return {\n create: createMutation,\n update: updateMutation,\n delete: deleteMutation,\n };\n },\n\n // -------------------------------------------------------------------------\n // useInfinite — infinite scroll / load more\n // -------------------------------------------------------------------------\n useInfinite: (params: TQueryParams = defaultParams) => {\n return useInfiniteQuery<ResourceListResult<Base<S>>, Error>({\n queryKey: [queryKeyPrefix, \"INFINITE\", params],\n queryFn: async ({ pageParam }) => {\n const raw = await service.list({\n ...params,\n page: pageParam as number,\n limit: params.limit ?? 20,\n });\n return adapters.fromList(raw as ListRaw<S>);\n },\n getNextPageParam: (lastPage) => {\n const { page, totalPages } = lastPage.pagination;\n return page < totalPages ? page + 1 : undefined;\n },\n initialPageParam: 1,\n });\n },\n };\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@void-snippets/react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "TanStack Query resource hooks factory for React",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -26,11 +26,11 @@
|
|
|
26
26
|
"license": "MIT",
|
|
27
27
|
"peerDependencies": {
|
|
28
28
|
"react": ">=17.0.0",
|
|
29
|
-
"@void-snippets/client": "0.1.
|
|
29
|
+
"@void-snippets/client": ">=0.1.0"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"@tanstack/react-query": "^5.0.0",
|
|
33
|
-
"@void-snippets/core": "0.
|
|
33
|
+
"@void-snippets/core": "0.2.0"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"@types/react": "^18.0.0",
|