enlace 0.0.1-beta.3 → 0.0.1-beta.5
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 +188 -0
- package/dist/index.d.mts +27 -4
- package/dist/index.d.ts +27 -4
- package/dist/index.js +89 -24
- package/dist/index.mjs +90 -27
- package/dist/next/hook/index.d.mts +27 -5
- package/dist/next/hook/index.d.ts +27 -5
- package/dist/next/hook/index.js +113 -70
- package/dist/next/hook/index.mjs +114 -73
- package/dist/next/index.d.mts +65 -6
- package/dist/next/index.d.ts +65 -6
- package/dist/next/index.js +26 -48
- package/dist/next/index.mjs +27 -51
- package/package.json +2 -2
package/dist/next/hook/index.mjs
CHANGED
|
@@ -29,9 +29,7 @@ import {
|
|
|
29
29
|
|
|
30
30
|
// src/next/fetch.ts
|
|
31
31
|
import {
|
|
32
|
-
|
|
33
|
-
isJsonBody,
|
|
34
|
-
mergeHeaders
|
|
32
|
+
executeFetch
|
|
35
33
|
} from "enlace-core";
|
|
36
34
|
|
|
37
35
|
// src/utils/generateTags.ts
|
|
@@ -45,20 +43,22 @@ async function executeNextFetch(baseUrl, path, method, combinedOptions, requestO
|
|
|
45
43
|
autoGenerateTags = true,
|
|
46
44
|
autoRevalidateTags = true,
|
|
47
45
|
revalidator,
|
|
48
|
-
|
|
49
|
-
...
|
|
46
|
+
onSuccess,
|
|
47
|
+
...coreOptions
|
|
50
48
|
} = combinedOptions;
|
|
51
|
-
const url = buildUrl(baseUrl, path, requestOptions?.query);
|
|
52
|
-
let headers = mergeHeaders(defaultHeaders, requestOptions?.headers);
|
|
53
49
|
const isGet = method === "GET";
|
|
54
50
|
const autoTags = generateTags(path);
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
51
|
+
const nextOnSuccess = (payload) => {
|
|
52
|
+
if (!isGet && !requestOptions?.skipRevalidator) {
|
|
53
|
+
const revalidateTags = requestOptions?.revalidateTags ?? (autoRevalidateTags ? autoTags : []);
|
|
54
|
+
const revalidatePaths = requestOptions?.revalidatePaths ?? [];
|
|
55
|
+
if (revalidateTags.length || revalidatePaths.length) {
|
|
56
|
+
revalidator?.(revalidateTags, revalidatePaths);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
onSuccess?.(payload);
|
|
58
60
|
};
|
|
59
|
-
|
|
60
|
-
fetchOptions.cache = requestOptions.cache;
|
|
61
|
-
}
|
|
61
|
+
const nextRequestOptions = { ...requestOptions };
|
|
62
62
|
if (isGet) {
|
|
63
63
|
const tags = requestOptions?.tags ?? (autoGenerateTags ? autoTags : void 0);
|
|
64
64
|
const nextFetchOptions = {};
|
|
@@ -68,44 +68,15 @@ async function executeNextFetch(baseUrl, path, method, combinedOptions, requestO
|
|
|
68
68
|
if (requestOptions?.revalidate !== void 0) {
|
|
69
69
|
nextFetchOptions.revalidate = requestOptions.revalidate;
|
|
70
70
|
}
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
if (headers) {
|
|
74
|
-
fetchOptions.headers = headers;
|
|
75
|
-
}
|
|
76
|
-
if (requestOptions?.body !== void 0) {
|
|
77
|
-
if (isJsonBody(requestOptions.body)) {
|
|
78
|
-
fetchOptions.body = JSON.stringify(requestOptions.body);
|
|
79
|
-
headers = mergeHeaders(headers, { "Content-Type": "application/json" });
|
|
80
|
-
if (headers) {
|
|
81
|
-
fetchOptions.headers = headers;
|
|
82
|
-
}
|
|
83
|
-
} else {
|
|
84
|
-
fetchOptions.body = requestOptions.body;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
const response = await fetch(url, fetchOptions);
|
|
88
|
-
const contentType = response.headers.get("content-type");
|
|
89
|
-
const isJson = contentType?.includes("application/json");
|
|
90
|
-
if (response.ok) {
|
|
91
|
-
if (!isGet && !requestOptions?.skipRevalidator) {
|
|
92
|
-
const revalidateTags = requestOptions?.revalidateTags ?? (autoRevalidateTags ? autoTags : []);
|
|
93
|
-
const revalidatePaths = requestOptions?.revalidatePaths ?? [];
|
|
94
|
-
if (revalidateTags.length || revalidatePaths.length) {
|
|
95
|
-
revalidator?.(revalidateTags, revalidatePaths);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
return {
|
|
99
|
-
ok: true,
|
|
100
|
-
status: response.status,
|
|
101
|
-
data: isJson ? await response.json() : response
|
|
102
|
-
};
|
|
71
|
+
nextRequestOptions.next = nextFetchOptions;
|
|
103
72
|
}
|
|
104
|
-
return
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
73
|
+
return executeFetch(
|
|
74
|
+
baseUrl,
|
|
75
|
+
path,
|
|
76
|
+
method,
|
|
77
|
+
{ ...coreOptions, onSuccess: nextOnSuccess },
|
|
78
|
+
nextRequestOptions
|
|
79
|
+
);
|
|
109
80
|
}
|
|
110
81
|
|
|
111
82
|
// src/next/index.ts
|
|
@@ -113,7 +84,12 @@ __reExport(next_exports, enlace_core_star);
|
|
|
113
84
|
import * as enlace_core_star from "enlace-core";
|
|
114
85
|
function createEnlace(baseUrl, defaultOptions = {}, nextOptions = {}) {
|
|
115
86
|
const combinedOptions = { ...defaultOptions, ...nextOptions };
|
|
116
|
-
return createProxyHandler(
|
|
87
|
+
return createProxyHandler(
|
|
88
|
+
baseUrl,
|
|
89
|
+
combinedOptions,
|
|
90
|
+
[],
|
|
91
|
+
executeNextFetch
|
|
92
|
+
);
|
|
117
93
|
}
|
|
118
94
|
|
|
119
95
|
// src/react/useQueryMode.ts
|
|
@@ -130,7 +106,7 @@ var initialState = {
|
|
|
130
106
|
function hookReducer(state, action) {
|
|
131
107
|
switch (action.type) {
|
|
132
108
|
case "RESET":
|
|
133
|
-
return action.state;
|
|
109
|
+
return action.state ?? initialState;
|
|
134
110
|
case "FETCH_START":
|
|
135
111
|
return {
|
|
136
112
|
...state,
|
|
@@ -255,11 +231,29 @@ function onRevalidate(callback) {
|
|
|
255
231
|
}
|
|
256
232
|
|
|
257
233
|
// src/react/useQueryMode.ts
|
|
234
|
+
function resolvePath(path, pathParams) {
|
|
235
|
+
if (!pathParams) return path;
|
|
236
|
+
return path.map((segment) => {
|
|
237
|
+
if (segment.startsWith(":")) {
|
|
238
|
+
const paramName = segment.slice(1);
|
|
239
|
+
const value = pathParams[paramName];
|
|
240
|
+
if (value === void 0) {
|
|
241
|
+
throw new Error(`Missing path parameter: ${paramName}`);
|
|
242
|
+
}
|
|
243
|
+
return String(value);
|
|
244
|
+
}
|
|
245
|
+
return segment;
|
|
246
|
+
});
|
|
247
|
+
}
|
|
258
248
|
function useQueryMode(api, trackedCall, options) {
|
|
259
|
-
const { autoGenerateTags, staleTime } = options;
|
|
249
|
+
const { autoGenerateTags, staleTime, enabled } = options;
|
|
260
250
|
const queryKey = createQueryKey(trackedCall);
|
|
261
251
|
const requestOptions = trackedCall.options;
|
|
262
|
-
const
|
|
252
|
+
const resolvedPath = resolvePath(
|
|
253
|
+
trackedCall.path,
|
|
254
|
+
requestOptions?.pathParams
|
|
255
|
+
);
|
|
256
|
+
const queryTags = requestOptions?.tags ?? (autoGenerateTags ? generateTags(resolvedPath) : []);
|
|
263
257
|
const getCacheState = (includeNeedsFetch = false) => {
|
|
264
258
|
const cached = getCache(queryKey);
|
|
265
259
|
const hasCachedData = cached?.data !== void 0;
|
|
@@ -273,17 +267,22 @@ function useQueryMode(api, trackedCall, options) {
|
|
|
273
267
|
error: cached?.error
|
|
274
268
|
};
|
|
275
269
|
};
|
|
276
|
-
const [state, dispatch] = useReducer(
|
|
270
|
+
const [state, dispatch] = useReducer(
|
|
271
|
+
hookReducer,
|
|
272
|
+
null,
|
|
273
|
+
() => getCacheState(true)
|
|
274
|
+
);
|
|
277
275
|
const mountedRef = useRef(true);
|
|
278
276
|
const fetchRef = useRef(null);
|
|
279
277
|
useEffect(() => {
|
|
280
278
|
mountedRef.current = true;
|
|
279
|
+
if (!enabled) {
|
|
280
|
+
dispatch({ type: "RESET" });
|
|
281
|
+
return () => {
|
|
282
|
+
mountedRef.current = false;
|
|
283
|
+
};
|
|
284
|
+
}
|
|
281
285
|
dispatch({ type: "RESET", state: getCacheState(true) });
|
|
282
|
-
const unsubscribe = subscribeCache(queryKey, () => {
|
|
283
|
-
if (mountedRef.current) {
|
|
284
|
-
dispatch({ type: "SYNC_CACHE", state: getCacheState() });
|
|
285
|
-
}
|
|
286
|
-
});
|
|
287
286
|
const doFetch = () => {
|
|
288
287
|
const cached2 = getCache(queryKey);
|
|
289
288
|
if (cached2?.promise) {
|
|
@@ -291,7 +290,7 @@ function useQueryMode(api, trackedCall, options) {
|
|
|
291
290
|
}
|
|
292
291
|
dispatch({ type: "FETCH_START" });
|
|
293
292
|
let current = api;
|
|
294
|
-
for (const segment of
|
|
293
|
+
for (const segment of resolvedPath) {
|
|
295
294
|
current = current[segment];
|
|
296
295
|
}
|
|
297
296
|
const method = current[trackedCall.method];
|
|
@@ -299,7 +298,7 @@ function useQueryMode(api, trackedCall, options) {
|
|
|
299
298
|
if (mountedRef.current) {
|
|
300
299
|
setCache(queryKey, {
|
|
301
300
|
data: res.ok ? res.data : void 0,
|
|
302
|
-
error: res.ok ? void 0 : res.error,
|
|
301
|
+
error: res.ok || res.status === 0 ? void 0 : res.error,
|
|
303
302
|
ok: res.ok,
|
|
304
303
|
timestamp: Date.now(),
|
|
305
304
|
tags: queryTags
|
|
@@ -318,12 +317,17 @@ function useQueryMode(api, trackedCall, options) {
|
|
|
318
317
|
} else {
|
|
319
318
|
doFetch();
|
|
320
319
|
}
|
|
320
|
+
const unsubscribe = subscribeCache(queryKey, () => {
|
|
321
|
+
if (mountedRef.current) {
|
|
322
|
+
dispatch({ type: "SYNC_CACHE", state: getCacheState() });
|
|
323
|
+
}
|
|
324
|
+
});
|
|
321
325
|
return () => {
|
|
322
326
|
mountedRef.current = false;
|
|
323
327
|
fetchRef.current = null;
|
|
324
328
|
unsubscribe();
|
|
325
329
|
};
|
|
326
|
-
}, [queryKey]);
|
|
330
|
+
}, [queryKey, enabled]);
|
|
327
331
|
useEffect(() => {
|
|
328
332
|
if (queryTags.length === 0) return;
|
|
329
333
|
return onRevalidate((invalidatedTags) => {
|
|
@@ -338,23 +342,56 @@ function useQueryMode(api, trackedCall, options) {
|
|
|
338
342
|
|
|
339
343
|
// src/react/useSelectorMode.ts
|
|
340
344
|
import { useRef as useRef2, useReducer as useReducer2 } from "react";
|
|
341
|
-
function
|
|
345
|
+
function resolvePath2(path, pathParams) {
|
|
346
|
+
if (!pathParams) return path;
|
|
347
|
+
return path.map((segment) => {
|
|
348
|
+
if (segment.startsWith(":")) {
|
|
349
|
+
const paramName = segment.slice(1);
|
|
350
|
+
const value = pathParams[paramName];
|
|
351
|
+
if (value === void 0) {
|
|
352
|
+
throw new Error(`Missing path parameter: ${paramName}`);
|
|
353
|
+
}
|
|
354
|
+
return String(value);
|
|
355
|
+
}
|
|
356
|
+
return segment;
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
function hasPathParams(path) {
|
|
360
|
+
return path.some((segment) => segment.startsWith(":"));
|
|
361
|
+
}
|
|
362
|
+
function useSelectorMode(config) {
|
|
363
|
+
const { method, api, path, methodName, autoRevalidateTags } = config;
|
|
342
364
|
const [state, dispatch] = useReducer2(hookReducer, initialState);
|
|
343
365
|
const methodRef = useRef2(method);
|
|
366
|
+
const apiRef = useRef2(api);
|
|
344
367
|
const triggerRef = useRef2(null);
|
|
345
368
|
const pathRef = useRef2(path);
|
|
369
|
+
const methodNameRef = useRef2(methodName);
|
|
346
370
|
const autoRevalidateRef = useRef2(autoRevalidateTags);
|
|
347
371
|
methodRef.current = method;
|
|
372
|
+
apiRef.current = api;
|
|
348
373
|
pathRef.current = path;
|
|
374
|
+
methodNameRef.current = methodName;
|
|
349
375
|
autoRevalidateRef.current = autoRevalidateTags;
|
|
350
376
|
if (!triggerRef.current) {
|
|
351
377
|
triggerRef.current = (async (...args) => {
|
|
352
378
|
dispatch({ type: "FETCH_START" });
|
|
353
|
-
const
|
|
379
|
+
const options = args[0];
|
|
380
|
+
const resolvedPath = resolvePath2(pathRef.current, options?.pathParams);
|
|
381
|
+
let res;
|
|
382
|
+
if (hasPathParams(pathRef.current)) {
|
|
383
|
+
let current = apiRef.current;
|
|
384
|
+
for (const segment of resolvedPath) {
|
|
385
|
+
current = current[segment];
|
|
386
|
+
}
|
|
387
|
+
const resolvedMethod = current[methodNameRef.current];
|
|
388
|
+
res = await resolvedMethod(...args);
|
|
389
|
+
} else {
|
|
390
|
+
res = await methodRef.current(...args);
|
|
391
|
+
}
|
|
354
392
|
if (res.ok) {
|
|
355
393
|
dispatch({ type: "FETCH_SUCCESS", data: res.data });
|
|
356
|
-
const
|
|
357
|
-
const tagsToInvalidate = options?.revalidateTags ?? (autoRevalidateRef.current ? generateTags(pathRef.current) : []);
|
|
394
|
+
const tagsToInvalidate = options?.revalidateTags ?? (autoRevalidateRef.current ? generateTags(resolvedPath) : []);
|
|
358
395
|
if (tagsToInvalidate.length > 0) {
|
|
359
396
|
invalidateTags(tagsToInvalidate);
|
|
360
397
|
}
|
|
@@ -415,26 +452,30 @@ function createEnlaceHook(baseUrl, defaultOptions = {}, hookOptions = {}) {
|
|
|
415
452
|
autoRevalidateTags,
|
|
416
453
|
...nextOptions
|
|
417
454
|
});
|
|
418
|
-
function useEnlaceHook(selectorOrQuery) {
|
|
455
|
+
function useEnlaceHook(selectorOrQuery, queryOptions) {
|
|
419
456
|
let trackedCall = null;
|
|
420
457
|
let selectorPath = null;
|
|
458
|
+
let selectorMethod = null;
|
|
421
459
|
const trackingProxy = createTrackingProxy((result2) => {
|
|
422
460
|
trackedCall = result2.trackedCall;
|
|
423
461
|
selectorPath = result2.selectorPath;
|
|
462
|
+
selectorMethod = result2.selectorMethod;
|
|
424
463
|
});
|
|
425
464
|
const result = selectorOrQuery(trackingProxy);
|
|
426
465
|
if (typeof result === "function") {
|
|
427
466
|
const actualResult = selectorOrQuery(api);
|
|
428
|
-
return useSelectorMode(
|
|
429
|
-
actualResult,
|
|
430
|
-
|
|
467
|
+
return useSelectorMode({
|
|
468
|
+
method: actualResult,
|
|
469
|
+
api,
|
|
470
|
+
path: selectorPath ?? [],
|
|
471
|
+
methodName: selectorMethod ?? "",
|
|
431
472
|
autoRevalidateTags
|
|
432
|
-
);
|
|
473
|
+
});
|
|
433
474
|
}
|
|
434
475
|
return useQueryMode(
|
|
435
476
|
api,
|
|
436
477
|
trackedCall,
|
|
437
|
-
{ autoGenerateTags, staleTime }
|
|
478
|
+
{ autoGenerateTags, staleTime, enabled: queryOptions?.enabled ?? true }
|
|
438
479
|
);
|
|
439
480
|
}
|
|
440
481
|
return useEnlaceHook;
|
package/dist/next/index.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { EnlaceCallbackPayload, EnlaceErrorCallbackPayload, WildcardClient, EnlaceClient, EnlaceResponse, EnlaceCallbacks, EnlaceOptions } from 'enlace-core';
|
|
2
2
|
export * from 'enlace-core';
|
|
3
|
-
export { EnlaceOptions } from 'enlace-core';
|
|
3
|
+
export { EnlaceCallbacks, EnlaceOptions } from 'enlace-core';
|
|
4
4
|
|
|
5
5
|
/** Per-request options for React hooks */
|
|
6
6
|
type ReactRequestOptionsBase = {
|
|
@@ -12,8 +12,55 @@ type ReactRequestOptionsBase = {
|
|
|
12
12
|
tags?: string[];
|
|
13
13
|
/** Tags to invalidate after mutation (triggers refetch in matching queries) */
|
|
14
14
|
revalidateTags?: string[];
|
|
15
|
+
/**
|
|
16
|
+
* Path parameters for dynamic URL segments.
|
|
17
|
+
* Used to replace :paramName placeholders in the URL path.
|
|
18
|
+
* @example
|
|
19
|
+
* // With path api.products[':id'].delete
|
|
20
|
+
* trigger({ pathParams: { id: '123' } }) // → DELETE /products/123
|
|
21
|
+
*/
|
|
22
|
+
pathParams?: Record<string, string | number>;
|
|
15
23
|
};
|
|
16
|
-
|
|
24
|
+
/** Options for query mode hooks */
|
|
25
|
+
type UseEnlaceQueryOptions = {
|
|
26
|
+
/**
|
|
27
|
+
* Whether the query should execute.
|
|
28
|
+
* Set to false to skip fetching (useful when ID is "new" or undefined).
|
|
29
|
+
* @default true
|
|
30
|
+
*/
|
|
31
|
+
enabled?: boolean;
|
|
32
|
+
};
|
|
33
|
+
type ApiClient<TSchema, TOptions = ReactRequestOptionsBase> = unknown extends TSchema ? WildcardClient<TOptions> : EnlaceClient<TSchema, TOptions>;
|
|
34
|
+
type QueryFn<TSchema, TData, TError, TOptions = ReactRequestOptionsBase> = (api: ApiClient<TSchema, TOptions>) => Promise<EnlaceResponse<TData, TError>>;
|
|
35
|
+
type SelectorFn<TSchema, TMethod, TOptions = ReactRequestOptionsBase> = (api: ApiClient<TSchema, TOptions>) => TMethod;
|
|
36
|
+
type ExtractData<T> = T extends (...args: any[]) => Promise<EnlaceResponse<infer D, unknown>> ? D : never;
|
|
37
|
+
type ExtractError<T> = T extends (...args: any[]) => Promise<EnlaceResponse<unknown, infer E>> ? E : never;
|
|
38
|
+
/** Discriminated union for hook response state - enables type narrowing on ok check */
|
|
39
|
+
type HookResponseState<TData, TError> = {
|
|
40
|
+
ok: undefined;
|
|
41
|
+
data: undefined;
|
|
42
|
+
error: undefined;
|
|
43
|
+
} | {
|
|
44
|
+
ok: true;
|
|
45
|
+
data: TData;
|
|
46
|
+
error: undefined;
|
|
47
|
+
} | {
|
|
48
|
+
ok: false;
|
|
49
|
+
data: undefined;
|
|
50
|
+
error: TError;
|
|
51
|
+
};
|
|
52
|
+
/** Result when hook is called with query function (auto-fetch mode) */
|
|
53
|
+
type UseEnlaceQueryResult<TData, TError> = {
|
|
54
|
+
loading: boolean;
|
|
55
|
+
fetching: boolean;
|
|
56
|
+
} & HookResponseState<TData, TError>;
|
|
57
|
+
/** Result when hook is called with method selector (trigger mode) */
|
|
58
|
+
type UseEnlaceSelectorResult<TMethod> = {
|
|
59
|
+
trigger: TMethod;
|
|
60
|
+
loading: boolean;
|
|
61
|
+
fetching: boolean;
|
|
62
|
+
} & HookResponseState<ExtractData<TMethod>, ExtractError<TMethod>>;
|
|
63
|
+
/** Options for createEnlaceHook factory */
|
|
17
64
|
type EnlaceHookOptions = {
|
|
18
65
|
/**
|
|
19
66
|
* Auto-generate cache tags from URL path for GET requests.
|
|
@@ -25,6 +72,10 @@ type EnlaceHookOptions = {
|
|
|
25
72
|
autoRevalidateTags?: boolean;
|
|
26
73
|
/** Time in ms before cached data is considered stale. @default 0 (always stale) */
|
|
27
74
|
staleTime?: number;
|
|
75
|
+
/** Callback called on successful API responses */
|
|
76
|
+
onSuccess?: (payload: EnlaceCallbackPayload<unknown>) => void;
|
|
77
|
+
/** Callback called on error responses (HTTP errors or network failures) */
|
|
78
|
+
onError?: (payload: EnlaceErrorCallbackPayload<unknown>) => void;
|
|
28
79
|
};
|
|
29
80
|
|
|
30
81
|
/**
|
|
@@ -34,7 +85,7 @@ type EnlaceHookOptions = {
|
|
|
34
85
|
*/
|
|
35
86
|
type RevalidateHandler = (tags: string[], paths: string[]) => void | Promise<void>;
|
|
36
87
|
/** Next.js-specific options (third argument for createEnlace) */
|
|
37
|
-
type NextOptions = Pick<EnlaceHookOptions, "autoGenerateTags" | "autoRevalidateTags"> & {
|
|
88
|
+
type NextOptions = Pick<EnlaceHookOptions, "autoGenerateTags" | "autoRevalidateTags"> & EnlaceCallbacks & {
|
|
38
89
|
/**
|
|
39
90
|
* Handler called after successful mutations to trigger server-side revalidation.
|
|
40
91
|
* Receives auto-generated or manually specified tags and paths.
|
|
@@ -68,7 +119,15 @@ type NextRequestOptionsBase = ReactRequestOptionsBase & {
|
|
|
68
119
|
*/
|
|
69
120
|
skipRevalidator?: boolean;
|
|
70
121
|
};
|
|
122
|
+
type NextApiClient<TSchema> = ApiClient<TSchema, NextRequestOptionsBase>;
|
|
123
|
+
type NextQueryFn<TSchema, TData, TError> = QueryFn<TSchema, TData, TError, NextRequestOptionsBase>;
|
|
124
|
+
type NextSelectorFn<TSchema, TMethod> = SelectorFn<TSchema, TMethod, NextRequestOptionsBase>;
|
|
125
|
+
/** Hook type returned by Next.js createEnlaceHook */
|
|
126
|
+
type NextEnlaceHook<TSchema> = {
|
|
127
|
+
<TMethod extends (...args: any[]) => Promise<EnlaceResponse<unknown, unknown>>>(selector: NextSelectorFn<TSchema, TMethod>): UseEnlaceSelectorResult<TMethod>;
|
|
128
|
+
<TData, TError>(queryFn: NextQueryFn<TSchema, TData, TError>, options?: UseEnlaceQueryOptions): UseEnlaceQueryResult<TData, TError>;
|
|
129
|
+
};
|
|
71
130
|
|
|
72
|
-
declare function createEnlace<TSchema = unknown>(baseUrl: string, defaultOptions?: EnlaceOptions, nextOptions?: NextOptions): unknown extends TSchema ? WildcardClient<NextRequestOptionsBase> : EnlaceClient<TSchema, NextRequestOptionsBase>;
|
|
131
|
+
declare function createEnlace<TSchema = unknown>(baseUrl: string, defaultOptions?: EnlaceOptions | null, nextOptions?: NextOptions): unknown extends TSchema ? WildcardClient<NextRequestOptionsBase> : EnlaceClient<TSchema, NextRequestOptionsBase>;
|
|
73
132
|
|
|
74
|
-
export { type NextHookOptions, type NextOptions, type NextRequestOptionsBase, type RevalidateHandler, createEnlace };
|
|
133
|
+
export { type NextApiClient, type NextEnlaceHook, type NextHookOptions, type NextOptions, type NextQueryFn, type NextRequestOptionsBase, type NextSelectorFn, type RevalidateHandler, createEnlace };
|
package/dist/next/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { EnlaceCallbackPayload, EnlaceErrorCallbackPayload, WildcardClient, EnlaceClient, EnlaceResponse, EnlaceCallbacks, EnlaceOptions } from 'enlace-core';
|
|
2
2
|
export * from 'enlace-core';
|
|
3
|
-
export { EnlaceOptions } from 'enlace-core';
|
|
3
|
+
export { EnlaceCallbacks, EnlaceOptions } from 'enlace-core';
|
|
4
4
|
|
|
5
5
|
/** Per-request options for React hooks */
|
|
6
6
|
type ReactRequestOptionsBase = {
|
|
@@ -12,8 +12,55 @@ type ReactRequestOptionsBase = {
|
|
|
12
12
|
tags?: string[];
|
|
13
13
|
/** Tags to invalidate after mutation (triggers refetch in matching queries) */
|
|
14
14
|
revalidateTags?: string[];
|
|
15
|
+
/**
|
|
16
|
+
* Path parameters for dynamic URL segments.
|
|
17
|
+
* Used to replace :paramName placeholders in the URL path.
|
|
18
|
+
* @example
|
|
19
|
+
* // With path api.products[':id'].delete
|
|
20
|
+
* trigger({ pathParams: { id: '123' } }) // → DELETE /products/123
|
|
21
|
+
*/
|
|
22
|
+
pathParams?: Record<string, string | number>;
|
|
15
23
|
};
|
|
16
|
-
|
|
24
|
+
/** Options for query mode hooks */
|
|
25
|
+
type UseEnlaceQueryOptions = {
|
|
26
|
+
/**
|
|
27
|
+
* Whether the query should execute.
|
|
28
|
+
* Set to false to skip fetching (useful when ID is "new" or undefined).
|
|
29
|
+
* @default true
|
|
30
|
+
*/
|
|
31
|
+
enabled?: boolean;
|
|
32
|
+
};
|
|
33
|
+
type ApiClient<TSchema, TOptions = ReactRequestOptionsBase> = unknown extends TSchema ? WildcardClient<TOptions> : EnlaceClient<TSchema, TOptions>;
|
|
34
|
+
type QueryFn<TSchema, TData, TError, TOptions = ReactRequestOptionsBase> = (api: ApiClient<TSchema, TOptions>) => Promise<EnlaceResponse<TData, TError>>;
|
|
35
|
+
type SelectorFn<TSchema, TMethod, TOptions = ReactRequestOptionsBase> = (api: ApiClient<TSchema, TOptions>) => TMethod;
|
|
36
|
+
type ExtractData<T> = T extends (...args: any[]) => Promise<EnlaceResponse<infer D, unknown>> ? D : never;
|
|
37
|
+
type ExtractError<T> = T extends (...args: any[]) => Promise<EnlaceResponse<unknown, infer E>> ? E : never;
|
|
38
|
+
/** Discriminated union for hook response state - enables type narrowing on ok check */
|
|
39
|
+
type HookResponseState<TData, TError> = {
|
|
40
|
+
ok: undefined;
|
|
41
|
+
data: undefined;
|
|
42
|
+
error: undefined;
|
|
43
|
+
} | {
|
|
44
|
+
ok: true;
|
|
45
|
+
data: TData;
|
|
46
|
+
error: undefined;
|
|
47
|
+
} | {
|
|
48
|
+
ok: false;
|
|
49
|
+
data: undefined;
|
|
50
|
+
error: TError;
|
|
51
|
+
};
|
|
52
|
+
/** Result when hook is called with query function (auto-fetch mode) */
|
|
53
|
+
type UseEnlaceQueryResult<TData, TError> = {
|
|
54
|
+
loading: boolean;
|
|
55
|
+
fetching: boolean;
|
|
56
|
+
} & HookResponseState<TData, TError>;
|
|
57
|
+
/** Result when hook is called with method selector (trigger mode) */
|
|
58
|
+
type UseEnlaceSelectorResult<TMethod> = {
|
|
59
|
+
trigger: TMethod;
|
|
60
|
+
loading: boolean;
|
|
61
|
+
fetching: boolean;
|
|
62
|
+
} & HookResponseState<ExtractData<TMethod>, ExtractError<TMethod>>;
|
|
63
|
+
/** Options for createEnlaceHook factory */
|
|
17
64
|
type EnlaceHookOptions = {
|
|
18
65
|
/**
|
|
19
66
|
* Auto-generate cache tags from URL path for GET requests.
|
|
@@ -25,6 +72,10 @@ type EnlaceHookOptions = {
|
|
|
25
72
|
autoRevalidateTags?: boolean;
|
|
26
73
|
/** Time in ms before cached data is considered stale. @default 0 (always stale) */
|
|
27
74
|
staleTime?: number;
|
|
75
|
+
/** Callback called on successful API responses */
|
|
76
|
+
onSuccess?: (payload: EnlaceCallbackPayload<unknown>) => void;
|
|
77
|
+
/** Callback called on error responses (HTTP errors or network failures) */
|
|
78
|
+
onError?: (payload: EnlaceErrorCallbackPayload<unknown>) => void;
|
|
28
79
|
};
|
|
29
80
|
|
|
30
81
|
/**
|
|
@@ -34,7 +85,7 @@ type EnlaceHookOptions = {
|
|
|
34
85
|
*/
|
|
35
86
|
type RevalidateHandler = (tags: string[], paths: string[]) => void | Promise<void>;
|
|
36
87
|
/** Next.js-specific options (third argument for createEnlace) */
|
|
37
|
-
type NextOptions = Pick<EnlaceHookOptions, "autoGenerateTags" | "autoRevalidateTags"> & {
|
|
88
|
+
type NextOptions = Pick<EnlaceHookOptions, "autoGenerateTags" | "autoRevalidateTags"> & EnlaceCallbacks & {
|
|
38
89
|
/**
|
|
39
90
|
* Handler called after successful mutations to trigger server-side revalidation.
|
|
40
91
|
* Receives auto-generated or manually specified tags and paths.
|
|
@@ -68,7 +119,15 @@ type NextRequestOptionsBase = ReactRequestOptionsBase & {
|
|
|
68
119
|
*/
|
|
69
120
|
skipRevalidator?: boolean;
|
|
70
121
|
};
|
|
122
|
+
type NextApiClient<TSchema> = ApiClient<TSchema, NextRequestOptionsBase>;
|
|
123
|
+
type NextQueryFn<TSchema, TData, TError> = QueryFn<TSchema, TData, TError, NextRequestOptionsBase>;
|
|
124
|
+
type NextSelectorFn<TSchema, TMethod> = SelectorFn<TSchema, TMethod, NextRequestOptionsBase>;
|
|
125
|
+
/** Hook type returned by Next.js createEnlaceHook */
|
|
126
|
+
type NextEnlaceHook<TSchema> = {
|
|
127
|
+
<TMethod extends (...args: any[]) => Promise<EnlaceResponse<unknown, unknown>>>(selector: NextSelectorFn<TSchema, TMethod>): UseEnlaceSelectorResult<TMethod>;
|
|
128
|
+
<TData, TError>(queryFn: NextQueryFn<TSchema, TData, TError>, options?: UseEnlaceQueryOptions): UseEnlaceQueryResult<TData, TError>;
|
|
129
|
+
};
|
|
71
130
|
|
|
72
|
-
declare function createEnlace<TSchema = unknown>(baseUrl: string, defaultOptions?: EnlaceOptions, nextOptions?: NextOptions): unknown extends TSchema ? WildcardClient<NextRequestOptionsBase> : EnlaceClient<TSchema, NextRequestOptionsBase>;
|
|
131
|
+
declare function createEnlace<TSchema = unknown>(baseUrl: string, defaultOptions?: EnlaceOptions | null, nextOptions?: NextOptions): unknown extends TSchema ? WildcardClient<NextRequestOptionsBase> : EnlaceClient<TSchema, NextRequestOptionsBase>;
|
|
73
132
|
|
|
74
|
-
export { type NextHookOptions, type NextOptions, type NextRequestOptionsBase, type RevalidateHandler, createEnlace };
|
|
133
|
+
export { type NextApiClient, type NextEnlaceHook, type NextHookOptions, type NextOptions, type NextQueryFn, type NextRequestOptionsBase, type NextSelectorFn, type RevalidateHandler, createEnlace };
|
package/dist/next/index.js
CHANGED
|
@@ -40,20 +40,22 @@ async function executeNextFetch(baseUrl, path, method, combinedOptions, requestO
|
|
|
40
40
|
autoGenerateTags = true,
|
|
41
41
|
autoRevalidateTags = true,
|
|
42
42
|
revalidator,
|
|
43
|
-
|
|
44
|
-
...
|
|
43
|
+
onSuccess,
|
|
44
|
+
...coreOptions
|
|
45
45
|
} = combinedOptions;
|
|
46
|
-
const url = (0, import_enlace_core.buildUrl)(baseUrl, path, requestOptions?.query);
|
|
47
|
-
let headers = (0, import_enlace_core.mergeHeaders)(defaultHeaders, requestOptions?.headers);
|
|
48
46
|
const isGet = method === "GET";
|
|
49
47
|
const autoTags = generateTags(path);
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
48
|
+
const nextOnSuccess = (payload) => {
|
|
49
|
+
if (!isGet && !requestOptions?.skipRevalidator) {
|
|
50
|
+
const revalidateTags = requestOptions?.revalidateTags ?? (autoRevalidateTags ? autoTags : []);
|
|
51
|
+
const revalidatePaths = requestOptions?.revalidatePaths ?? [];
|
|
52
|
+
if (revalidateTags.length || revalidatePaths.length) {
|
|
53
|
+
revalidator?.(revalidateTags, revalidatePaths);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
onSuccess?.(payload);
|
|
53
57
|
};
|
|
54
|
-
|
|
55
|
-
fetchOptions.cache = requestOptions.cache;
|
|
56
|
-
}
|
|
58
|
+
const nextRequestOptions = { ...requestOptions };
|
|
57
59
|
if (isGet) {
|
|
58
60
|
const tags = requestOptions?.tags ?? (autoGenerateTags ? autoTags : void 0);
|
|
59
61
|
const nextFetchOptions = {};
|
|
@@ -63,49 +65,25 @@ async function executeNextFetch(baseUrl, path, method, combinedOptions, requestO
|
|
|
63
65
|
if (requestOptions?.revalidate !== void 0) {
|
|
64
66
|
nextFetchOptions.revalidate = requestOptions.revalidate;
|
|
65
67
|
}
|
|
66
|
-
|
|
67
|
-
}
|
|
68
|
-
if (headers) {
|
|
69
|
-
fetchOptions.headers = headers;
|
|
68
|
+
nextRequestOptions.next = nextFetchOptions;
|
|
70
69
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
} else {
|
|
79
|
-
fetchOptions.body = requestOptions.body;
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
const response = await fetch(url, fetchOptions);
|
|
83
|
-
const contentType = response.headers.get("content-type");
|
|
84
|
-
const isJson = contentType?.includes("application/json");
|
|
85
|
-
if (response.ok) {
|
|
86
|
-
if (!isGet && !requestOptions?.skipRevalidator) {
|
|
87
|
-
const revalidateTags = requestOptions?.revalidateTags ?? (autoRevalidateTags ? autoTags : []);
|
|
88
|
-
const revalidatePaths = requestOptions?.revalidatePaths ?? [];
|
|
89
|
-
if (revalidateTags.length || revalidatePaths.length) {
|
|
90
|
-
revalidator?.(revalidateTags, revalidatePaths);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
return {
|
|
94
|
-
ok: true,
|
|
95
|
-
status: response.status,
|
|
96
|
-
data: isJson ? await response.json() : response
|
|
97
|
-
};
|
|
98
|
-
}
|
|
99
|
-
return {
|
|
100
|
-
ok: false,
|
|
101
|
-
status: response.status,
|
|
102
|
-
error: isJson ? await response.json() : response
|
|
103
|
-
};
|
|
70
|
+
return (0, import_enlace_core.executeFetch)(
|
|
71
|
+
baseUrl,
|
|
72
|
+
path,
|
|
73
|
+
method,
|
|
74
|
+
{ ...coreOptions, onSuccess: nextOnSuccess },
|
|
75
|
+
nextRequestOptions
|
|
76
|
+
);
|
|
104
77
|
}
|
|
105
78
|
|
|
106
79
|
// src/next/index.ts
|
|
107
80
|
__reExport(next_exports, require("enlace-core"), module.exports);
|
|
108
81
|
function createEnlace(baseUrl, defaultOptions = {}, nextOptions = {}) {
|
|
109
82
|
const combinedOptions = { ...defaultOptions, ...nextOptions };
|
|
110
|
-
return (0, import_enlace_core2.createProxyHandler)(
|
|
83
|
+
return (0, import_enlace_core2.createProxyHandler)(
|
|
84
|
+
baseUrl,
|
|
85
|
+
combinedOptions,
|
|
86
|
+
[],
|
|
87
|
+
executeNextFetch
|
|
88
|
+
);
|
|
111
89
|
}
|