@void-snippets/react 0.1.4 → 0.1.6
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 +7 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
package/dist/index.d.mts
CHANGED
|
@@ -3,7 +3,13 @@ import { ResourceService } from '@void-snippets/client';
|
|
|
3
3
|
import { ResourceAdapters, TQueryParams, TPagination, ResourceListResult } from '@void-snippets/core';
|
|
4
4
|
|
|
5
5
|
type Capitalize<S extends string> = S extends `${infer F}${infer Rest}` ? `${Uppercase<F>}${Rest}` : S;
|
|
6
|
-
type AnyResourceService = ResourceService<
|
|
6
|
+
type AnyResourceService = ResourceService<never, // TId — input (get/update/delete param) → contravariant → never
|
|
7
|
+
unknown, // TBase — output (list response item) → covariant → unknown
|
|
8
|
+
unknown, // TDetail — output (single response item) → covariant → unknown
|
|
9
|
+
never, // TCreate — input (create payload) → contravariant → never
|
|
10
|
+
never, // TUpdate — input (update payload) → contravariant → never
|
|
11
|
+
unknown, // TListRaw — output → covariant → unknown
|
|
12
|
+
unknown>;
|
|
7
13
|
type ServiceGenerics<S extends AnyResourceService> = S extends ResourceService<infer TId, infer TBase, infer TDetail, infer TCreate, infer TUpdate, infer TListRaw, infer TSingleRaw> ? {
|
|
8
14
|
TId: TId;
|
|
9
15
|
TBase: TBase;
|
package/dist/index.d.ts
CHANGED
|
@@ -3,7 +3,13 @@ import { ResourceService } from '@void-snippets/client';
|
|
|
3
3
|
import { ResourceAdapters, TQueryParams, TPagination, ResourceListResult } from '@void-snippets/core';
|
|
4
4
|
|
|
5
5
|
type Capitalize<S extends string> = S extends `${infer F}${infer Rest}` ? `${Uppercase<F>}${Rest}` : S;
|
|
6
|
-
type AnyResourceService = ResourceService<
|
|
6
|
+
type AnyResourceService = ResourceService<never, // TId — input (get/update/delete param) → contravariant → never
|
|
7
|
+
unknown, // TBase — output (list response item) → covariant → unknown
|
|
8
|
+
unknown, // TDetail — output (single response item) → covariant → unknown
|
|
9
|
+
never, // TCreate — input (create payload) → contravariant → never
|
|
10
|
+
never, // TUpdate — input (update payload) → contravariant → never
|
|
11
|
+
unknown, // TListRaw — output → covariant → unknown
|
|
12
|
+
unknown>;
|
|
7
13
|
type ServiceGenerics<S extends AnyResourceService> = S extends ResourceService<infer TId, infer TBase, infer TDetail, infer TCreate, infer TUpdate, infer TListRaw, infer TSingleRaw> ? {
|
|
8
14
|
TId: TId;
|
|
9
15
|
TBase: TBase;
|
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 unknown,\n unknown,\n unknown,\n unknown,\n unknown,\n unknown,\n unknown\n>;\n\ntype ServiceGenerics<S extends AnyResourceService> =\n S extends ResourceService<\n infer TId,\n infer TBase,\n infer TDetail,\n infer TCreate,\n infer TUpdate,\n infer TListRaw,\n infer TSingleRaw\n >\n ? {\n TId: TId;\n TBase: TBase;\n TDetail: TDetail;\n TCreate: TCreate;\n TUpdate: TUpdate;\n TListRaw: TListRaw;\n TSingleRaw: TSingleRaw;\n }\n : never;\n\n// Convenience aliases so usage inside the function body stays readable\ntype Id<S extends AnyResourceService> = ServiceGenerics<S>[\"TId\"];\ntype Base<S extends AnyResourceService> = ServiceGenerics<S>[\"TBase\"];\ntype Detail<S extends AnyResourceService> = ServiceGenerics<S>[\"TDetail\"];\ntype Create<S extends AnyResourceService> = ServiceGenerics<S>[\"TCreate\"];\ntype Update<S extends AnyResourceService> = ServiceGenerics<S>[\"TUpdate\"];\ntype ListRaw<S extends AnyResourceService> = ServiceGenerics<S>[\"TListRaw\"];\ntype SingleRaw<S extends AnyResourceService> = ServiceGenerics<S>[\"TSingleRaw\"];\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":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,yBAKO;AAUP,kBAAsC;AAUtC,IAAM,qBAA6C;AAAA,EACjD,MAAM;AAAA,EACN,OAAO;AACT;AAuIO,SAAS,oBAId,gBACA,YACA,UAKI,CAAC,GACL;AACA,QAAM;AAAA,IACJ,eAAW,mCAA0C;AAAA,IAMrD,gBAAgB;AAAA,EAClB,IAAI;AAEJ,QAAM,aAAa,CAAC,QAClB,IAAI,OAAO,CAAC,EAAE,YAAY,IAAI,IAAI,MAAM,CAAC;AAC3C,QAAM,YAAY,WAAW,cAAc;AAE3C,SAAO;AAAA;AAAA;AAAA;AAAA,IAIL,SAAS,CACP,SAAuB,kBACO;AApMpC;AAqMM,YAAM,kBAAc,mCAAe;AACnC,YAAM,WAAW,CAAC,gBAAgB,MAAM;AAExC,YAAM,YAAQ,6BAA6C;AAAA,QACzD;AAAA,QACA,SAAS,YAAY;AACnB,gBAAM,MAAM,MAAM,WAAW,KAAK,MAAM;AACxC,iBAAO,SAAS,SAAS,GAAiB;AAAA,QAC5C;AAAA,MACF,CAAC;AAED,YAAM,SAAQ,iBAAM,SAAN,mBAAY,UAAZ,YAAqB,CAAC;AACpC,YAAM,cAA0B,iBAAM,SAAN,mBAAY,eAAZ,YAA0B;AAAA,QACxD,MAAM;AAAA,QACN,QAAO,mBAAc,UAAd,YAAuB;AAAA,QAC9B,YAAY;AAAA,QACZ,gBAAgB;AAAA,MAClB;AAEA,aAAO;AAAA,QACL,CAAC,cAAc,GAAG;AAAA,QAClB;AAAA,QACA,CAAC,KAAK,SAAS,SAAS,GAAG,MAAM,aAAa,MAAM;AAAA,QACpD,CAAC,GAAG,cAAc,OAAO,GAAG,MAAM;AAAA,QAClC,CAAC,aAAa,SAAS,EAAE,GAAG,MAC1B,YAAY,kBAAkB,EAAE,UAAU,CAAC,cAAc,EAAE,CAAC;AAAA,MAChE;AAAA,IACF;AAAA;AAAA;AAAA;AAAA,IAKA,QAAQ,CAAC,IAAW,YAAY,QAAW;AACzC,YAAM,YAAQ,6BAA2B;AAAA,QACvC,UAAU,CAAC,gBAAgB,EAAE;AAAA,QAC7B,SAAS,YAAY;AACnB,gBAAM,MAAM,MAAM,WAAW,IAAI,EAAE;AACnC,iBAAO,SAAS,WAAW,GAAmB;AAAA,QAChD;AAAA,QACA,SAAS,OAAO,UAAa,OAAO,QAAQ,OAAO;AAAA,QACnD;AAAA,MACF,CAAC;AAED,aAAO;AAAA,QACL,MAAM,MAAM;AAAA,QACZ,WAAW,MAAM,aAAa,MAAM;AAAA,QACpC,OAAO,MAAM;AAAA,QACb,SAAS,MAAM;AAAA,MACjB;AAAA,IACF;AAAA;AAAA;AAAA;AAAA,IAKA,cAAc,MAAM;AAClB,YAAM,kBAAc,mCAAe;AACnC,YAAM,aAAa,MACjB,YAAY,kBAAkB,EAAE,UAAU,CAAC,cAAc,EAAE,CAAC;AAE9D,YAAM,qBAAiB,gCAAyC;AAAA,QAC9D,YAAY,OAAO,YAAY;AAC7B,gBAAM,MAAM,MAAM,WAAW,OAAO,OAAO;AAC3C,iBAAO,SAAS,WAAW,GAAmB;AAAA,QAChD;AAAA,QACA,WAAW;AAAA,MACb,CAAC;AAED,YAAM,qBAAiB,gCAIrB;AAAA,QACA,YAAY,OAAO,EAAE,KAAK,QAAQ,MAAM;AACtC,gBAAM,MAAM,MAAM,WAAW,OAAO,KAAK,OAAO;AAChD,iBAAO,SAAS,WAAW,GAAmB;AAAA,QAChD;AAAA,QACA,WAAW;AAAA,MACb,CAAC;AAED,YAAM,qBAAiB,gCAAqC;AAAA,QAC1D,YAAY,OAAO,QAAQ;AACzB,gBAAM,MAAM,MAAM,WAAW,OAAO,GAAG;AACvC,iBAAO,SAAS,WAAW,GAAmB;AAAA,QAChD;AAAA,QACA,WAAW;AAAA,MACb,CAAC;AAED,aAAO;AAAA,QACL,QAAQ;AAAA,QACR,QAAQ;AAAA,QACR,QAAQ;AAAA,MACV;AAAA,IACF;AAAA;AAAA;AAAA;AAAA,IAKA,aAAa,CAAC,SAAuB,kBAAkB;AACrD,iBAAO,qCAAqD;AAAA,QAC1D,UAAU,CAAC,gBAAgB,YAAY,MAAM;AAAA,QAC7C,SAAS,OAAO,EAAE,UAAU,MAAM;AAzS1C;AA0SU,gBAAM,MAAM,MAAM,WAAW,KAAK;AAAA,YAChC,GAAG;AAAA,YACH,MAAM;AAAA,YACN,QAAO,YAAO,UAAP,YAAgB;AAAA,UACzB,CAAC;AACD,iBAAO,SAAS,SAAS,GAAiB;AAAA,QAC5C;AAAA,QACA,kBAAkB,CAAC,aAAa;AAC9B,gBAAM,EAAE,MAAM,WAAW,IAAI,SAAS;AACtC,iBAAO,OAAO,aAAa,OAAO,IAAI;AAAA,QACxC;AAAA,QACA,kBAAkB;AAAA,MACpB,CAAC;AAAA,IACH;AAAA,EACF;AACF;","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 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, // TId — input (get/update/delete param) → contravariant → never\n unknown, // TBase — output (list response item) → covariant → unknown\n unknown, // TDetail — output (single response item) → covariant → unknown\n never, // TCreate — input (create payload) → contravariant → never\n never, // TUpdate — input (update payload) → contravariant → never\n unknown, // TListRaw — output → covariant → unknown\n unknown // TSingleRaw — output → covariant → unknown\n>;\n\ntype ServiceGenerics<S extends AnyResourceService> =\n S extends ResourceService<\n infer TId,\n infer TBase,\n infer TDetail,\n infer TCreate,\n infer TUpdate,\n infer TListRaw,\n infer TSingleRaw\n >\n ? {\n TId: TId;\n TBase: TBase;\n TDetail: TDetail;\n TCreate: TCreate;\n TUpdate: TUpdate;\n TListRaw: TListRaw;\n TSingleRaw: TSingleRaw;\n }\n : never;\n\n// Convenience aliases so usage inside the function body stays readable\ntype Id<S extends AnyResourceService> = ServiceGenerics<S>[\"TId\"];\ntype Base<S extends AnyResourceService> = ServiceGenerics<S>[\"TBase\"];\ntype Detail<S extends AnyResourceService> = ServiceGenerics<S>[\"TDetail\"];\ntype Create<S extends AnyResourceService> = ServiceGenerics<S>[\"TCreate\"];\ntype Update<S extends AnyResourceService> = ServiceGenerics<S>[\"TUpdate\"];\ntype ListRaw<S extends AnyResourceService> = ServiceGenerics<S>[\"TListRaw\"];\ntype SingleRaw<S extends AnyResourceService> = ServiceGenerics<S>[\"TSingleRaw\"];\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":[]}
|
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 unknown,\n unknown,\n unknown,\n unknown,\n unknown,\n unknown,\n unknown\n>;\n\ntype ServiceGenerics<S extends AnyResourceService> =\n S extends ResourceService<\n infer TId,\n infer TBase,\n infer TDetail,\n infer TCreate,\n infer TUpdate,\n infer TListRaw,\n infer TSingleRaw\n >\n ? {\n TId: TId;\n TBase: TBase;\n TDetail: TDetail;\n TCreate: TCreate;\n TUpdate: TUpdate;\n TListRaw: TListRaw;\n TSingleRaw: TSingleRaw;\n }\n : never;\n\n// Convenience aliases so usage inside the function body stays readable\ntype Id<S extends AnyResourceService> = ServiceGenerics<S>[\"TId\"];\ntype Base<S extends AnyResourceService> = ServiceGenerics<S>[\"TBase\"];\ntype Detail<S extends AnyResourceService> = ServiceGenerics<S>[\"TDetail\"];\ntype Create<S extends AnyResourceService> = ServiceGenerics<S>[\"TCreate\"];\ntype Update<S extends AnyResourceService> = ServiceGenerics<S>[\"TUpdate\"];\ntype ListRaw<S extends AnyResourceService> = ServiceGenerics<S>[\"TListRaw\"];\ntype SingleRaw<S extends AnyResourceService> = ServiceGenerics<S>[\"TSingleRaw\"];\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 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, // TId — input (get/update/delete param) → contravariant → never\n unknown, // TBase — output (list response item) → covariant → unknown\n unknown, // TDetail — output (single response item) → covariant → unknown\n never, // TCreate — input (create payload) → contravariant → never\n never, // TUpdate — input (update payload) → contravariant → never\n unknown, // TListRaw — output → covariant → unknown\n unknown // TSingleRaw — output → covariant → unknown\n>;\n\ntype ServiceGenerics<S extends AnyResourceService> =\n S extends ResourceService<\n infer TId,\n infer TBase,\n infer TDetail,\n infer TCreate,\n infer TUpdate,\n infer TListRaw,\n infer TSingleRaw\n >\n ? {\n TId: TId;\n TBase: TBase;\n TDetail: TDetail;\n TCreate: TCreate;\n TUpdate: TUpdate;\n TListRaw: TListRaw;\n TSingleRaw: TSingleRaw;\n }\n : never;\n\n// Convenience aliases so usage inside the function body stays readable\ntype Id<S extends AnyResourceService> = ServiceGenerics<S>[\"TId\"];\ntype Base<S extends AnyResourceService> = ServiceGenerics<S>[\"TBase\"];\ntype Detail<S extends AnyResourceService> = ServiceGenerics<S>[\"TDetail\"];\ntype Create<S extends AnyResourceService> = ServiceGenerics<S>[\"TCreate\"];\ntype Update<S extends AnyResourceService> = ServiceGenerics<S>[\"TUpdate\"];\ntype ListRaw<S extends AnyResourceService> = ServiceGenerics<S>[\"TListRaw\"];\ntype SingleRaw<S extends AnyResourceService> = ServiceGenerics<S>[\"TSingleRaw\"];\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":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@void-snippets/react",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "TanStack Query resource hooks factory for React",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -25,11 +25,11 @@
|
|
|
25
25
|
],
|
|
26
26
|
"license": "MIT",
|
|
27
27
|
"peerDependencies": {
|
|
28
|
-
"react": ">=17.0.0"
|
|
28
|
+
"react": ">=17.0.0",
|
|
29
|
+
"@void-snippets/client": "0.1.3"
|
|
29
30
|
},
|
|
30
31
|
"dependencies": {
|
|
31
32
|
"@tanstack/react-query": "^5.0.0",
|
|
32
|
-
"@void-snippets/client": "0.1.3",
|
|
33
33
|
"@void-snippets/core": "0.1.3"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|