@leancodepl/react-query-cqrs-client 8.5.0 → 8.6.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/README.md ADDED
@@ -0,0 +1,155 @@
1
+ # @leancodepl/react-query-cqrs-client
2
+
3
+ TanStack Query CQRS client with hooks for queries, operations, and commands.
4
+
5
+ ## Features
6
+
7
+ - **TanStack Query integration** - Built-in caching, optimistic updates, and background refetching
8
+ - **CQRS pattern** - Separate queries, commands, and operations with proper typing
9
+ - **Custom hooks** - Hook factories for all operation types with loading states
10
+ - **Error handling** - Validation errors with custom error codes and handlers
11
+ - **Authentication** - Token handling with automatic refresh integration
12
+ - **Cache management** - Smart invalidation and query dependency management
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ npm install @leancodepl/react-query-cqrs-client
18
+ # or
19
+ yarn add @leancodepl/react-query-cqrs-client
20
+ ```
21
+
22
+ ## API
23
+
24
+ ### `mkCqrsClient(cqrsEndpoint, queryClient, tokenProvider, ajaxOptions, tokenHeader)`
25
+
26
+ Creates TanStack Query CQRS client with hooks for queries, operations, and commands.
27
+
28
+ **Parameters:**
29
+
30
+ - `cqrsEndpoint: string` - Base URL for CQRS API endpoints
31
+ - `queryClient: QueryClient` - TanStack Query client instance
32
+ - `tokenProvider?: Partial<TokenProvider>` - Optional token provider for authentication
33
+ - `ajaxOptions?: Omit<AjaxConfig, ...>` - Optional RxJS Ajax configuration options
34
+ - `tokenHeader?: string` - Header name for authentication token (default: "Authorization")
35
+
36
+ **Returns:** Object with `createQuery`, `createOperation`, and `createCommand` hook factories
37
+
38
+ ## Usage Examples
39
+
40
+ ### Basic Setup
41
+
42
+ ```typescript
43
+ import { mkCqrsClient } from "@leancodepl/react-query-cqrs-client"
44
+ import { QueryClient } from "@tanstack/react-query"
45
+
46
+ const queryClient = new QueryClient()
47
+
48
+ const client = mkCqrsClient({
49
+ cqrsEndpoint: "https://api.example.com",
50
+ queryClient,
51
+ tokenProvider: {
52
+ getToken: () => Promise.resolve(localStorage.getItem("token")),
53
+ },
54
+ })
55
+ ```
56
+
57
+ ### Query Hook
58
+
59
+ ```typescript
60
+ import React from 'react';
61
+
62
+ interface GetUserQuery {
63
+ userId: string;
64
+ }
65
+
66
+ interface UserResult {
67
+ id: string;
68
+ name: string;
69
+ email: string;
70
+ }
71
+
72
+ const useGetUser = client.createQuery<GetUserQuery, UserResult>('GetUser');
73
+
74
+ function UserProfile({ userId }: { userId: string }) {
75
+ const { data, isLoading, error } = useGetUser({ userId });
76
+
77
+ if (isLoading) return <div>Loading...</div>;
78
+ if (error) return <div>Error loading user</div>;
79
+
80
+ return (
81
+ <div>
82
+ <h1>{data?.name}</h1>
83
+ <p>{data?.email}</p>
84
+ </div>
85
+ );
86
+ }
87
+ ```
88
+
89
+ ### Command Hook
90
+
91
+ ```typescript
92
+ import React from 'react';
93
+
94
+ interface CreateUserCommand {
95
+ name: string;
96
+ email: string;
97
+ }
98
+
99
+ const errorCodes = { EmailExists: 1, InvalidEmail: 2 } as const;
100
+ const useCreateUser = client.createCommand<CreateUserCommand, typeof errorCodes>('CreateUser', errorCodes);
101
+
102
+ function CreateUserForm() {
103
+ const { mutate: createUser, isPending } = useCreateUser({
104
+ handler: (handle) =>
105
+ handle('success', () => 'User created successfully')
106
+ .handle('EmailExists', () => 'Email already exists')
107
+ .handle('failure', () => 'Failed to create user')
108
+ .check(),
109
+ });
110
+
111
+ const handleSubmit = () => {
112
+ createUser({ name: 'John', email: 'john@example.com' });
113
+ };
114
+
115
+ return (
116
+ <button onClick={handleSubmit} disabled={isPending}>
117
+ {isPending ? 'Creating...' : 'Create User'}
118
+ </button>
119
+ );
120
+ }
121
+ ```
122
+
123
+ ### Operation Hook
124
+
125
+ ```typescript
126
+ interface UploadFileOperation {
127
+ file: File;
128
+ folder: string;
129
+ }
130
+
131
+ interface UploadResult {
132
+ url: string;
133
+ filename: string;
134
+ }
135
+
136
+ const useUploadFile = client.createOperation<UploadFileOperation, UploadResult>('UploadFile');
137
+
138
+ function FileUploader() {
139
+ const { mutate: uploadFile, isPending } = useUploadFile({
140
+ invalidateQueries: [['GetFiles']],
141
+ });
142
+
143
+ const handleUpload = (file: File) => {
144
+ uploadFile({ file, folder: 'documents' });
145
+ };
146
+
147
+ return (
148
+ <input
149
+ type="file"
150
+ onChange={(e) => e.target.files?.[0] && handleUpload(e.target.files[0])}
151
+ disabled={isPending}
152
+ />
153
+ );
154
+ }
155
+ ```
@@ -0,0 +1 @@
1
+ exports._default = require('./index.cjs.js').default;
package/index.cjs.js ADDED
@@ -0,0 +1,281 @@
1
+ 'use strict';
2
+
3
+ var reactQuery = require('@tanstack/react-query');
4
+ var rxjs = require('rxjs');
5
+ var ajax = require('rxjs/ajax');
6
+ var operators = require('rxjs/operators');
7
+ var validation = require('@leancodepl/validation');
8
+ var utils = require('@leancodepl/utils');
9
+
10
+ function _extends() {
11
+ _extends = Object.assign || function assign(target) {
12
+ for(var i = 1; i < arguments.length; i++){
13
+ var source = arguments[i];
14
+ for(var key in source)if (Object.prototype.hasOwnProperty.call(source, key)) target[key] = source[key];
15
+ }
16
+ return target;
17
+ };
18
+ return _extends.apply(this, arguments);
19
+ }
20
+
21
+ function _object_without_properties_loose(source, excluded) {
22
+ if (source == null) return {};
23
+ var target = {};
24
+ var sourceKeys = Object.keys(source);
25
+ var key, i;
26
+ for(i = 0; i < sourceKeys.length; i++){
27
+ key = sourceKeys[i];
28
+ if (excluded.indexOf(key) >= 0) continue;
29
+ target[key] = source[key];
30
+ }
31
+ return target;
32
+ }
33
+
34
+ function authGuard(tokenProvider) {
35
+ if (!(tokenProvider == null ? void 0 : tokenProvider.invalidateToken)) {
36
+ return (response)=>response;
37
+ }
38
+ return (response)=>response.pipe(rxjs.catchError((error)=>{
39
+ if (error.status === 401) {
40
+ tokenProvider.invalidateToken == null ? void 0 : tokenProvider.invalidateToken.call(tokenProvider);
41
+ }
42
+ return rxjs.throwError(()=>error);
43
+ }));
44
+ }
45
+
46
+ function uncapitalizedJSONParse(json) {
47
+ return JSON.parse(json, (key, value)=>{
48
+ if (value === null && key !== "") return undefined;
49
+ if (!value || Array.isArray(value) || typeof value !== "object") return value;
50
+ return Object.fromEntries(Object.entries(value).map(([key, value])=>[
51
+ utils.toLowerFirst(key),
52
+ value
53
+ ]));
54
+ });
55
+ }
56
+
57
+ function uncapitalizedParse() {
58
+ return ($source)=>$source.pipe(operators.map(uncapitalizedJSONParse));
59
+ }
60
+ /**
61
+ * Creates React Query CQRS client with hooks for queries, operations, and commands.
62
+ *
63
+ * Integrates with React Query to provide caching, background updates, and optimistic updates
64
+ * for CQRS operations. Automatically handles authentication, retries, and response transformation
65
+ * with uncapitalized keys.
66
+ *
67
+ * @param cqrsEndpoint - Base URL for CQRS API endpoints
68
+ * @param queryClient - React Query client instance
69
+ * @param tokenProvider - Optional token provider for authentication
70
+ * @param ajaxOptions - Optional RxJS Ajax configuration options
71
+ * @param tokenHeader - Header name for authentication token (default: "Authorization")
72
+ * @returns Object with `createQuery`, `createOperation`, and `createCommand` hook factories
73
+ * @example
74
+ * ```typescript
75
+ * const client = mkCqrsClient({
76
+ * cqrsEndpoint: 'https://api.example.com',
77
+ * queryClient: new QueryClient()
78
+ * });
79
+ * ```
80
+ */ function mkCqrsClient({ cqrsEndpoint, queryClient, tokenProvider, ajaxOptions, tokenHeader = "Authorization" }) {
81
+ function mkFetcher(endpoint, config = {}) {
82
+ const apiCall = (data, token)=>{
83
+ var _ajaxOptions_withCredentials;
84
+ return ajax.ajax(_extends({}, ajaxOptions, config, {
85
+ headers: {
86
+ [tokenHeader]: token,
87
+ "Content-Type": "application/json"
88
+ },
89
+ url: `${cqrsEndpoint}/${endpoint}`,
90
+ method: "POST",
91
+ body: data,
92
+ withCredentials: (_ajaxOptions_withCredentials = ajaxOptions == null ? void 0 : ajaxOptions.withCredentials) != null ? _ajaxOptions_withCredentials : true
93
+ }));
94
+ };
95
+ const getToken = tokenProvider == null ? void 0 : tokenProvider.getToken;
96
+ const mk$apiCall = (data, token)=>apiCall(data, token).pipe(authGuard(tokenProvider), operators.map((result)=>result.response));
97
+ if (getToken) {
98
+ return (data)=>rxjs.from(getToken()).pipe(operators.mergeMap((token)=>mk$apiCall(data, token)));
99
+ }
100
+ return (data)=>mk$apiCall(data);
101
+ }
102
+ return {
103
+ createQuery (type) {
104
+ const fetcher = mkFetcher(`query/${type}`, {
105
+ responseType: "text"
106
+ });
107
+ function useApiQuery(data, options) {
108
+ return reactQuery.useQuery(_extends({
109
+ queryKey: useApiQuery.key(data),
110
+ queryFn: (context)=>rxjs.firstValueFrom(useApiQuery.fetcher(data, context))
111
+ }, options), queryClient);
112
+ }
113
+ useApiQuery.type = type;
114
+ useApiQuery.fetcher = (data, context)=>rxjs.race([
115
+ fetcher(data).pipe(uncapitalizedParse()),
116
+ ...(context == null ? void 0 : context.signal) ? [
117
+ rxjs.fromEvent(context.signal, "abort").pipe(operators.mergeMap(()=>rxjs.throwError(()=>new Error("Query aborted"))))
118
+ ] : []
119
+ ]);
120
+ useApiQuery.fetch = (data, options)=>queryClient.fetchQuery(_extends({
121
+ queryKey: useApiQuery.key(data),
122
+ queryFn: (context)=>rxjs.firstValueFrom(useApiQuery.fetcher(data, context))
123
+ }, options));
124
+ useApiQuery.lazy = function(options = {}) {
125
+ // eslint-disable-next-line react-hooks/rules-of-hooks
126
+ return reactQuery.useMutation(_extends({
127
+ mutationKey: [
128
+ type
129
+ ],
130
+ mutationFn: (variables)=>rxjs.firstValueFrom(useApiQuery.fetcher(variables))
131
+ }, options), queryClient);
132
+ };
133
+ useApiQuery.infinite = function(initialPageData, options) {
134
+ // eslint-disable-next-line react-hooks/rules-of-hooks
135
+ return reactQuery.useInfiniteQuery(_extends({
136
+ queryKey: [
137
+ type
138
+ ],
139
+ queryFn: async (context)=>await rxjs.firstValueFrom(useApiQuery.fetcher(context.pageParam, context)),
140
+ initialPageParam: initialPageData
141
+ }, options), queryClient);
142
+ };
143
+ useApiQuery.key = (query)=>[
144
+ type,
145
+ query
146
+ ];
147
+ function setQueryData(queryOrQueryKey, updater) {
148
+ const key = Array.isArray(queryOrQueryKey) ? queryOrQueryKey : useApiQuery.key(queryOrQueryKey);
149
+ return queryClient.setQueryData(key, updater);
150
+ }
151
+ useApiQuery.setQueryData = setQueryData;
152
+ useApiQuery.setQueriesData = (query, updater)=>queryClient.setQueriesData({
153
+ queryKey: useApiQuery.key(query)
154
+ }, updater);
155
+ useApiQuery.getQueryData = (query)=>queryClient.getQueryData(useApiQuery.key(query));
156
+ useApiQuery.getQueriesData = (query)=>queryClient.getQueriesData({
157
+ queryKey: useApiQuery.key(query)
158
+ });
159
+ useApiQuery.prefetch = (data, options)=>queryClient.prefetchQuery(_extends({
160
+ queryKey: useApiQuery.key(data),
161
+ queryFn: (context)=>rxjs.firstValueFrom(useApiQuery.fetcher(data, context))
162
+ }, options));
163
+ useApiQuery.invalidate = (query)=>queryClient.invalidateQueries({
164
+ queryKey: useApiQuery.key(query)
165
+ });
166
+ useApiQuery.cancel = (query)=>queryClient.cancelQueries({
167
+ queryKey: useApiQuery.key(query)
168
+ });
169
+ useApiQuery.optimisticUpdate = async (updater, query = {})=>{
170
+ await useApiQuery.cancel(query);
171
+ const data = useApiQuery.getQueriesData(query);
172
+ useApiQuery.setQueriesData(query, updater);
173
+ return ()=>data.forEach(([key, result])=>queryClient.setQueryData(key, result));
174
+ };
175
+ return useApiQuery;
176
+ },
177
+ createOperation (type) {
178
+ const fetcher = mkFetcher(`operation/${type}`, {
179
+ responseType: "text"
180
+ });
181
+ function useApiOperation(_param = {}) {
182
+ var { onSuccess: onSuccessBase, invalidateQueries } = _param, options = _object_without_properties_loose(_param, [
183
+ "onSuccess",
184
+ "invalidateQueries"
185
+ ]);
186
+ return reactQuery.useMutation(_extends({
187
+ mutationKey: useApiOperation.key,
188
+ mutationFn: (variables)=>rxjs.firstValueFrom(useApiOperation.fetcher(variables))
189
+ }, options, {
190
+ async onSuccess (data, variables, context) {
191
+ const result = await (onSuccessBase == null ? void 0 : onSuccessBase(data, variables, context));
192
+ if (invalidateQueries) {
193
+ await Promise.allSettled(invalidateQueries.map((queryKey)=>queryClient.invalidateQueries({
194
+ queryKey
195
+ })));
196
+ }
197
+ return result;
198
+ }
199
+ }), queryClient);
200
+ }
201
+ useApiOperation.type = type;
202
+ useApiOperation.key = [
203
+ useApiOperation.type
204
+ ];
205
+ useApiOperation.fetcher = (variables)=>fetcher(variables).pipe(uncapitalizedParse());
206
+ return useApiOperation;
207
+ },
208
+ createCommand (type, errorCodes) {
209
+ const fetcher = mkFetcher(`command/${type}`);
210
+ function useApiCommand(_param = {}) {
211
+ var { invalidateQueries, handler, optimisticUpdate, onMutate, onError, onSettled } = _param, options = _object_without_properties_loose(_param, [
212
+ "invalidateQueries",
213
+ "handler",
214
+ "optimisticUpdate",
215
+ "onMutate",
216
+ "onError",
217
+ "onSettled"
218
+ ]);
219
+ return reactQuery.useMutation(_extends({}, options, {
220
+ mutationKey: useApiCommand.key,
221
+ mutationFn: (variables)=>rxjs.firstValueFrom(useApiCommand.call(variables, handler)),
222
+ async onMutate (variables) {
223
+ // there's really no good way to do it without type cast
224
+ const baseContext = await (onMutate == null ? void 0 : onMutate(variables));
225
+ var _optimisticUpdate;
226
+ const optimisticUpdateReverts = await Promise.all((_optimisticUpdate = optimisticUpdate == null ? void 0 : optimisticUpdate(variables)) != null ? _optimisticUpdate : []);
227
+ return _extends({}, baseContext, {
228
+ revertOptimisticUpdate: ()=>optimisticUpdateReverts.forEach((revertOptimisticUpdate)=>revertOptimisticUpdate())
229
+ });
230
+ },
231
+ async onError (error, variables, context) {
232
+ await (onError == null ? void 0 : onError(error, variables, context));
233
+ context == null ? void 0 : context.revertOptimisticUpdate();
234
+ },
235
+ async onSettled (data, error, variables, context) {
236
+ if (invalidateQueries) {
237
+ await Promise.allSettled(invalidateQueries.map((queryKey)=>queryClient.invalidateQueries({
238
+ queryKey
239
+ })));
240
+ }
241
+ return await (onSettled == null ? void 0 : onSettled(data, error, variables, context));
242
+ }
243
+ }), queryClient);
244
+ }
245
+ useApiCommand.type = type;
246
+ useApiCommand.key = [
247
+ useApiCommand.type
248
+ ];
249
+ useApiCommand.fetcher = (variables)=>fetcher(variables);
250
+ useApiCommand.call = (variables, handler)=>{
251
+ const $response = useApiCommand.fetcher(variables);
252
+ return $response.pipe(operators.map((result)=>({
253
+ isSuccess: true,
254
+ result
255
+ })), rxjs.catchError((e)=>rxjs.of(useApiCommand.mapError(e))), operators.map((response)=>{
256
+ const result = handler ? useApiCommand.handleResponse(handler)(response) : response;
257
+ if (!response.isSuccess || !response.result.WasSuccessful) {
258
+ throw result;
259
+ }
260
+ return result;
261
+ }));
262
+ };
263
+ useApiCommand.mapError = (e)=>{
264
+ if (e instanceof ajax.AjaxError && e.status === 422) {
265
+ return {
266
+ isSuccess: true,
267
+ result: e.response
268
+ };
269
+ }
270
+ return {
271
+ isSuccess: false,
272
+ error: e
273
+ };
274
+ };
275
+ useApiCommand.handleResponse = (handler)=>(response)=>handler(validation.handleResponse(response, errorCodes));
276
+ return useApiCommand;
277
+ }
278
+ };
279
+ }
280
+
281
+ exports.mkCqrsClient = mkCqrsClient;
package/index.cjs.mjs ADDED
@@ -0,0 +1,2 @@
1
+ export * from './index.cjs.js';
2
+ export { _default as default } from './index.cjs.default.js';
package/index.d.ts ADDED
@@ -0,0 +1 @@
1
+ export * from "./src/index";
package/index.esm.js ADDED
@@ -0,0 +1,279 @@
1
+ import { useMutation, useInfiniteQuery, useQuery } from '@tanstack/react-query';
2
+ import { catchError, throwError, of, race, fromEvent, from, firstValueFrom } from 'rxjs';
3
+ import { AjaxError, ajax } from 'rxjs/ajax';
4
+ import { map, mergeMap } from 'rxjs/operators';
5
+ import { handleResponse } from '@leancodepl/validation';
6
+ import { toLowerFirst } from '@leancodepl/utils';
7
+
8
+ function _extends() {
9
+ _extends = Object.assign || function assign(target) {
10
+ for(var i = 1; i < arguments.length; i++){
11
+ var source = arguments[i];
12
+ for(var key in source)if (Object.prototype.hasOwnProperty.call(source, key)) target[key] = source[key];
13
+ }
14
+ return target;
15
+ };
16
+ return _extends.apply(this, arguments);
17
+ }
18
+
19
+ function _object_without_properties_loose(source, excluded) {
20
+ if (source == null) return {};
21
+ var target = {};
22
+ var sourceKeys = Object.keys(source);
23
+ var key, i;
24
+ for(i = 0; i < sourceKeys.length; i++){
25
+ key = sourceKeys[i];
26
+ if (excluded.indexOf(key) >= 0) continue;
27
+ target[key] = source[key];
28
+ }
29
+ return target;
30
+ }
31
+
32
+ function authGuard(tokenProvider) {
33
+ if (!(tokenProvider == null ? void 0 : tokenProvider.invalidateToken)) {
34
+ return (response)=>response;
35
+ }
36
+ return (response)=>response.pipe(catchError((error)=>{
37
+ if (error.status === 401) {
38
+ tokenProvider.invalidateToken == null ? void 0 : tokenProvider.invalidateToken.call(tokenProvider);
39
+ }
40
+ return throwError(()=>error);
41
+ }));
42
+ }
43
+
44
+ function uncapitalizedJSONParse(json) {
45
+ return JSON.parse(json, (key, value)=>{
46
+ if (value === null && key !== "") return undefined;
47
+ if (!value || Array.isArray(value) || typeof value !== "object") return value;
48
+ return Object.fromEntries(Object.entries(value).map(([key, value])=>[
49
+ toLowerFirst(key),
50
+ value
51
+ ]));
52
+ });
53
+ }
54
+
55
+ function uncapitalizedParse() {
56
+ return ($source)=>$source.pipe(map(uncapitalizedJSONParse));
57
+ }
58
+ /**
59
+ * Creates React Query CQRS client with hooks for queries, operations, and commands.
60
+ *
61
+ * Integrates with React Query to provide caching, background updates, and optimistic updates
62
+ * for CQRS operations. Automatically handles authentication, retries, and response transformation
63
+ * with uncapitalized keys.
64
+ *
65
+ * @param cqrsEndpoint - Base URL for CQRS API endpoints
66
+ * @param queryClient - React Query client instance
67
+ * @param tokenProvider - Optional token provider for authentication
68
+ * @param ajaxOptions - Optional RxJS Ajax configuration options
69
+ * @param tokenHeader - Header name for authentication token (default: "Authorization")
70
+ * @returns Object with `createQuery`, `createOperation`, and `createCommand` hook factories
71
+ * @example
72
+ * ```typescript
73
+ * const client = mkCqrsClient({
74
+ * cqrsEndpoint: 'https://api.example.com',
75
+ * queryClient: new QueryClient()
76
+ * });
77
+ * ```
78
+ */ function mkCqrsClient({ cqrsEndpoint, queryClient, tokenProvider, ajaxOptions, tokenHeader = "Authorization" }) {
79
+ function mkFetcher(endpoint, config = {}) {
80
+ const apiCall = (data, token)=>{
81
+ var _ajaxOptions_withCredentials;
82
+ return ajax(_extends({}, ajaxOptions, config, {
83
+ headers: {
84
+ [tokenHeader]: token,
85
+ "Content-Type": "application/json"
86
+ },
87
+ url: `${cqrsEndpoint}/${endpoint}`,
88
+ method: "POST",
89
+ body: data,
90
+ withCredentials: (_ajaxOptions_withCredentials = ajaxOptions == null ? void 0 : ajaxOptions.withCredentials) != null ? _ajaxOptions_withCredentials : true
91
+ }));
92
+ };
93
+ const getToken = tokenProvider == null ? void 0 : tokenProvider.getToken;
94
+ const mk$apiCall = (data, token)=>apiCall(data, token).pipe(authGuard(tokenProvider), map((result)=>result.response));
95
+ if (getToken) {
96
+ return (data)=>from(getToken()).pipe(mergeMap((token)=>mk$apiCall(data, token)));
97
+ }
98
+ return (data)=>mk$apiCall(data);
99
+ }
100
+ return {
101
+ createQuery (type) {
102
+ const fetcher = mkFetcher(`query/${type}`, {
103
+ responseType: "text"
104
+ });
105
+ function useApiQuery(data, options) {
106
+ return useQuery(_extends({
107
+ queryKey: useApiQuery.key(data),
108
+ queryFn: (context)=>firstValueFrom(useApiQuery.fetcher(data, context))
109
+ }, options), queryClient);
110
+ }
111
+ useApiQuery.type = type;
112
+ useApiQuery.fetcher = (data, context)=>race([
113
+ fetcher(data).pipe(uncapitalizedParse()),
114
+ ...(context == null ? void 0 : context.signal) ? [
115
+ fromEvent(context.signal, "abort").pipe(mergeMap(()=>throwError(()=>new Error("Query aborted"))))
116
+ ] : []
117
+ ]);
118
+ useApiQuery.fetch = (data, options)=>queryClient.fetchQuery(_extends({
119
+ queryKey: useApiQuery.key(data),
120
+ queryFn: (context)=>firstValueFrom(useApiQuery.fetcher(data, context))
121
+ }, options));
122
+ useApiQuery.lazy = function(options = {}) {
123
+ // eslint-disable-next-line react-hooks/rules-of-hooks
124
+ return useMutation(_extends({
125
+ mutationKey: [
126
+ type
127
+ ],
128
+ mutationFn: (variables)=>firstValueFrom(useApiQuery.fetcher(variables))
129
+ }, options), queryClient);
130
+ };
131
+ useApiQuery.infinite = function(initialPageData, options) {
132
+ // eslint-disable-next-line react-hooks/rules-of-hooks
133
+ return useInfiniteQuery(_extends({
134
+ queryKey: [
135
+ type
136
+ ],
137
+ queryFn: async (context)=>await firstValueFrom(useApiQuery.fetcher(context.pageParam, context)),
138
+ initialPageParam: initialPageData
139
+ }, options), queryClient);
140
+ };
141
+ useApiQuery.key = (query)=>[
142
+ type,
143
+ query
144
+ ];
145
+ function setQueryData(queryOrQueryKey, updater) {
146
+ const key = Array.isArray(queryOrQueryKey) ? queryOrQueryKey : useApiQuery.key(queryOrQueryKey);
147
+ return queryClient.setQueryData(key, updater);
148
+ }
149
+ useApiQuery.setQueryData = setQueryData;
150
+ useApiQuery.setQueriesData = (query, updater)=>queryClient.setQueriesData({
151
+ queryKey: useApiQuery.key(query)
152
+ }, updater);
153
+ useApiQuery.getQueryData = (query)=>queryClient.getQueryData(useApiQuery.key(query));
154
+ useApiQuery.getQueriesData = (query)=>queryClient.getQueriesData({
155
+ queryKey: useApiQuery.key(query)
156
+ });
157
+ useApiQuery.prefetch = (data, options)=>queryClient.prefetchQuery(_extends({
158
+ queryKey: useApiQuery.key(data),
159
+ queryFn: (context)=>firstValueFrom(useApiQuery.fetcher(data, context))
160
+ }, options));
161
+ useApiQuery.invalidate = (query)=>queryClient.invalidateQueries({
162
+ queryKey: useApiQuery.key(query)
163
+ });
164
+ useApiQuery.cancel = (query)=>queryClient.cancelQueries({
165
+ queryKey: useApiQuery.key(query)
166
+ });
167
+ useApiQuery.optimisticUpdate = async (updater, query = {})=>{
168
+ await useApiQuery.cancel(query);
169
+ const data = useApiQuery.getQueriesData(query);
170
+ useApiQuery.setQueriesData(query, updater);
171
+ return ()=>data.forEach(([key, result])=>queryClient.setQueryData(key, result));
172
+ };
173
+ return useApiQuery;
174
+ },
175
+ createOperation (type) {
176
+ const fetcher = mkFetcher(`operation/${type}`, {
177
+ responseType: "text"
178
+ });
179
+ function useApiOperation(_param = {}) {
180
+ var { onSuccess: onSuccessBase, invalidateQueries } = _param, options = _object_without_properties_loose(_param, [
181
+ "onSuccess",
182
+ "invalidateQueries"
183
+ ]);
184
+ return useMutation(_extends({
185
+ mutationKey: useApiOperation.key,
186
+ mutationFn: (variables)=>firstValueFrom(useApiOperation.fetcher(variables))
187
+ }, options, {
188
+ async onSuccess (data, variables, context) {
189
+ const result = await (onSuccessBase == null ? void 0 : onSuccessBase(data, variables, context));
190
+ if (invalidateQueries) {
191
+ await Promise.allSettled(invalidateQueries.map((queryKey)=>queryClient.invalidateQueries({
192
+ queryKey
193
+ })));
194
+ }
195
+ return result;
196
+ }
197
+ }), queryClient);
198
+ }
199
+ useApiOperation.type = type;
200
+ useApiOperation.key = [
201
+ useApiOperation.type
202
+ ];
203
+ useApiOperation.fetcher = (variables)=>fetcher(variables).pipe(uncapitalizedParse());
204
+ return useApiOperation;
205
+ },
206
+ createCommand (type, errorCodes) {
207
+ const fetcher = mkFetcher(`command/${type}`);
208
+ function useApiCommand(_param = {}) {
209
+ var { invalidateQueries, handler, optimisticUpdate, onMutate, onError, onSettled } = _param, options = _object_without_properties_loose(_param, [
210
+ "invalidateQueries",
211
+ "handler",
212
+ "optimisticUpdate",
213
+ "onMutate",
214
+ "onError",
215
+ "onSettled"
216
+ ]);
217
+ return useMutation(_extends({}, options, {
218
+ mutationKey: useApiCommand.key,
219
+ mutationFn: (variables)=>firstValueFrom(useApiCommand.call(variables, handler)),
220
+ async onMutate (variables) {
221
+ // there's really no good way to do it without type cast
222
+ const baseContext = await (onMutate == null ? void 0 : onMutate(variables));
223
+ var _optimisticUpdate;
224
+ const optimisticUpdateReverts = await Promise.all((_optimisticUpdate = optimisticUpdate == null ? void 0 : optimisticUpdate(variables)) != null ? _optimisticUpdate : []);
225
+ return _extends({}, baseContext, {
226
+ revertOptimisticUpdate: ()=>optimisticUpdateReverts.forEach((revertOptimisticUpdate)=>revertOptimisticUpdate())
227
+ });
228
+ },
229
+ async onError (error, variables, context) {
230
+ await (onError == null ? void 0 : onError(error, variables, context));
231
+ context == null ? void 0 : context.revertOptimisticUpdate();
232
+ },
233
+ async onSettled (data, error, variables, context) {
234
+ if (invalidateQueries) {
235
+ await Promise.allSettled(invalidateQueries.map((queryKey)=>queryClient.invalidateQueries({
236
+ queryKey
237
+ })));
238
+ }
239
+ return await (onSettled == null ? void 0 : onSettled(data, error, variables, context));
240
+ }
241
+ }), queryClient);
242
+ }
243
+ useApiCommand.type = type;
244
+ useApiCommand.key = [
245
+ useApiCommand.type
246
+ ];
247
+ useApiCommand.fetcher = (variables)=>fetcher(variables);
248
+ useApiCommand.call = (variables, handler)=>{
249
+ const $response = useApiCommand.fetcher(variables);
250
+ return $response.pipe(map((result)=>({
251
+ isSuccess: true,
252
+ result
253
+ })), catchError((e)=>of(useApiCommand.mapError(e))), map((response)=>{
254
+ const result = handler ? useApiCommand.handleResponse(handler)(response) : response;
255
+ if (!response.isSuccess || !response.result.WasSuccessful) {
256
+ throw result;
257
+ }
258
+ return result;
259
+ }));
260
+ };
261
+ useApiCommand.mapError = (e)=>{
262
+ if (e instanceof AjaxError && e.status === 422) {
263
+ return {
264
+ isSuccess: true,
265
+ result: e.response
266
+ };
267
+ }
268
+ return {
269
+ isSuccess: false,
270
+ error: e
271
+ };
272
+ };
273
+ useApiCommand.handleResponse = (handler)=>(response)=>handler(handleResponse(response, errorCodes));
274
+ return useApiCommand;
275
+ }
276
+ };
277
+ }
278
+
279
+ export { mkCqrsClient };
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@leancodepl/react-query-cqrs-client",
3
- "version": "8.5.0",
3
+ "version": "8.6.0",
4
4
  "license": "Apache-2.0",
5
5
  "dependencies": {
6
- "@leancodepl/cqrs-client-base": "8.5.0",
7
- "@leancodepl/utils": "8.5.0",
8
- "@leancodepl/validation": "8.5.0",
6
+ "@leancodepl/cqrs-client-base": "8.6.0",
7
+ "@leancodepl/utils": "8.6.0",
8
+ "@leancodepl/validation": "8.6.0",
9
9
  "@tanstack/react-query": ">=5.0.0",
10
10
  "rxjs": ">=7.0.0"
11
11
  },
@@ -44,11 +44,6 @@
44
44
  "name": "LeanCode",
45
45
  "url": "https://leancode.co"
46
46
  },
47
- "files": [
48
- "dist",
49
- "README.md",
50
- "CHANGELOG.md"
51
- ],
52
47
  "sideEffects": false,
53
48
  "exports": {
54
49
  "./package.json": "./package.json",
package/src/index.d.ts ADDED
@@ -0,0 +1 @@
1
+ export { mkCqrsClient } from "./lib/mkCqrsClient";
@@ -0,0 +1,3 @@
1
+ import { MonoTypeOperatorFunction } from "rxjs";
2
+ import { TokenProvider } from "@leancodepl/cqrs-client-base";
3
+ export declare function authGuard<T>(tokenProvider?: Partial<TokenProvider>): MonoTypeOperatorFunction<T>;
@@ -0,0 +1,93 @@
1
+ import { FetchQueryOptions, InfiniteData, QueryClient, QueryFunctionContext, QueryKey, UndefinedInitialDataInfiniteOptions, UndefinedInitialDataOptions, Updater, UseMutationOptions, UseMutationResult } from "@tanstack/react-query";
2
+ import { Observable, OperatorFunction } from "rxjs";
3
+ import { AjaxConfig } from "rxjs/ajax";
4
+ import { ApiResponse, ApiSuccess, CommandResult, FailedCommandResult, SuccessfulCommandResult, TokenProvider } from "@leancodepl/cqrs-client-base";
5
+ import { ValidationErrorsHandler } from "@leancodepl/validation";
6
+ import { NullableUncapitalizeDeep } from "./types";
7
+ export declare function uncapitalizedParse<TResult>(): OperatorFunction<string, NullableUncapitalizeDeep<TResult>>;
8
+ /**
9
+ * Creates React Query CQRS client with hooks for queries, operations, and commands.
10
+ *
11
+ * Integrates with React Query to provide caching, background updates, and optimistic updates
12
+ * for CQRS operations. Automatically handles authentication, retries, and response transformation
13
+ * with uncapitalized keys.
14
+ *
15
+ * @param cqrsEndpoint - Base URL for CQRS API endpoints
16
+ * @param queryClient - React Query client instance
17
+ * @param tokenProvider - Optional token provider for authentication
18
+ * @param ajaxOptions - Optional RxJS Ajax configuration options
19
+ * @param tokenHeader - Header name for authentication token (default: "Authorization")
20
+ * @returns Object with `createQuery`, `createOperation`, and `createCommand` hook factories
21
+ * @example
22
+ * ```typescript
23
+ * const client = mkCqrsClient({
24
+ * cqrsEndpoint: 'https://api.example.com',
25
+ * queryClient: new QueryClient()
26
+ * });
27
+ * ```
28
+ */
29
+ export declare function mkCqrsClient({ cqrsEndpoint, queryClient, tokenProvider, ajaxOptions, tokenHeader, }: {
30
+ cqrsEndpoint: string;
31
+ queryClient: QueryClient;
32
+ tokenProvider?: Partial<TokenProvider>;
33
+ ajaxOptions?: Omit<AjaxConfig, "body" | "headers" | "method" | "responseType" | "url">;
34
+ tokenHeader?: string;
35
+ }): {
36
+ createQuery<TQuery, TResult>(type: string): {
37
+ (data: TQuery, options?: Omit<UndefinedInitialDataOptions<NullableUncapitalizeDeep<TResult>, unknown>, "queryFn" | "queryKey">): import("@tanstack/react-query").UseQueryResult<NullableUncapitalizeDeep<TResult>, unknown>;
38
+ type: string;
39
+ fetcher(data: TQuery, context?: QueryFunctionContext<QueryKey>): Observable<NullableUncapitalizeDeep<TResult>>;
40
+ fetch(data: TQuery, options?: Omit<FetchQueryOptions<NullableUncapitalizeDeep<TResult>, unknown>, "queryFn" | "queryKey">): Promise<NullableUncapitalizeDeep<TResult>>;
41
+ lazy<TContext = unknown>(options?: Omit<UseMutationOptions<NullableUncapitalizeDeep<TResult>, unknown, TQuery, TContext>, "mutationFn" | "mutationKey">): UseMutationResult<NullableUncapitalizeDeep<TResult>, unknown, TQuery, TContext>;
42
+ infinite(initialPageData: TQuery, options: Omit<UndefinedInitialDataInfiniteOptions<NullableUncapitalizeDeep<TResult>, unknown, InfiniteData<NullableUncapitalizeDeep<TResult>>, QueryKey, TQuery>, "initialPageParam" | "queryFn" | "queryKey" | "select">): import("@tanstack/react-query").UseInfiniteQueryResult<InfiniteData<NullableUncapitalizeDeep<TResult>, TQuery>, unknown>;
43
+ key(query: Partial<TQuery>): readonly [string, Partial<TQuery>];
44
+ setQueryData: {
45
+ (query: TQuery, updater: Updater<NullableUncapitalizeDeep<TResult> | undefined, NullableUncapitalizeDeep<TResult> | undefined>): NullableUncapitalizeDeep<TResult> | undefined;
46
+ (queryKey: QueryKey, updater: Updater<NullableUncapitalizeDeep<TResult> | undefined, NullableUncapitalizeDeep<TResult> | undefined>): NullableUncapitalizeDeep<TResult> | undefined;
47
+ };
48
+ setQueriesData(query: Partial<TQuery>, updater: Updater<NullableUncapitalizeDeep<TResult> | undefined, NullableUncapitalizeDeep<TResult> | undefined>): [readonly unknown[], unknown][];
49
+ getQueryData(query: TQuery): NullableUncapitalizeDeep<TResult> | undefined;
50
+ getQueriesData(query: Partial<TQuery>): [readonly unknown[], NullableUncapitalizeDeep<TResult> | undefined][];
51
+ prefetch(data: TQuery, options?: Omit<FetchQueryOptions<NullableUncapitalizeDeep<TResult>, unknown>, "initialData" | "queryFn" | "queryKey">): Promise<void>;
52
+ invalidate(query: Partial<TQuery>): Promise<void>;
53
+ cancel(query: Partial<TQuery>): Promise<void>;
54
+ optimisticUpdate(updater: Updater<NullableUncapitalizeDeep<TResult> | undefined, NullableUncapitalizeDeep<TResult> | undefined>, query?: Partial<TQuery>): Promise<() => void>;
55
+ };
56
+ createOperation<TOperation, TResult>(type: string): {
57
+ <TContext = unknown>({ onSuccess: onSuccessBase, invalidateQueries, ...options }?: Omit<UseMutationOptions<NullableUncapitalizeDeep<TResult>, unknown, TOperation, TContext>, "mutationFn" | "mutationKey"> & {
58
+ invalidateQueries?: QueryKey[];
59
+ }): UseMutationResult<NullableUncapitalizeDeep<TResult>, unknown, TOperation, TContext>;
60
+ type: string;
61
+ key: string[];
62
+ fetcher(variables: TOperation): Observable<NullableUncapitalizeDeep<TResult>>;
63
+ };
64
+ createCommand<TCommand, TErrorCodes extends {
65
+ [name: string]: number;
66
+ }>(type: string, errorCodes: TErrorCodes): {
67
+ <TContext extends Record<string, unknown> = {}>(options?: Omit<UseMutationOptions<ApiSuccess<SuccessfulCommandResult>, ApiResponse<FailedCommandResult<TErrorCodes>>, TCommand, TContext>, "mutationFn" | "mutationKey"> & {
68
+ invalidateQueries?: QueryKey[];
69
+ handler?: undefined;
70
+ optimisticUpdate?: (variables: TCommand) => Promise<() => void>[];
71
+ }): UseMutationResult<ApiSuccess<SuccessfulCommandResult>, ApiResponse<FailedCommandResult<TErrorCodes>>, TCommand, TContext>;
72
+ <TResult, TContext extends Record<string, unknown> = {}>(options?: Omit<UseMutationOptions<TResult, TResult, TCommand, TContext>, "mutationFn" | "mutationKey"> & {
73
+ invalidateQueries?: QueryKey[];
74
+ handler: (handler: ValidationErrorsHandler<TErrorCodes & {
75
+ success: -1;
76
+ failure: -2;
77
+ }, never>) => TResult;
78
+ optimisticUpdate?: (variables: TCommand) => Promise<() => void>[];
79
+ }): UseMutationResult<TResult, TResult, TCommand, TContext>;
80
+ type: string;
81
+ key: string[];
82
+ fetcher(variables: TCommand): Observable<SuccessfulCommandResult>;
83
+ call<TResult>(variables: TCommand, handler?: (handler: ValidationErrorsHandler<TErrorCodes & {
84
+ success: -1;
85
+ failure: -2;
86
+ }, never>) => TResult): Observable<ApiSuccess<SuccessfulCommandResult> | TResult>;
87
+ mapError(e: unknown): ApiResponse<CommandResult<TErrorCodes>>;
88
+ handleResponse<TResult>(handler: (handler: ValidationErrorsHandler<TErrorCodes & {
89
+ success: -1;
90
+ failure: -2;
91
+ }, never>) => TResult): (response: ApiResponse<CommandResult<TErrorCodes>>) => TResult;
92
+ };
93
+ };
@@ -0,0 +1,2 @@
1
+ import { UncapitalizeDeep } from "@leancodepl/utils";
2
+ export type NullableUncapitalizeDeep<T> = T extends null ? null : UncapitalizeDeep<T>;
@@ -0,0 +1 @@
1
+ export declare function uncapitalizedJSONParse(json: string): any;