@wisemen/vue-core-api-utils 2.0.2 → 2.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +851 -0
- package/dist/index.mjs +709 -0
- package/package.json +2 -2
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,709 @@
|
|
|
1
|
+
import { ResultAsync, err, ok } from "neverthrow";
|
|
2
|
+
import { QueryClient as QueryClient$1, VueQueryPlugin, useInfiniteQuery, useMutation as useMutation$1, useQuery as useQuery$1, useQueryClient } from "@tanstack/vue-query";
|
|
3
|
+
import { computed } from "vue";
|
|
4
|
+
import z from "zod";
|
|
5
|
+
|
|
6
|
+
//#region src/async-result/asyncResult.ts
|
|
7
|
+
/**
|
|
8
|
+
* Base class for AsyncResult - internal use only.
|
|
9
|
+
* Use AsyncResult<T, E> as the public type.
|
|
10
|
+
*/
|
|
11
|
+
var AsyncResultBase = class {
|
|
12
|
+
_error;
|
|
13
|
+
_status;
|
|
14
|
+
_value;
|
|
15
|
+
constructor(status, value, error) {
|
|
16
|
+
this._status = status;
|
|
17
|
+
this._value = value;
|
|
18
|
+
this._error = error;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Check if the result is an error (type predicate for narrowing)
|
|
22
|
+
*/
|
|
23
|
+
isErr() {
|
|
24
|
+
return this._status === "err";
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Check if the result is in loading state (type predicate for narrowing)
|
|
28
|
+
*/
|
|
29
|
+
isLoading() {
|
|
30
|
+
return this._status === "loading";
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Check if the result is a success (type predicate for narrowing)
|
|
34
|
+
*/
|
|
35
|
+
isOk() {
|
|
36
|
+
return this._status === "ok";
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Map the success value to a new value
|
|
40
|
+
*/
|
|
41
|
+
map(fn) {
|
|
42
|
+
if (this._status === "loading") return AsyncResult.loading();
|
|
43
|
+
if (this._status === "ok") return AsyncResult.ok(fn(this._value));
|
|
44
|
+
return AsyncResult.err(this._error);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Map the error to a new error
|
|
48
|
+
*/
|
|
49
|
+
mapErr(fn) {
|
|
50
|
+
if (this._status === "loading") return AsyncResult.loading();
|
|
51
|
+
if (this._status === "err") return AsyncResult.err(fn(this._error));
|
|
52
|
+
return AsyncResult.ok(this._value);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Pattern match on all three states
|
|
56
|
+
*/
|
|
57
|
+
match(handlers) {
|
|
58
|
+
if (this._status === "loading") return handlers.loading();
|
|
59
|
+
if (this._status === "ok") return handlers.ok(this._value);
|
|
60
|
+
return handlers.err(this._error);
|
|
61
|
+
}
|
|
62
|
+
unwrapOr(defaultValue) {
|
|
63
|
+
if (this._status === "ok") return this._value;
|
|
64
|
+
return defaultValue;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* AsyncResult representing an error state
|
|
69
|
+
*/
|
|
70
|
+
var AsyncResultErr = class AsyncResultErr extends AsyncResultBase {
|
|
71
|
+
constructor(error) {
|
|
72
|
+
super("err", void 0, error);
|
|
73
|
+
}
|
|
74
|
+
/** @internal */
|
|
75
|
+
static _create(error) {
|
|
76
|
+
return new AsyncResultErr(error);
|
|
77
|
+
}
|
|
78
|
+
/** Get the error value - only available after isErr() check */
|
|
79
|
+
getError() {
|
|
80
|
+
return this._error;
|
|
81
|
+
}
|
|
82
|
+
getResult() {
|
|
83
|
+
return err(this._error);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
/**
|
|
87
|
+
* AsyncResult representing a loading state
|
|
88
|
+
*/
|
|
89
|
+
var AsyncResultLoading = class AsyncResultLoading extends AsyncResultBase {
|
|
90
|
+
constructor() {
|
|
91
|
+
super("loading");
|
|
92
|
+
}
|
|
93
|
+
/** @internal */
|
|
94
|
+
static _create() {
|
|
95
|
+
return new AsyncResultLoading();
|
|
96
|
+
}
|
|
97
|
+
getResult() {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
/**
|
|
102
|
+
* AsyncResult representing a success state
|
|
103
|
+
*/
|
|
104
|
+
var AsyncResultOk = class AsyncResultOk extends AsyncResultBase {
|
|
105
|
+
constructor(value) {
|
|
106
|
+
super("ok", value);
|
|
107
|
+
}
|
|
108
|
+
/** @internal */
|
|
109
|
+
static _create(value) {
|
|
110
|
+
return new AsyncResultOk(value);
|
|
111
|
+
}
|
|
112
|
+
getResult() {
|
|
113
|
+
return ok(this._value);
|
|
114
|
+
}
|
|
115
|
+
/** Get the success value - only available after isOk() check */
|
|
116
|
+
getValue() {
|
|
117
|
+
return this._value;
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
/**
|
|
121
|
+
* Static factory methods for creating AsyncResult instances.
|
|
122
|
+
* This pattern (same name for type and value) is intentional for ergonomic API.
|
|
123
|
+
*/
|
|
124
|
+
const AsyncResult = {
|
|
125
|
+
err(error) {
|
|
126
|
+
return AsyncResultErr._create(error);
|
|
127
|
+
},
|
|
128
|
+
fromResult(result) {
|
|
129
|
+
if (result.isOk()) return AsyncResult.ok(result.value);
|
|
130
|
+
return AsyncResult.err(result.error);
|
|
131
|
+
},
|
|
132
|
+
loading() {
|
|
133
|
+
return AsyncResultLoading._create();
|
|
134
|
+
},
|
|
135
|
+
ok(value) {
|
|
136
|
+
return AsyncResultOk._create(value);
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
function isAsyncResult(value) {
|
|
140
|
+
return value instanceof AsyncResultErr || value instanceof AsyncResultLoading || value instanceof AsyncResultOk;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
//#endregion
|
|
144
|
+
//#region src/composables/mutation/mutation.composable.ts
|
|
145
|
+
function useMutation(options) {
|
|
146
|
+
const isDebug = options.isDebug ?? false;
|
|
147
|
+
const queryClient = useQueryClient();
|
|
148
|
+
async function onSuccess(responseData, params) {
|
|
149
|
+
if (!options.queryKeysToInvalidate) return;
|
|
150
|
+
await Promise.all(Object.entries(options.queryKeysToInvalidate).map(async ([queryKey, queryKeyParams]) => {
|
|
151
|
+
if (!queryKeyParams) {
|
|
152
|
+
if (isDebug) console.log(`[MUTATION] Invalidating ${queryKey}`);
|
|
153
|
+
await queryClient.invalidateQueries({ queryKey: [queryKey] });
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const qkp = queryKeyParams;
|
|
157
|
+
const paramEntries = Object.entries(qkp);
|
|
158
|
+
if (paramEntries.length === 0) {
|
|
159
|
+
if (isDebug) console.log(`[MUTATION] Invalidating ${queryKey}`);
|
|
160
|
+
await queryClient.invalidateQueries({ queryKey: [queryKey] });
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
const paramsWithValues = paramEntries.reduce((acc, [key, value]) => {
|
|
164
|
+
acc[key] = value(params, responseData);
|
|
165
|
+
return acc;
|
|
166
|
+
}, {});
|
|
167
|
+
if (isDebug) console.log(`[MUTATION] Invalidating ${queryKey}`, paramsWithValues);
|
|
168
|
+
await queryClient.invalidateQueries({
|
|
169
|
+
exact: false,
|
|
170
|
+
queryKey: [queryKey, paramsWithValues]
|
|
171
|
+
});
|
|
172
|
+
}));
|
|
173
|
+
}
|
|
174
|
+
const mutation = useMutation$1({
|
|
175
|
+
mutationFn: options.queryFn,
|
|
176
|
+
onSuccess: async (result$1, variables) => {
|
|
177
|
+
if (!result$1.isOk()) return;
|
|
178
|
+
const data = result$1.value;
|
|
179
|
+
if (variables !== void 0 && "params" in variables) {
|
|
180
|
+
await onSuccess(data, variables.params);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
await onSuccess(data, {});
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
async function execute(data) {
|
|
187
|
+
return await mutation.mutateAsync(data);
|
|
188
|
+
}
|
|
189
|
+
const result = computed(() => {
|
|
190
|
+
if (mutation.isPending.value) return AsyncResult.loading();
|
|
191
|
+
if (mutation.isError.value) return AsyncResult.err(mutation.error.value);
|
|
192
|
+
if (mutation.isSuccess.value && mutation.data.value !== void 0) {
|
|
193
|
+
const apiResult = mutation.data.value;
|
|
194
|
+
if (apiResult.isOk()) return AsyncResult.ok(apiResult.value);
|
|
195
|
+
if (apiResult.isErr()) return AsyncResult.err(apiResult.error);
|
|
196
|
+
}
|
|
197
|
+
return AsyncResult.loading();
|
|
198
|
+
});
|
|
199
|
+
return {
|
|
200
|
+
isLoading: computed(() => mutation.isPending.value),
|
|
201
|
+
data: computed(() => {
|
|
202
|
+
if (mutation.data.value?.isOk()) return mutation.data.value.value;
|
|
203
|
+
return null;
|
|
204
|
+
}),
|
|
205
|
+
execute,
|
|
206
|
+
result
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
//#endregion
|
|
211
|
+
//#region src/config/config.ts
|
|
212
|
+
const DEFAULT_LIMIT$2 = 20;
|
|
213
|
+
const DEFAULT_PREFETCH_STALE_TIME = 60;
|
|
214
|
+
const QUERY_CONFIG = {
|
|
215
|
+
prefetchStaleTime: DEFAULT_PREFETCH_STALE_TIME,
|
|
216
|
+
limit: DEFAULT_LIMIT$2
|
|
217
|
+
};
|
|
218
|
+
let globalQueryClient = null;
|
|
219
|
+
/**
|
|
220
|
+
* Initialize the API utilities with a QueryClient.
|
|
221
|
+
* Call this once during app setup (e.g. in a plugin or main.ts).
|
|
222
|
+
*
|
|
223
|
+
* After calling this, `createApiUtils()` can be called without options.
|
|
224
|
+
*
|
|
225
|
+
* @example
|
|
226
|
+
* ```typescript
|
|
227
|
+
* import { initializeApiUtils } from '@wisemen/vue-core-api-utils'
|
|
228
|
+
*
|
|
229
|
+
* const queryClient = new QueryClient()
|
|
230
|
+
* initializeApiUtils(queryClient)
|
|
231
|
+
*
|
|
232
|
+
* // Then in your api lib:
|
|
233
|
+
* export const { useQuery, useMutation, ... } = createApiUtils<MyQueryKeys>()
|
|
234
|
+
* ```
|
|
235
|
+
*/
|
|
236
|
+
function initializeApiUtils(queryClient) {
|
|
237
|
+
globalQueryClient = queryClient;
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* @internal
|
|
241
|
+
*/
|
|
242
|
+
function getQueryClient() {
|
|
243
|
+
if (globalQueryClient == null) throw new Error("[api-utils] QueryClient not available. Call initializeApiUtils(queryClient) before using createApiUtils().");
|
|
244
|
+
return globalQueryClient;
|
|
245
|
+
}
|
|
246
|
+
function setQueryConfig(config) {
|
|
247
|
+
if (config.limit != null && config.limit > 0) QUERY_CONFIG.limit = config.limit;
|
|
248
|
+
if (config.prefetchStaleTime != null && config.prefetchStaleTime > 0) QUERY_CONFIG.prefetchStaleTime = config.prefetchStaleTime;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
//#endregion
|
|
252
|
+
//#region src/composables/query/keysetInfiniteQuery.composable.ts
|
|
253
|
+
const DEFAULT_LIMIT$1 = QUERY_CONFIG.limit;
|
|
254
|
+
function useKeysetInfiniteQuery(key, options) {
|
|
255
|
+
const queryKey = [key, options.params];
|
|
256
|
+
const infiniteQuery = useInfiniteQuery({
|
|
257
|
+
staleTime: options.staleTime,
|
|
258
|
+
enabled: options.isEnabled,
|
|
259
|
+
getNextPageParam: (lastPage) => {
|
|
260
|
+
if (lastPage.isErr() || lastPage.isLoading()) return null;
|
|
261
|
+
return lastPage.getValue().meta.next ?? null;
|
|
262
|
+
},
|
|
263
|
+
initialPageParam: void 0,
|
|
264
|
+
placeholderData: (data) => data,
|
|
265
|
+
queryFn: async ({ pageParam }) => {
|
|
266
|
+
return AsyncResult.fromResult(await options.queryFn({
|
|
267
|
+
key: pageParam,
|
|
268
|
+
limit: options.limit ?? DEFAULT_LIMIT$1
|
|
269
|
+
}));
|
|
270
|
+
},
|
|
271
|
+
queryKey
|
|
272
|
+
});
|
|
273
|
+
const hasError = computed(() => {
|
|
274
|
+
return Boolean(infiniteQuery.data.value?.pages.find((page) => page.isErr()));
|
|
275
|
+
});
|
|
276
|
+
const result = computed(() => {
|
|
277
|
+
if (infiniteQuery.isLoading.value) return AsyncResult.loading();
|
|
278
|
+
const firstError = infiniteQuery.data.value?.pages.find((page) => page.isErr());
|
|
279
|
+
if (firstError) return AsyncResult.err(firstError.getError());
|
|
280
|
+
const data = infiniteQuery.data.value?.pages.filter((page) => page.isOk()).flatMap((page) => page.getValue().data) ?? [];
|
|
281
|
+
const firstPage = infiniteQuery.data.value?.pages[0];
|
|
282
|
+
const meta = firstPage?.isOk() ? firstPage.getValue().meta : { next: null };
|
|
283
|
+
const response = {
|
|
284
|
+
data,
|
|
285
|
+
meta: { next: infiniteQuery.hasNextPage.value ? meta.next : null }
|
|
286
|
+
};
|
|
287
|
+
return AsyncResult.ok(response);
|
|
288
|
+
});
|
|
289
|
+
function fetchNextPage() {
|
|
290
|
+
if (!infiniteQuery.hasNextPage.value || infiniteQuery.isFetchingNextPage.value) return;
|
|
291
|
+
return infiniteQuery.fetchNextPage();
|
|
292
|
+
}
|
|
293
|
+
return {
|
|
294
|
+
hasNextPage: computed(() => infiniteQuery.hasNextPage.value),
|
|
295
|
+
isError: computed(() => hasError.value),
|
|
296
|
+
isFetching: computed(() => infiniteQuery.isFetching.value),
|
|
297
|
+
isFetchingNextPage: computed(() => infiniteQuery.isFetchingNextPage.value),
|
|
298
|
+
isLoading: computed(() => infiniteQuery.isLoading.value),
|
|
299
|
+
isSuccess: computed(() => !hasError.value),
|
|
300
|
+
fetchNextPage: async () => {
|
|
301
|
+
await fetchNextPage();
|
|
302
|
+
},
|
|
303
|
+
refetch: async () => {
|
|
304
|
+
await infiniteQuery.refetch();
|
|
305
|
+
},
|
|
306
|
+
result
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
//#endregion
|
|
311
|
+
//#region src/composables/query/offsetInfiniteQuery.composable.ts
|
|
312
|
+
const DEFAULT_LIMIT = QUERY_CONFIG.limit;
|
|
313
|
+
function useOffsetInfiniteQuery(key, options) {
|
|
314
|
+
const queryKey = [key, options.params];
|
|
315
|
+
const infiniteQuery = useInfiniteQuery({
|
|
316
|
+
staleTime: options.staleTime,
|
|
317
|
+
enabled: options.isEnabled,
|
|
318
|
+
getNextPageParam: (lastPage) => {
|
|
319
|
+
if (lastPage.isErr() || lastPage.isLoading()) return null;
|
|
320
|
+
const total = lastPage.getValue().meta.offset + lastPage.getValue().meta.limit;
|
|
321
|
+
if (total >= lastPage.getValue().meta.total) return null;
|
|
322
|
+
return total;
|
|
323
|
+
},
|
|
324
|
+
initialPageParam: 0,
|
|
325
|
+
placeholderData: (data) => data,
|
|
326
|
+
queryFn: async ({ pageParam }) => AsyncResult.fromResult(await options.queryFn({
|
|
327
|
+
limit: options.limit ?? DEFAULT_LIMIT,
|
|
328
|
+
offset: pageParam ?? 0
|
|
329
|
+
})),
|
|
330
|
+
queryKey
|
|
331
|
+
});
|
|
332
|
+
const hasError = computed(() => {
|
|
333
|
+
return Boolean(infiniteQuery.data.value?.pages.find((page) => page.isErr()));
|
|
334
|
+
});
|
|
335
|
+
const result = computed(() => {
|
|
336
|
+
if (infiniteQuery.isLoading.value) return AsyncResult.loading();
|
|
337
|
+
const firstError = infiniteQuery.data.value?.pages.find((page) => page.isErr());
|
|
338
|
+
if (firstError) return AsyncResult.err(firstError.getError());
|
|
339
|
+
const data = infiniteQuery.data.value?.pages.filter((page) => page.isOk()).flatMap((page) => page.getValue().data) ?? [];
|
|
340
|
+
const firstPage = infiniteQuery.data.value?.pages[0];
|
|
341
|
+
const meta = firstPage?.isOk() ? firstPage.getValue().meta : null;
|
|
342
|
+
const response = {
|
|
343
|
+
data,
|
|
344
|
+
meta: {
|
|
345
|
+
limit: meta?.limit ?? 0,
|
|
346
|
+
offset: meta?.offset ?? 0,
|
|
347
|
+
total: meta?.total ?? data.length
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
return AsyncResult.ok(response);
|
|
351
|
+
});
|
|
352
|
+
function fetchNextPage() {
|
|
353
|
+
if (!infiniteQuery.hasNextPage.value || infiniteQuery.isFetchingNextPage.value) return;
|
|
354
|
+
return infiniteQuery.fetchNextPage();
|
|
355
|
+
}
|
|
356
|
+
return {
|
|
357
|
+
hasNextPage: computed(() => infiniteQuery.hasNextPage.value),
|
|
358
|
+
isError: computed(() => hasError.value),
|
|
359
|
+
isFetching: computed(() => infiniteQuery.isFetching.value),
|
|
360
|
+
isFetchingNextPage: computed(() => infiniteQuery.isFetchingNextPage.value),
|
|
361
|
+
isLoading: computed(() => infiniteQuery.isLoading.value),
|
|
362
|
+
isSuccess: computed(() => !hasError.value),
|
|
363
|
+
fetchNextPage: async () => {
|
|
364
|
+
await fetchNextPage();
|
|
365
|
+
},
|
|
366
|
+
refetch: async () => {
|
|
367
|
+
await infiniteQuery.refetch();
|
|
368
|
+
},
|
|
369
|
+
result
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
//#endregion
|
|
374
|
+
//#region src/composables/query/prefetchKeysetInfiniteQuery.composable.ts
|
|
375
|
+
function usePrefetchKeysetInfiniteQuery(key, options) {
|
|
376
|
+
const queryClient = useQueryClient();
|
|
377
|
+
const params = options.params;
|
|
378
|
+
async function execute() {
|
|
379
|
+
await queryClient.prefetchInfiniteQuery({
|
|
380
|
+
staleTime: options.staleTime ?? QUERY_CONFIG.prefetchStaleTime,
|
|
381
|
+
getNextPageParam: (lastPage) => {
|
|
382
|
+
if (!lastPage.isOk()) return null;
|
|
383
|
+
const next = lastPage.getValue().meta.next;
|
|
384
|
+
return next === null || next === void 0 ? null : next;
|
|
385
|
+
},
|
|
386
|
+
initialPageParam: void 0,
|
|
387
|
+
queryFn: async ({ pageParam }) => AsyncResult.fromResult(await options.queryFn({
|
|
388
|
+
key: pageParam,
|
|
389
|
+
limit: options.limit ?? QUERY_CONFIG.limit
|
|
390
|
+
})),
|
|
391
|
+
queryKey: [key, params]
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
return { execute };
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
//#endregion
|
|
398
|
+
//#region src/composables/query/prefetchOffsetInfiniteQuery.composable.ts
|
|
399
|
+
function usePrefetchOffsetInfiniteQuery(key, options) {
|
|
400
|
+
const queryClient = useQueryClient();
|
|
401
|
+
const params = options.params;
|
|
402
|
+
async function execute() {
|
|
403
|
+
await queryClient.prefetchInfiniteQuery({
|
|
404
|
+
staleTime: options.staleTime ?? QUERY_CONFIG.prefetchStaleTime,
|
|
405
|
+
getNextPageParam: (lastPage) => {
|
|
406
|
+
if (!lastPage.isOk()) return null;
|
|
407
|
+
const total = lastPage.getValue().meta.offset + lastPage.getValue().meta.limit;
|
|
408
|
+
if (total >= lastPage.getValue().meta.total) return null;
|
|
409
|
+
return total;
|
|
410
|
+
},
|
|
411
|
+
initialPageParam: 0,
|
|
412
|
+
queryFn: async ({ pageParam }) => AsyncResult.fromResult(await options.queryFn({
|
|
413
|
+
limit: options.limit ?? QUERY_CONFIG.limit,
|
|
414
|
+
offset: pageParam ?? 0
|
|
415
|
+
})),
|
|
416
|
+
queryKey: [key, params]
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
return { execute };
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
//#endregion
|
|
423
|
+
//#region src/composables/query/prefetchQuery.composable.ts
|
|
424
|
+
function usePrefetchQuery(key, options) {
|
|
425
|
+
const queryClient = useQueryClient();
|
|
426
|
+
const params = options.params;
|
|
427
|
+
async function execute() {
|
|
428
|
+
await queryClient.prefetchQuery({
|
|
429
|
+
staleTime: options.staleTime ?? QUERY_CONFIG.prefetchStaleTime,
|
|
430
|
+
queryFn: async () => AsyncResult.fromResult(await options.queryFn()),
|
|
431
|
+
queryKey: [key, params]
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
return { execute };
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
//#endregion
|
|
438
|
+
//#region src/composables/query/query.composable.ts
|
|
439
|
+
function useQuery(key, options) {
|
|
440
|
+
const isDebug = options.isDebug ?? false;
|
|
441
|
+
const params = options.params;
|
|
442
|
+
const query = useQuery$1({
|
|
443
|
+
staleTime: options.staleTime,
|
|
444
|
+
enabled: options.isEnabled,
|
|
445
|
+
placeholderData: (data) => data,
|
|
446
|
+
queryFn: async () => {
|
|
447
|
+
return AsyncResult.fromResult(await options.queryFn());
|
|
448
|
+
},
|
|
449
|
+
queryKey: [key, params]
|
|
450
|
+
});
|
|
451
|
+
if (isDebug) console.debug(`Create query with key ${String(key)}`, params);
|
|
452
|
+
async function refetch() {
|
|
453
|
+
await query.refetch();
|
|
454
|
+
}
|
|
455
|
+
return {
|
|
456
|
+
isError: computed(() => query.data.value?.isErr() ?? false),
|
|
457
|
+
isFetching: computed(() => query.isFetching.value),
|
|
458
|
+
isLoading: computed(() => query.isLoading.value),
|
|
459
|
+
isSuccess: computed(() => query.data.value?.isOk() ?? false),
|
|
460
|
+
refetch,
|
|
461
|
+
result: computed(() => {
|
|
462
|
+
if (query.isLoading.value) return AsyncResult.loading();
|
|
463
|
+
if (query.data.value?.isOk()) return AsyncResult.ok(query.data.value.getValue());
|
|
464
|
+
if (query.data.value?.isErr()) return AsyncResult.err(query.data.value.getError());
|
|
465
|
+
return AsyncResult.loading();
|
|
466
|
+
})
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
//#endregion
|
|
471
|
+
//#region src/plugin/apiUtilsPlugin.ts
|
|
472
|
+
/**
|
|
473
|
+
* Create a Vue plugin that sets up TanStack Query and initializes API utilities.
|
|
474
|
+
*
|
|
475
|
+
* This plugin handles:
|
|
476
|
+
* - Creating a QueryClient with the provided config
|
|
477
|
+
* - Installing VueQueryPlugin on the app
|
|
478
|
+
* - Initializing the global QueryClient for api-utils
|
|
479
|
+
*
|
|
480
|
+
* @example
|
|
481
|
+
* ```typescript
|
|
482
|
+
* import { apiUtilsPlugin } from '@wisemen/vue-core-api-utils'
|
|
483
|
+
* import { vueQueryClientConfig } from '@wisemen/vue-core-configs'
|
|
484
|
+
*
|
|
485
|
+
* app.use(apiUtilsPlugin(vueQueryClientConfig()))
|
|
486
|
+
* ```
|
|
487
|
+
*
|
|
488
|
+
* @param config - QueryClient configuration
|
|
489
|
+
* @returns A Vue plugin that can be used with app.use()
|
|
490
|
+
*/
|
|
491
|
+
function apiUtilsPlugin(config) {
|
|
492
|
+
const queryClient = new QueryClient$1(config);
|
|
493
|
+
return { install: (app) => {
|
|
494
|
+
app.use(VueQueryPlugin, { queryClient });
|
|
495
|
+
initializeApiUtils(queryClient);
|
|
496
|
+
} };
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
//#endregion
|
|
500
|
+
//#region src/utils/api-error/apiError.util.ts
|
|
501
|
+
var ApiErrorUtil = class ApiErrorUtil {
|
|
502
|
+
static getApiErrorCode(error) {
|
|
503
|
+
return error.errors?.[0]?.code ?? null;
|
|
504
|
+
}
|
|
505
|
+
static getApiErrorMessage(error) {
|
|
506
|
+
return error.errors?.[0]?.detail ?? null;
|
|
507
|
+
}
|
|
508
|
+
static getMessage(error) {
|
|
509
|
+
return error.errors?.[0]?.detail ?? null;
|
|
510
|
+
}
|
|
511
|
+
static handleApiError({ error, message }) {
|
|
512
|
+
if (ApiErrorUtil.isExpectedApiError(error)) return error;
|
|
513
|
+
console.error(`Unexpected API error: ${error}`);
|
|
514
|
+
if (error instanceof Error) return error;
|
|
515
|
+
return new Error(message ?? "An unknown error occurred");
|
|
516
|
+
}
|
|
517
|
+
static isExpectedApiError(error) {
|
|
518
|
+
return error?.errors !== void 0;
|
|
519
|
+
}
|
|
520
|
+
static isZodError(error) {
|
|
521
|
+
return error instanceof z.ZodError;
|
|
522
|
+
}
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
//#endregion
|
|
526
|
+
//#region src/utils/api/api.util.ts
|
|
527
|
+
var ApiUtil = class ApiUtil {
|
|
528
|
+
static async fromPromise(promise, message) {
|
|
529
|
+
return await ResultAsync.fromPromise(promise, (error) => {
|
|
530
|
+
return ApiErrorUtil.handleApiError({
|
|
531
|
+
error,
|
|
532
|
+
message
|
|
533
|
+
});
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
static getKeysetPaginationNextOffset(keysetPaginationMeta) {
|
|
537
|
+
return keysetPaginationMeta.next?.offset ?? null;
|
|
538
|
+
}
|
|
539
|
+
static getResultError(result) {
|
|
540
|
+
if (result === null) return null;
|
|
541
|
+
if (!result.isErr()) return null;
|
|
542
|
+
if (ApiUtil.isAsyncResult(result)) return result.getError();
|
|
543
|
+
return result.error;
|
|
544
|
+
}
|
|
545
|
+
static isAsyncResult(value) {
|
|
546
|
+
return value.getResult !== void 0;
|
|
547
|
+
}
|
|
548
|
+
static void(result) {
|
|
549
|
+
if (result.isErr()) return err(result.error);
|
|
550
|
+
return ok();
|
|
551
|
+
}
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
//#endregion
|
|
555
|
+
//#region src/utils/query-client/queryClient.ts
|
|
556
|
+
/**
|
|
557
|
+
* QueryClient utility class for type-safe query operations
|
|
558
|
+
*/
|
|
559
|
+
var QueryClient = class {
|
|
560
|
+
constructor(queryClient) {
|
|
561
|
+
this.queryClient = queryClient;
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* Extract the raw entity from AsyncResult data
|
|
565
|
+
*/
|
|
566
|
+
extractEntityFromAsyncResult(data) {
|
|
567
|
+
if (data.isOk()) return data.getValue();
|
|
568
|
+
return null;
|
|
569
|
+
}
|
|
570
|
+
hasDataArray(value) {
|
|
571
|
+
return Boolean(value && typeof value === "object" && Array.isArray(value.data));
|
|
572
|
+
}
|
|
573
|
+
isInfiniteDataLike(data) {
|
|
574
|
+
return Boolean(data && typeof data === "object" && "pages" in data && Array.isArray(data.pages));
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* Determine if an item should be updated
|
|
578
|
+
*/
|
|
579
|
+
shouldUpdateItem(by, item) {
|
|
580
|
+
return by(item);
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* Internal method to update entity based on the "by" option
|
|
584
|
+
*/
|
|
585
|
+
updateEntity(by, currentData, value) {
|
|
586
|
+
if (Array.isArray(currentData)) return currentData.map((item) => {
|
|
587
|
+
return this.shouldUpdateItem(by, item) ? value(item) : item;
|
|
588
|
+
});
|
|
589
|
+
if (this.shouldUpdateItem(by, currentData)) return value(currentData);
|
|
590
|
+
return currentData;
|
|
591
|
+
}
|
|
592
|
+
updateInfinitePageValue(by, value, pageValue) {
|
|
593
|
+
if (!this.hasDataArray(pageValue)) return pageValue;
|
|
594
|
+
return {
|
|
595
|
+
...pageValue,
|
|
596
|
+
data: this.updateEntity(by, pageValue.data, value)
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* Wrap a raw entity in an AsyncResult (preserving ok state)
|
|
601
|
+
*/
|
|
602
|
+
wrapEntityInAsyncResult(entity) {
|
|
603
|
+
return AsyncResult.ok(entity);
|
|
604
|
+
}
|
|
605
|
+
get(queryKey, options) {
|
|
606
|
+
if (Array.isArray(queryKey)) {
|
|
607
|
+
const data = this.queryClient.getQueryData(queryKey);
|
|
608
|
+
if (data == null) return null;
|
|
609
|
+
return this.extractEntityFromAsyncResult(data);
|
|
610
|
+
}
|
|
611
|
+
if (options?.isExact ?? false) {
|
|
612
|
+
const normalizedKey = [queryKey];
|
|
613
|
+
const data = this.queryClient.getQueryData(normalizedKey);
|
|
614
|
+
if (data == null) return null;
|
|
615
|
+
return this.extractEntityFromAsyncResult(data);
|
|
616
|
+
}
|
|
617
|
+
const allQueries = this.queryClient.getQueryCache().findAll({ predicate: (query) => {
|
|
618
|
+
return query.queryKey[0] === queryKey;
|
|
619
|
+
} });
|
|
620
|
+
const results = [];
|
|
621
|
+
for (const query of allQueries) {
|
|
622
|
+
const data = query.state.data;
|
|
623
|
+
const entity = this.extractEntityFromAsyncResult(data);
|
|
624
|
+
if (entity !== null) results.push(entity);
|
|
625
|
+
}
|
|
626
|
+
return results;
|
|
627
|
+
}
|
|
628
|
+
async invalidate(keyOrTuple) {
|
|
629
|
+
const isSpecific = Array.isArray(keyOrTuple);
|
|
630
|
+
const key = isSpecific ? keyOrTuple[0] : keyOrTuple;
|
|
631
|
+
const params = isSpecific ? keyOrTuple[1] : null;
|
|
632
|
+
await this.queryClient.invalidateQueries({ predicate: (query) => {
|
|
633
|
+
const queryKey = query.queryKey;
|
|
634
|
+
if (queryKey[0] !== key) return false;
|
|
635
|
+
if (isSpecific && params && queryKey[1]) return Object.entries(params).every(([paramKey, paramValue]) => {
|
|
636
|
+
return queryKey[1][paramKey] === paramValue;
|
|
637
|
+
});
|
|
638
|
+
return !isSpecific;
|
|
639
|
+
} });
|
|
640
|
+
}
|
|
641
|
+
set(queryKey, entity) {
|
|
642
|
+
const wrappedData = this.wrapEntityInAsyncResult(entity);
|
|
643
|
+
const normalizedKey = Array.isArray(queryKey) ? queryKey : [queryKey];
|
|
644
|
+
this.queryClient.setQueryData(normalizedKey, wrappedData);
|
|
645
|
+
}
|
|
646
|
+
update(keyOrTuple, options) {
|
|
647
|
+
const by = options.by;
|
|
648
|
+
const value = options.value;
|
|
649
|
+
const isSpecific = Array.isArray(keyOrTuple);
|
|
650
|
+
const key = isSpecific ? keyOrTuple[0] : keyOrTuple;
|
|
651
|
+
const params = isSpecific ? keyOrTuple[1] : null;
|
|
652
|
+
const queries = this.queryClient.getQueryCache().findAll({ predicate: (query) => {
|
|
653
|
+
const queryKey = query.queryKey;
|
|
654
|
+
if (queryKey[0] !== key) return false;
|
|
655
|
+
if (isSpecific && params && queryKey[1]) return Object.entries(params).every(([paramKey, paramValue]) => {
|
|
656
|
+
return queryKey[1][paramKey] === paramValue;
|
|
657
|
+
});
|
|
658
|
+
return !isSpecific;
|
|
659
|
+
} });
|
|
660
|
+
const snapshots = /* @__PURE__ */ new Map();
|
|
661
|
+
for (const query of queries) {
|
|
662
|
+
const currentData = query.state.data;
|
|
663
|
+
if (this.isInfiniteDataLike(currentData)) {
|
|
664
|
+
snapshots.set(query.queryKey, currentData);
|
|
665
|
+
const updatedInfiniteData = {
|
|
666
|
+
...currentData,
|
|
667
|
+
pages: currentData.pages.map((page) => {
|
|
668
|
+
if (!isAsyncResult(page)) return page;
|
|
669
|
+
return page.map((pageValue) => this.updateInfinitePageValue(by, value, pageValue));
|
|
670
|
+
})
|
|
671
|
+
};
|
|
672
|
+
this.queryClient.setQueryData(query.queryKey, updatedInfiniteData);
|
|
673
|
+
continue;
|
|
674
|
+
}
|
|
675
|
+
if (!isAsyncResult(currentData)) continue;
|
|
676
|
+
const rawEntity = this.extractEntityFromAsyncResult(currentData);
|
|
677
|
+
if (rawEntity === null) continue;
|
|
678
|
+
snapshots.set(query.queryKey, currentData);
|
|
679
|
+
const updatedEntity = this.updateEntity(by, rawEntity, value);
|
|
680
|
+
const wrappedData = this.wrapEntityInAsyncResult(updatedEntity);
|
|
681
|
+
this.queryClient.setQueryData(query.queryKey, wrappedData);
|
|
682
|
+
}
|
|
683
|
+
let rolledBack = false;
|
|
684
|
+
return { rollback: () => {
|
|
685
|
+
if (rolledBack) return;
|
|
686
|
+
rolledBack = true;
|
|
687
|
+
for (const [queryKey, data] of snapshots) this.queryClient.setQueryData(queryKey, data);
|
|
688
|
+
} };
|
|
689
|
+
}
|
|
690
|
+
};
|
|
691
|
+
|
|
692
|
+
//#endregion
|
|
693
|
+
//#region src/utils/sort/sort.utils.ts
|
|
694
|
+
var SortDirectionDto = /* @__PURE__ */ function(SortDirectionDto$1) {
|
|
695
|
+
SortDirectionDto$1["ASC"] = "asc";
|
|
696
|
+
SortDirectionDto$1["DESC"] = "desc";
|
|
697
|
+
return SortDirectionDto$1;
|
|
698
|
+
}(SortDirectionDto || {});
|
|
699
|
+
var SortUtil = class {
|
|
700
|
+
static toDto(sort, sortKeyMap) {
|
|
701
|
+
return sort.filter((s) => s.direction !== null).map((s) => ({
|
|
702
|
+
key: sortKeyMap[s.key],
|
|
703
|
+
order: s.direction === "asc" ? SortDirectionDto.ASC : SortDirectionDto.DESC
|
|
704
|
+
}));
|
|
705
|
+
}
|
|
706
|
+
};
|
|
707
|
+
|
|
708
|
+
//#endregion
|
|
709
|
+
export { ApiErrorUtil, ApiUtil, AsyncResult, AsyncResultErr, AsyncResultLoading, AsyncResultOk, QueryClient, SortUtil, apiUtilsPlugin, getQueryClient as getTanstackQueryClient, initializeApiUtils, setQueryConfig, useKeysetInfiniteQuery, useMutation, useOffsetInfiniteQuery, usePrefetchKeysetInfiniteQuery, usePrefetchOffsetInfiniteQuery, usePrefetchQuery, useQuery };
|