@emeryld/rrroutes-client 1.2.1 → 1.2.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/README.md +21 -0
- package/dist/index.cjs +191 -21
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +191 -21
- package/dist/index.mjs.map +1 -1
- package/dist/routesV3.client.types.d.ts +36 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -33,6 +33,27 @@ function Users() {
|
|
|
33
33
|
}
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
+
`createRouteClient` passes your `QueryClient` instance directly into every `useQuery`, `useInfiniteQuery`, and `useMutation` call. That means you can skip wrapping your app with `QueryClientProvider` if you prefer manual wiring (though it's still supported if you need context for devtools or hydration).
|
|
37
|
+
|
|
38
|
+
### Debug logging
|
|
39
|
+
|
|
40
|
+
You can opt into verbose logging by passing `debug` when constructing the client:
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
const routeClient = createRouteClient({
|
|
44
|
+
baseUrl: '/api',
|
|
45
|
+
queryClient: new QueryClient(),
|
|
46
|
+
debug: process.env.NODE_ENV !== 'production',
|
|
47
|
+
// or customize the logger:
|
|
48
|
+
// debug: {
|
|
49
|
+
// enabled: true,
|
|
50
|
+
// logger: (event) => console.info('[rrroutes-client]', event),
|
|
51
|
+
// },
|
|
52
|
+
});
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Events cover fetch lifecycles (`start`/`success`/`error`), cache updates, and invalidations so you can trace how RRRoutes interacts with React Query.
|
|
56
|
+
|
|
36
57
|
## Scripts
|
|
37
58
|
|
|
38
59
|
```sh
|
package/dist/index.cjs
CHANGED
|
@@ -27,11 +27,6 @@ __export(index_exports, {
|
|
|
27
27
|
module.exports = __toCommonJS(index_exports);
|
|
28
28
|
|
|
29
29
|
// src/routesV3.client.fetch.ts
|
|
30
|
-
var logger = {
|
|
31
|
-
debug: (...args) => {
|
|
32
|
-
console.log("[routesV3.client.fetch]", ...args);
|
|
33
|
-
}
|
|
34
|
-
};
|
|
35
30
|
var defaultFetcher = async (req) => {
|
|
36
31
|
const headers = { ...req.headers ?? {} };
|
|
37
32
|
const isFormData = typeof FormData !== "undefined" && req.body instanceof FormData;
|
|
@@ -39,7 +34,6 @@ var defaultFetcher = async (req) => {
|
|
|
39
34
|
headers["Content-Type"] || (headers["Content-Type"] = "application/json");
|
|
40
35
|
headers["Accept"] || (headers["Accept"] = "application/json");
|
|
41
36
|
}
|
|
42
|
-
logger.debug("fetch", req.method, req.url, isFormData ? "FormData" : req.body);
|
|
43
37
|
const res = await fetch(req.url, {
|
|
44
38
|
method: req.method,
|
|
45
39
|
headers,
|
|
@@ -95,6 +89,35 @@ function stripKey(obj, key) {
|
|
|
95
89
|
return rest;
|
|
96
90
|
}
|
|
97
91
|
var defaultGetNextCursor = (p) => p && typeof p === "object" && "nextCursor" in p ? p.nextCursor : void 0;
|
|
92
|
+
var noopDebugLogger = () => {
|
|
93
|
+
};
|
|
94
|
+
var defaultDebugLogger = (event) => {
|
|
95
|
+
if (typeof console === "undefined") return;
|
|
96
|
+
const fn = console.debug ?? console.log;
|
|
97
|
+
fn?.call(console, "[rrroutes-client]", event);
|
|
98
|
+
};
|
|
99
|
+
function createDebugEmitter(option) {
|
|
100
|
+
const disabled = { emit: noopDebugLogger, mode: "minimal" };
|
|
101
|
+
if (!option) {
|
|
102
|
+
return disabled;
|
|
103
|
+
}
|
|
104
|
+
if (option === true || option === "minimal") {
|
|
105
|
+
return { emit: defaultDebugLogger, mode: "minimal" };
|
|
106
|
+
}
|
|
107
|
+
if (option === "complete") {
|
|
108
|
+
return { emit: defaultDebugLogger, mode: "complete" };
|
|
109
|
+
}
|
|
110
|
+
if (typeof option === "function") {
|
|
111
|
+
return { emit: option, mode: "minimal" };
|
|
112
|
+
}
|
|
113
|
+
if (option.enabled === false) {
|
|
114
|
+
return { emit: noopDebugLogger, mode: option.mode ?? "minimal" };
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
emit: option.logger ?? defaultDebugLogger,
|
|
118
|
+
mode: option.mode ?? "minimal"
|
|
119
|
+
};
|
|
120
|
+
}
|
|
98
121
|
function extractArgs(args) {
|
|
99
122
|
return args[0];
|
|
100
123
|
}
|
|
@@ -103,7 +126,7 @@ function buildUrl(leaf, baseUrl, params, query) {
|
|
|
103
126
|
const normalizedQuery = zParse(query, leaf.cfg.querySchema);
|
|
104
127
|
const path = (0, import_rrroutes_contract.compilePath)(leaf.path, normalizedParams ?? {});
|
|
105
128
|
const url = `${baseUrl ?? ""}${path}${toSearchString(normalizedQuery)}`;
|
|
106
|
-
return { url, normalizedQuery };
|
|
129
|
+
return { url, normalizedQuery, normalizedParams };
|
|
107
130
|
}
|
|
108
131
|
function createRouteClient(opts) {
|
|
109
132
|
const queryClient = opts.queryClient;
|
|
@@ -111,12 +134,23 @@ function createRouteClient(opts) {
|
|
|
111
134
|
const baseUrl = opts.baseUrl;
|
|
112
135
|
const cursorParam = opts.cursorParam ?? "cursor";
|
|
113
136
|
const getNextCursor = opts.getNextCursor ?? defaultGetNextCursor;
|
|
137
|
+
const { emit: emitDebug, mode: debugMode } = createDebugEmitter(opts.debug);
|
|
138
|
+
const isVerboseDebug = debugMode === "complete";
|
|
139
|
+
const decorateDebugEvent = (event, details) => {
|
|
140
|
+
if (!isVerboseDebug || !details) return event;
|
|
141
|
+
return { ...event, ...details };
|
|
142
|
+
};
|
|
114
143
|
async function invalidate(prefix, exact = false) {
|
|
115
|
-
|
|
144
|
+
const queryKey = prefix;
|
|
145
|
+
await queryClient.invalidateQueries({ queryKey, exact });
|
|
146
|
+
emitDebug({ type: "invalidate", key: queryKey, exact });
|
|
116
147
|
}
|
|
117
148
|
function buildInternal(leaf, rqOpts) {
|
|
118
149
|
const isGet = leaf.method === "get";
|
|
119
150
|
const isFeed = !!leaf.cfg.feed;
|
|
151
|
+
const method = toUpper(leaf.method);
|
|
152
|
+
const leafLabel = `${leaf.method.toUpperCase()} ${String(leaf.path)}`;
|
|
153
|
+
emitDebug({ type: "build", leaf: leafLabel });
|
|
120
154
|
const key = (...tuple) => {
|
|
121
155
|
const a = extractArgs(tuple);
|
|
122
156
|
const params = a?.params;
|
|
@@ -124,7 +158,11 @@ function createRouteClient(opts) {
|
|
|
124
158
|
const qForKey = isGet && isFeed ? stripKey(query, cursorParam) : query;
|
|
125
159
|
return (0, import_rrroutes_contract.buildCacheKey)({ leaf, params, query: qForKey });
|
|
126
160
|
};
|
|
127
|
-
const invalidateExact = (...tuple) =>
|
|
161
|
+
const invalidateExact = async (...tuple) => {
|
|
162
|
+
const queryKey = key(...tuple);
|
|
163
|
+
await queryClient.invalidateQueries({ queryKey, exact: true });
|
|
164
|
+
emitDebug({ type: "invalidate", key: queryKey, exact: true });
|
|
165
|
+
};
|
|
128
166
|
const setData = (...args) => {
|
|
129
167
|
const [updater, ...rest] = args;
|
|
130
168
|
const k = key(...rest);
|
|
@@ -139,13 +177,15 @@ function createRouteClient(opts) {
|
|
|
139
177
|
(prev) => typeof updater === "function" ? updater(prev) : updater
|
|
140
178
|
);
|
|
141
179
|
}
|
|
180
|
+
emitDebug({ type: "setData", key: k });
|
|
142
181
|
};
|
|
143
182
|
if (isGet && isFeed) {
|
|
144
183
|
const useEndpoint2 = (...tuple) => {
|
|
184
|
+
emitDebug({ type: "useInfiniteGet", leaf: leafLabel });
|
|
145
185
|
const a = extractArgs(tuple);
|
|
146
186
|
const params = a?.params;
|
|
147
187
|
const query = a?.query;
|
|
148
|
-
const { normalizedQuery } = buildUrl(leaf, baseUrl, params, query);
|
|
188
|
+
const { normalizedQuery, normalizedParams } = buildUrl(leaf, baseUrl, params, query);
|
|
149
189
|
return (0, import_react_query.useInfiniteQuery)({
|
|
150
190
|
...rqOpts,
|
|
151
191
|
queryKey: key(...tuple),
|
|
@@ -158,11 +198,51 @@ function createRouteClient(opts) {
|
|
|
158
198
|
...pageParam ? { [cursorParam]: pageParam } : {}
|
|
159
199
|
};
|
|
160
200
|
const { url } = buildUrl(leaf, baseUrl, params, pageQuery);
|
|
161
|
-
const
|
|
162
|
-
|
|
201
|
+
const startedAt = Date.now();
|
|
202
|
+
const detail = isVerboseDebug ? { params: normalizedParams, query: pageQuery } : void 0;
|
|
203
|
+
emitDebug(
|
|
204
|
+
decorateDebugEvent(
|
|
205
|
+
{ type: "fetch", stage: "start", method, url, leaf: leafLabel },
|
|
206
|
+
detail
|
|
207
|
+
)
|
|
208
|
+
);
|
|
209
|
+
try {
|
|
210
|
+
const out = await fetcher({ url, method });
|
|
211
|
+
const parsed = zParse(out, leaf.cfg.outputSchema);
|
|
212
|
+
emitDebug(
|
|
213
|
+
decorateDebugEvent(
|
|
214
|
+
{
|
|
215
|
+
type: "fetch",
|
|
216
|
+
stage: "success",
|
|
217
|
+
method,
|
|
218
|
+
url,
|
|
219
|
+
leaf: leafLabel,
|
|
220
|
+
durationMs: Date.now() - startedAt
|
|
221
|
+
},
|
|
222
|
+
isVerboseDebug ? { params: normalizedParams, query: pageQuery, output: parsed } : void 0
|
|
223
|
+
)
|
|
224
|
+
);
|
|
225
|
+
return parsed;
|
|
226
|
+
} catch (error) {
|
|
227
|
+
emitDebug(
|
|
228
|
+
decorateDebugEvent(
|
|
229
|
+
{
|
|
230
|
+
type: "fetch",
|
|
231
|
+
stage: "error",
|
|
232
|
+
method,
|
|
233
|
+
url,
|
|
234
|
+
leaf: leafLabel,
|
|
235
|
+
durationMs: Date.now() - startedAt,
|
|
236
|
+
error
|
|
237
|
+
},
|
|
238
|
+
detail
|
|
239
|
+
)
|
|
240
|
+
);
|
|
241
|
+
throw error;
|
|
242
|
+
}
|
|
163
243
|
}
|
|
164
244
|
// NOTE: TData is InfiniteData<T>, so we don't need a select here.
|
|
165
|
-
});
|
|
245
|
+
}, queryClient);
|
|
166
246
|
};
|
|
167
247
|
return {
|
|
168
248
|
key,
|
|
@@ -173,19 +253,60 @@ function createRouteClient(opts) {
|
|
|
173
253
|
}
|
|
174
254
|
if (isGet) {
|
|
175
255
|
const useEndpoint2 = (...tuple) => {
|
|
256
|
+
emitDebug({ type: "useGet", leaf: leafLabel });
|
|
176
257
|
const a = extractArgs(tuple);
|
|
177
258
|
const params = a?.params;
|
|
178
259
|
const query = a?.query;
|
|
179
|
-
const { url } = buildUrl(leaf, baseUrl, params, query);
|
|
260
|
+
const { url, normalizedQuery, normalizedParams } = buildUrl(leaf, baseUrl, params, query);
|
|
180
261
|
return (0, import_react_query.useQuery)({
|
|
181
262
|
...rqOpts,
|
|
182
263
|
queryKey: key(...tuple),
|
|
183
264
|
placeholderData: import_react_query.keepPreviousData,
|
|
184
265
|
queryFn: async () => {
|
|
185
|
-
const
|
|
186
|
-
|
|
266
|
+
const startedAt = Date.now();
|
|
267
|
+
const detail = isVerboseDebug ? { params: normalizedParams, query: normalizedQuery } : void 0;
|
|
268
|
+
emitDebug(
|
|
269
|
+
decorateDebugEvent(
|
|
270
|
+
{ type: "fetch", stage: "start", method, url, leaf: leafLabel },
|
|
271
|
+
detail
|
|
272
|
+
)
|
|
273
|
+
);
|
|
274
|
+
try {
|
|
275
|
+
const out = await fetcher({ url, method });
|
|
276
|
+
const parsed = zParse(out, leaf.cfg.outputSchema);
|
|
277
|
+
emitDebug(
|
|
278
|
+
decorateDebugEvent(
|
|
279
|
+
{
|
|
280
|
+
type: "fetch",
|
|
281
|
+
stage: "success",
|
|
282
|
+
method,
|
|
283
|
+
url,
|
|
284
|
+
leaf: leafLabel,
|
|
285
|
+
durationMs: Date.now() - startedAt
|
|
286
|
+
},
|
|
287
|
+
isVerboseDebug ? { params: normalizedParams, query: normalizedQuery, output: parsed } : void 0
|
|
288
|
+
)
|
|
289
|
+
);
|
|
290
|
+
return parsed;
|
|
291
|
+
} catch (error) {
|
|
292
|
+
emitDebug(
|
|
293
|
+
decorateDebugEvent(
|
|
294
|
+
{
|
|
295
|
+
type: "fetch",
|
|
296
|
+
stage: "error",
|
|
297
|
+
method,
|
|
298
|
+
url,
|
|
299
|
+
leaf: leafLabel,
|
|
300
|
+
durationMs: Date.now() - startedAt,
|
|
301
|
+
error
|
|
302
|
+
},
|
|
303
|
+
detail
|
|
304
|
+
)
|
|
305
|
+
);
|
|
306
|
+
throw error;
|
|
307
|
+
}
|
|
187
308
|
}
|
|
188
|
-
});
|
|
309
|
+
}, queryClient);
|
|
189
310
|
};
|
|
190
311
|
return {
|
|
191
312
|
key,
|
|
@@ -195,10 +316,11 @@ function createRouteClient(opts) {
|
|
|
195
316
|
};
|
|
196
317
|
}
|
|
197
318
|
const useEndpoint = (...tuple) => {
|
|
319
|
+
emitDebug({ type: "useMutation", leaf: leafLabel });
|
|
198
320
|
const a = extractArgs(tuple);
|
|
199
321
|
const params = a?.params;
|
|
200
322
|
const query = a?.query;
|
|
201
|
-
const { url } = buildUrl(leaf, baseUrl, params, query);
|
|
323
|
+
const { url, normalizedQuery, normalizedParams } = buildUrl(leaf, baseUrl, params, query);
|
|
202
324
|
return (0, import_react_query.useMutation)({
|
|
203
325
|
...rqOpts,
|
|
204
326
|
mutationKey: key(...tuple),
|
|
@@ -206,10 +328,58 @@ function createRouteClient(opts) {
|
|
|
206
328
|
const normalizedBody = zParse(body, leaf.cfg.bodySchema);
|
|
207
329
|
const isMultipart = Array.isArray(leaf.cfg.bodyFiles) && leaf.cfg.bodyFiles.length > 0;
|
|
208
330
|
const payload = isMultipart ? toFormData(normalizedBody) : normalizedBody;
|
|
209
|
-
const
|
|
210
|
-
|
|
331
|
+
const startedAt = Date.now();
|
|
332
|
+
const detail = isVerboseDebug ? { params: normalizedParams, query: normalizedQuery } : void 0;
|
|
333
|
+
emitDebug(
|
|
334
|
+
decorateDebugEvent(
|
|
335
|
+
{
|
|
336
|
+
type: "fetch",
|
|
337
|
+
stage: "start",
|
|
338
|
+
method,
|
|
339
|
+
url,
|
|
340
|
+
leaf: leafLabel,
|
|
341
|
+
body: payload
|
|
342
|
+
},
|
|
343
|
+
detail
|
|
344
|
+
)
|
|
345
|
+
);
|
|
346
|
+
try {
|
|
347
|
+
const out = await fetcher({ url, method, body: payload });
|
|
348
|
+
const parsed = zParse(out, leaf.cfg.outputSchema);
|
|
349
|
+
emitDebug(
|
|
350
|
+
decorateDebugEvent(
|
|
351
|
+
{
|
|
352
|
+
type: "fetch",
|
|
353
|
+
stage: "success",
|
|
354
|
+
method,
|
|
355
|
+
url,
|
|
356
|
+
leaf: leafLabel,
|
|
357
|
+
durationMs: Date.now() - startedAt
|
|
358
|
+
},
|
|
359
|
+
isVerboseDebug ? { params: normalizedParams, query: normalizedQuery, output: parsed } : void 0
|
|
360
|
+
)
|
|
361
|
+
);
|
|
362
|
+
return parsed;
|
|
363
|
+
} catch (error) {
|
|
364
|
+
emitDebug(
|
|
365
|
+
decorateDebugEvent(
|
|
366
|
+
{
|
|
367
|
+
type: "fetch",
|
|
368
|
+
stage: "error",
|
|
369
|
+
method,
|
|
370
|
+
url,
|
|
371
|
+
leaf: leafLabel,
|
|
372
|
+
durationMs: Date.now() - startedAt,
|
|
373
|
+
body: payload,
|
|
374
|
+
error
|
|
375
|
+
},
|
|
376
|
+
detail
|
|
377
|
+
)
|
|
378
|
+
);
|
|
379
|
+
throw error;
|
|
380
|
+
}
|
|
211
381
|
}
|
|
212
|
-
});
|
|
382
|
+
}, queryClient);
|
|
213
383
|
};
|
|
214
384
|
return {
|
|
215
385
|
key,
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/routesV3.client.fetch.ts","../src/routesV3.client.index.ts"],"sourcesContent":["/**\n * This package exports React Query hooks, so we mark it as client-only to keep Next.js happy.\n */\n'use client';\n\nexport * from './routesV3.client.types';\nexport * from './routesV3.client.fetch';\nexport * from './routesV3.client.index';\n","// routesV3.client.fetch.ts\n\nimport { Fetcher, FetchInput } from './routesV3.client.types';\n\nconst logger = {\n debug: (...args: any[]) => {\n // Uncomment to enable debug logging\n console.log('[routesV3.client.fetch]', ...args);\n },\n};\n\n/**\n * Default fetch implementation used by the route client helper.\n * @param req Normalized request information (URL, method, body, headers).\n * @returns Parsed JSON (or text fallback) from the server response.\n */\nexport const defaultFetcher: Fetcher = async <T>(req: FetchInput): Promise<T> => {\n const headers: Record<string, string> = { ...(req.headers ?? {}) };\n const isFormData = typeof FormData !== 'undefined' && req.body instanceof FormData;\n if (!isFormData) {\n headers['Content-Type'] ||= 'application/json';\n headers['Accept'] ||= 'application/json';\n }\n\n logger.debug('fetch', req.method, req.url, isFormData ? 'FormData' : req.body);\n const res = await fetch(req.url, {\n method: req.method,\n headers,\n body: isFormData ? (req.body as any) : req.body == null ? undefined : JSON.stringify(req.body),\n });\n\n const text = await res.text();\n if (!res.ok) {\n const snippet = text.slice(0, 400);\n throw new Error(`[${res.status}] ${res.statusText} — ${snippet}`);\n }\n\n try {\n return JSON.parse(text) as T;\n } catch {\n return text as unknown as T;\n }\n};\n","// routesV3.client.ts\nimport {\n keepPreviousData,\n useInfiniteQuery,\n useMutation,\n useQuery,\n type InfiniteData,\n type QueryKey,\n} from '@tanstack/react-query';\nimport type { ZodType } from 'zod';\nimport {\n HttpMethod,\n buildCacheKey,\n compilePath,\n} from '@emeryld/rrroutes-contract';\nimport type {\n AnyLeaf,\n InferBody,\n InferOutput,\n InferParams,\n InferQuery,\n} from '@emeryld/rrroutes-contract';\nimport { defaultFetcher } from './routesV3.client.fetch';\nimport type {\n ArgsFor,\n ArgsTuple,\n BuiltForLeaf,\n BuiltInfinite,\n BuiltMutation,\n BuiltQuery,\n Cursor,\n DataShape,\n InfiniteBuildOptionsFor,\n MutationBuildOptionsFor,\n QueryBuildOptionsFor,\n RouteClient,\n RouteClientOptions,\n Updater,\n} from './routesV3.client.types';\n\n// -------------------------------------------------------------------------------------\n// Tiny helpers\n// -------------------------------------------------------------------------------------\n/**\n * Convert an HTTP method to uppercase (as expected by fetch).\n * @param m Lowercase HTTP method.\n * @returns Uppercase method string.\n */\nconst toUpper = (m: HttpMethod): Uppercase<HttpMethod> => m.toUpperCase() as Uppercase<HttpMethod>;\n\n/**\n * Parse the given value with the supplied schema (if present).\n * @param value Raw value to validate.\n * @param schema Optional Zod schema used for validation/coercion.\n * @returns The validated or original value.\n */\nfunction zParse<T>(value: unknown, schema?: ZodType): T {\n return schema ? (schema.parse(value) as T) : (value as T);\n}\n\n/**\n * Serialize a query object into a search string.\n * @param query Query params object (possibly undefined).\n * @returns Query string prefixed with `?`, or empty string.\n */\nfunction toSearchString(query: Record<string, unknown> | undefined) {\n if (!query) return '';\n const params = new URLSearchParams();\n for (const [k, v] of Object.entries(query)) {\n if (v == null) continue;\n if (Array.isArray(v)) {\n v.forEach((x) => {\n if (x == null) return;\n if (typeof x === 'object') {\n params.append(k, JSON.stringify(x));\n } else {\n params.append(k, String(x));\n }\n });\n continue;\n }\n if (typeof v === 'object') {\n params.set(k, JSON.stringify(v));\n continue;\n }\n params.set(k, String(v));\n }\n const s = params.toString();\n return s ? `?${s}` : '';\n}\n\n/**\n * Remove the given key from an object (used to drop cursors from cache keys).\n * @param obj Source object.\n * @param key Property name to omit.\n * @returns Copy of the object without the specified key.\n */\nfunction stripKey<Q extends Record<string, unknown> | undefined>(obj: Q, key: string): Q {\n if (!obj) return obj;\n const { [key]: _omit, ...rest } = obj as any;\n return rest as Q;\n}\n\n/**\n * Default cursor extractor used by infinite queries.\n * @param p Page result returned from the server.\n * @returns Next cursor string, if present.\n */\nconst defaultGetNextCursor = (p: unknown): Cursor =>\n p && typeof p === 'object' && 'nextCursor' in p ? (p as any).nextCursor : undefined;\n\n// Split the variadic tuple at runtime\n/**\n * Extract the optional argument object from a variadic tuple.\n * @param args Tuple passed to a built endpoint helper.\n * @returns The argument object if present.\n */\nfunction extractArgs<L extends AnyLeaf>(args: ArgsTuple<L>): ArgsFor<L> | undefined {\n // At runtime ArgsTuple<L> is either [] or [obj]; we just pick the first if present.\n return (args as unknown as any[])[0] as any;\n}\n\n/**\n * Normalize params and query values, then construct a request URL for the given leaf.\n * @param leaf Leaf describing the endpoint.\n * @param baseUrl Optional base URL prepended to the path.\n * @param params Route parameters supplied by the caller.\n * @param query Query parameters supplied by the caller.\n * @returns Object containing the composed URL and normalized query payload.\n */\nfunction buildUrl<L extends AnyLeaf>(\n leaf: L,\n baseUrl: string,\n params: InferParams<L> | undefined,\n query: InferQuery<L> | undefined,\n) {\n const normalizedParams = zParse<InferParams<L>>(params, leaf.cfg.paramsSchema);\n const normalizedQuery = zParse<InferQuery<L>>(query, leaf.cfg.querySchema);\n const path = compilePath<L['path']>(leaf.path, (normalizedParams ?? {}) as any);\n const url = `${baseUrl ?? ''}${path}${toSearchString(normalizedQuery as any)}`;\n return { url, normalizedQuery };\n}\n\n// -------------------------------------------------------------------------------------\n// Client factory\n// -------------------------------------------------------------------------------------\n/**\n * Construct typed React Query helpers backed by a routes-v3 registry leaf.\n * @param opts Route client configuration (query client, fetcher overrides, etc).\n * @returns Object that can build endpoint hooks/mutations from leaves.\n */\nexport function createRouteClient(opts: RouteClientOptions): RouteClient {\n const queryClient = opts.queryClient;\n const fetcher = opts.fetcher ?? defaultFetcher;\n const baseUrl = opts.baseUrl;\n const cursorParam = opts.cursorParam ?? 'cursor';\n const getNextCursor = opts.getNextCursor ?? defaultGetNextCursor;\n\n /**\n * Invalidate a set of queries sharing the given prefix.\n * @param prefix Key parts shared by matching endpoints.\n * @param exact When true, invalidate only exact key matches.\n */\n async function invalidate(prefix: string[], exact = false) {\n await queryClient.invalidateQueries({ queryKey: prefix as unknown as QueryKey, exact });\n }\n\n /**\n * Build the client surface for a single leaf (query/mutation/infinite query).\n * @param leaf Leaf describing the endpoint.\n * @param rqOpts Optional React Query configuration.\n * @returns Helper object exposing key/invalidate/setData/useEndpoint.\n */\n function buildInternal<L extends AnyLeaf>(\n leaf: L,\n rqOpts?: QueryBuildOptionsFor<L> | InfiniteBuildOptionsFor<L> | MutationBuildOptionsFor<L>,\n ): BuiltForLeaf<L> {\n const isGet = leaf.method === 'get';\n const isFeed = !!leaf.cfg.feed;\n\n // --- key/invalidate/setData shared helpers ---\n const key = (...tuple: ArgsTuple<L>): QueryKey => {\n const a = extractArgs<L>(tuple);\n const params = (a as any)?.params as InferParams<L> | undefined;\n const query = (a as any)?.query as InferQuery<L> | undefined;\n const qForKey = isGet && isFeed ? stripKey(query as any, cursorParam) : (query as any);\n return buildCacheKey({ leaf, params: params as any, query: qForKey }) as unknown as QueryKey;\n };\n\n /**\n * Invalidate the React Query cache for this exact leaf invocation.\n * @param tuple Optional params/query tuple.\n */\n const invalidateExact = (...tuple: ArgsTuple<L>) =>\n queryClient.invalidateQueries({ queryKey: key(...tuple), exact: true });\n\n /**\n * Update the cache entries for this leaf.\n * @param args Tuple whose first entry is the updater and optional params/query follow.\n */\n const setData = (...args: [Updater<DataShape<L>>, ...rest: ArgsTuple<L>]) => {\n const [updater, ...rest] = args;\n const k = key(...(rest as ArgsTuple<L>));\n if (isGet && isFeed) {\n queryClient.setQueryData<InfiniteData<InferOutput<L>> | undefined>(k, (prev) =>\n typeof updater === 'function' ? (updater as any)(prev) : (updater as any),\n );\n } else {\n queryClient.setQueryData<InferOutput<L> | undefined>(k, (prev) =>\n typeof updater === 'function' ? (updater as any)(prev) : (updater as any),\n );\n }\n };\n\n // --- Infinite GET ---\n if (isGet && isFeed) {\n const useEndpoint: BuiltInfinite<L>['useEndpoint'] = (...tuple) => {\n const a = extractArgs<L>(tuple);\n const params = (a as any)?.params as InferParams<L> | undefined;\n const query = (a as any)?.query as InferQuery<L> | undefined;\n\n // Normalize once; we’ll inject the cursor per page below.\n const { normalizedQuery } = buildUrl(leaf, baseUrl, params, query);\n return useInfiniteQuery<\n InferOutput<L>, // TQueryFnData (per page)\n unknown, // TError\n InfiniteData<InferOutput<L>>, // TData (returned by the hook)\n QueryKey,\n Cursor\n >({\n ...(rqOpts as InfiniteBuildOptionsFor<L>),\n queryKey: key(...tuple),\n initialPageParam: undefined,\n getNextPageParam: (lastPage) => getNextCursor(lastPage),\n placeholderData: keepPreviousData,\n queryFn: async ({ pageParam }) => {\n const pageQuery = {\n ...(normalizedQuery as any),\n ...(pageParam ? { [cursorParam]: pageParam } : {}),\n };\n const { url } = buildUrl(leaf, baseUrl, params, pageQuery);\n const out = await fetcher<unknown>({ url, method: toUpper(leaf.method) });\n return zParse<InferOutput<L>>(out, leaf.cfg.outputSchema);\n },\n // NOTE: TData is InfiniteData<T>, so we don't need a select here.\n });\n };\n\n return {\n key,\n invalidate: invalidateExact,\n setData: setData as any,\n useEndpoint,\n } as BuiltForLeaf<L>;\n }\n\n // --- Plain GET ---\n if (isGet) {\n const useEndpoint: BuiltQuery<L>['useEndpoint'] = (...tuple) => {\n const a = extractArgs<L>(tuple);\n const params = (a as any)?.params as InferParams<L> | undefined;\n const query = (a as any)?.query as InferQuery<L> | undefined;\n\n const { url } = buildUrl(leaf, baseUrl, params, query);\n return useQuery<InferOutput<L>, unknown, InferOutput<L>, QueryKey>({\n ...(rqOpts as QueryBuildOptionsFor<L>),\n queryKey: key(...tuple),\n placeholderData: keepPreviousData,\n queryFn: async () => {\n const out = await fetcher<unknown>({ url, method: toUpper(leaf.method) });\n return zParse<InferOutput<L>>(out, leaf.cfg.outputSchema);\n },\n });\n };\n\n return {\n key,\n invalidate: invalidateExact,\n setData: setData as any,\n useEndpoint,\n } as BuiltForLeaf<L>;\n }\n\n // --- Mutation (POST/PUT/PATCH/DELETE) ---\n const useEndpoint: BuiltMutation<L>['useEndpoint'] = (...tuple) => {\n const a = extractArgs<L>(tuple);\n const params = (a as any)?.params as InferParams<L> | undefined;\n const query = (a as any)?.query as InferQuery<L> | undefined;\n\n const { url } = buildUrl(leaf, baseUrl, params, query);\n return useMutation<InferOutput<L>, unknown, InferBody<L>, unknown>({\n ...(rqOpts as MutationBuildOptionsFor<L>),\n mutationKey: key(...tuple),\n mutationFn: async (body: InferBody<L>) => {\n const normalizedBody = zParse<InferBody<L>>(body, leaf.cfg.bodySchema);\n\n // Optional: switch to FormData if your method declares bodyFiles\n const isMultipart = Array.isArray(leaf.cfg.bodyFiles) && leaf.cfg.bodyFiles.length > 0;\n const payload = isMultipart ? toFormData(normalizedBody as any) : normalizedBody;\n\n const out = await fetcher<unknown>({ url, method: toUpper(leaf.method), body: payload });\n return zParse<InferOutput<L>>(out, leaf.cfg.outputSchema);\n },\n });\n };\n\n return {\n key,\n invalidate: invalidateExact,\n setData: setData as any,\n useEndpoint,\n } as BuiltForLeaf<L>;\n }\n\n return {\n queryClient,\n invalidate,\n build: buildInternal as RouteClient['build'],\n };\n}\n\n// -------------------------------------------------------------------------------------\n// Multipart helper\n// -------------------------------------------------------------------------------------\nfunction toFormData(body: Record<string, any>): FormData {\n const fd = new FormData();\n for (const [k, v] of Object.entries(body ?? {})) {\n if (v == null) continue;\n if (Array.isArray(v)) v.forEach((item, i) => fd.append(`${k}[${i}]`, item as any));\n else fd.append(k, v as any);\n }\n return fd;\n}\n"],"mappings":"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","names":["useEndpoint"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/routesV3.client.fetch.ts","../src/routesV3.client.index.ts"],"sourcesContent":["/**\n * This package exports React Query hooks, so we mark it as client-only to keep Next.js happy.\n */\n'use client';\n\nexport * from './routesV3.client.types';\nexport * from './routesV3.client.fetch';\nexport * from './routesV3.client.index';\n","// routesV3.client.fetch.ts\n\nimport { Fetcher, FetchInput } from './routesV3.client.types';\n\n/**\n * Default fetch implementation used by the route client helper.\n * @param req Normalized request information (URL, method, body, headers).\n * @returns Parsed JSON (or text fallback) from the server response.\n */\nexport const defaultFetcher: Fetcher = async <T>(req: FetchInput): Promise<T> => {\n const headers: Record<string, string> = { ...(req.headers ?? {}) };\n const isFormData = typeof FormData !== 'undefined' && req.body instanceof FormData;\n if (!isFormData) {\n headers['Content-Type'] ||= 'application/json';\n headers['Accept'] ||= 'application/json';\n }\n\n const res = await fetch(req.url, {\n method: req.method,\n headers,\n body: isFormData ? (req.body as any) : req.body == null ? undefined : JSON.stringify(req.body),\n });\n\n const text = await res.text();\n if (!res.ok) {\n const snippet = text.slice(0, 400);\n throw new Error(`[${res.status}] ${res.statusText} — ${snippet}`);\n }\n\n try {\n return JSON.parse(text) as T;\n } catch {\n return text as unknown as T;\n }\n};\n","// routesV3.client.ts\nimport {\n keepPreviousData,\n useInfiniteQuery,\n useMutation,\n useQuery,\n type InfiniteData,\n type QueryKey,\n} from '@tanstack/react-query';\nimport type { ZodType } from 'zod';\nimport {\n HttpMethod,\n buildCacheKey,\n compilePath,\n} from '@emeryld/rrroutes-contract';\nimport type {\n AnyLeaf,\n InferBody,\n InferOutput,\n InferParams,\n InferQuery,\n} from '@emeryld/rrroutes-contract';\nimport { defaultFetcher } from './routesV3.client.fetch';\nimport type {\n ArgsFor,\n ArgsTuple,\n BuiltForLeaf,\n BuiltInfinite,\n BuiltMutation,\n BuiltQuery,\n Cursor,\n DataShape,\n InfiniteBuildOptionsFor,\n MutationBuildOptionsFor,\n QueryBuildOptionsFor,\n RouteClient,\n RouteClientOptions,\n RouteClientDebugEvent,\n RouteClientDebugLogger,\n RouteClientDebugMode,\n RouteClientDebugOptions,\n Updater,\n} from './routesV3.client.types';\n\n// -------------------------------------------------------------------------------------\n// Tiny helpers\n// -------------------------------------------------------------------------------------\n/**\n * Convert an HTTP method to uppercase (as expected by fetch).\n * @param m Lowercase HTTP method.\n * @returns Uppercase method string.\n */\nconst toUpper = (m: HttpMethod): Uppercase<HttpMethod> => m.toUpperCase() as Uppercase<HttpMethod>;\n\n/**\n * Parse the given value with the supplied schema (if present).\n * @param value Raw value to validate.\n * @param schema Optional Zod schema used for validation/coercion.\n * @returns The validated or original value.\n */\nfunction zParse<T>(value: unknown, schema?: ZodType): T {\n return schema ? (schema.parse(value) as T) : (value as T);\n}\n\n/**\n * Serialize a query object into a search string.\n * @param query Query params object (possibly undefined).\n * @returns Query string prefixed with `?`, or empty string.\n */\nfunction toSearchString(query: Record<string, unknown> | undefined) {\n if (!query) return '';\n const params = new URLSearchParams();\n for (const [k, v] of Object.entries(query)) {\n if (v == null) continue;\n if (Array.isArray(v)) {\n v.forEach((x) => {\n if (x == null) return;\n if (typeof x === 'object') {\n params.append(k, JSON.stringify(x));\n } else {\n params.append(k, String(x));\n }\n });\n continue;\n }\n if (typeof v === 'object') {\n params.set(k, JSON.stringify(v));\n continue;\n }\n params.set(k, String(v));\n }\n const s = params.toString();\n return s ? `?${s}` : '';\n}\n\n/**\n * Remove the given key from an object (used to drop cursors from cache keys).\n * @param obj Source object.\n * @param key Property name to omit.\n * @returns Copy of the object without the specified key.\n */\nfunction stripKey<Q extends Record<string, unknown> | undefined>(obj: Q, key: string): Q {\n if (!obj) return obj;\n const { [key]: _omit, ...rest } = obj as any;\n return rest as Q;\n}\n\n/**\n * Default cursor extractor used by infinite queries.\n * @param p Page result returned from the server.\n * @returns Next cursor string, if present.\n */\nconst defaultGetNextCursor = (p: unknown): Cursor =>\n p && typeof p === 'object' && 'nextCursor' in p ? (p as any).nextCursor : undefined;\n\n// Debug logging --------------------------------------------------------------\nconst noopDebugLogger: RouteClientDebugLogger = () => {};\n\nconst defaultDebugLogger: RouteClientDebugLogger = (event: RouteClientDebugEvent) => {\n if (typeof console === 'undefined') return;\n const fn = console.debug ?? console.log;\n fn?.call(console, '[rrroutes-client]', event);\n};\n\ntype DebugEmitter = {\n emit: RouteClientDebugLogger;\n mode: RouteClientDebugMode;\n};\n\nfunction createDebugEmitter(option?: RouteClientDebugOptions): DebugEmitter {\n const disabled: DebugEmitter = { emit: noopDebugLogger, mode: 'minimal' };\n\n if (!option) {\n return disabled;\n }\n if (option === true || option === 'minimal') {\n return { emit: defaultDebugLogger, mode: 'minimal' };\n }\n if (option === 'complete') {\n return { emit: defaultDebugLogger, mode: 'complete' };\n }\n if (typeof option === 'function') {\n return { emit: option, mode: 'minimal' };\n }\n if (option.enabled === false) {\n return { emit: noopDebugLogger, mode: option.mode ?? 'minimal' };\n }\n return {\n emit: option.logger ?? defaultDebugLogger,\n mode: option.mode ?? 'minimal',\n };\n}\n\n// Split the variadic tuple at runtime\n/**\n * Extract the optional argument object from a variadic tuple.\n * @param args Tuple passed to a built endpoint helper.\n * @returns The argument object if present.\n */\nfunction extractArgs<L extends AnyLeaf>(args: ArgsTuple<L>): ArgsFor<L> | undefined {\n // At runtime ArgsTuple<L> is either [] or [obj]; we just pick the first if present.\n return (args as unknown as any[])[0] as any;\n}\n\n/**\n * Normalize params and query values, then construct a request URL for the given leaf.\n * @param leaf Leaf describing the endpoint.\n * @param baseUrl Optional base URL prepended to the path.\n * @param params Route parameters supplied by the caller.\n * @param query Query parameters supplied by the caller.\n * @returns Object containing the composed URL plus normalized params/query payloads.\n */\nfunction buildUrl<L extends AnyLeaf>(\n leaf: L,\n baseUrl: string,\n params: InferParams<L> | undefined,\n query: InferQuery<L> | undefined,\n) {\n const normalizedParams = zParse<InferParams<L>>(params, leaf.cfg.paramsSchema);\n const normalizedQuery = zParse<InferQuery<L>>(query, leaf.cfg.querySchema);\n const path = compilePath<L['path']>(leaf.path, (normalizedParams ?? {}) as any);\n const url = `${baseUrl ?? ''}${path}${toSearchString(normalizedQuery as any)}`;\n return { url, normalizedQuery, normalizedParams };\n}\n\n// -------------------------------------------------------------------------------------\n// Client factory\n// -------------------------------------------------------------------------------------\n/**\n * Construct typed React Query helpers backed by a routes-v3 registry leaf.\n * @param opts Route client configuration (query client, fetcher overrides, etc).\n * @returns Object that can build endpoint hooks/mutations from leaves.\n */\nexport function createRouteClient(opts: RouteClientOptions): RouteClient {\n const queryClient = opts.queryClient;\n const fetcher = opts.fetcher ?? defaultFetcher;\n const baseUrl = opts.baseUrl;\n const cursorParam = opts.cursorParam ?? 'cursor';\n const getNextCursor = opts.getNextCursor ?? defaultGetNextCursor;\n const { emit: emitDebug, mode: debugMode } = createDebugEmitter(opts.debug);\n const isVerboseDebug = debugMode === 'complete';\n const decorateDebugEvent = <T extends RouteClientDebugEvent>(\n event: T,\n details?: Partial<RouteClientDebugEvent>,\n ): RouteClientDebugEvent => {\n if (!isVerboseDebug || !details) return event;\n return { ...event, ...details } as RouteClientDebugEvent;\n };\n\n /**\n * Invalidate a set of queries sharing the given prefix.\n * @param prefix Key parts shared by matching endpoints.\n * @param exact When true, invalidate only exact key matches.\n */\n async function invalidate(prefix: string[], exact = false) {\n const queryKey = prefix as unknown as QueryKey;\n await queryClient.invalidateQueries({ queryKey, exact });\n emitDebug({ type: 'invalidate', key: queryKey, exact });\n }\n\n /**\n * Build the client surface for a single leaf (query/mutation/infinite query).\n * @param leaf Leaf describing the endpoint.\n * @param rqOpts Optional React Query configuration.\n * @returns Helper object exposing key/invalidate/setData/useEndpoint.\n */\n function buildInternal<L extends AnyLeaf>(\n leaf: L,\n rqOpts?: QueryBuildOptionsFor<L> | InfiniteBuildOptionsFor<L> | MutationBuildOptionsFor<L>,\n ): BuiltForLeaf<L> {\n const isGet = leaf.method === 'get';\n const isFeed = !!leaf.cfg.feed;\n const method = toUpper(leaf.method);\n const leafLabel = `${leaf.method.toUpperCase()} ${String(leaf.path)}`;\n emitDebug({ type: 'build', leaf: leafLabel });\n\n // --- key/invalidate/setData shared helpers ---\n const key = (...tuple: ArgsTuple<L>): QueryKey => {\n const a = extractArgs<L>(tuple);\n const params = (a as any)?.params as InferParams<L> | undefined;\n const query = (a as any)?.query as InferQuery<L> | undefined;\n const qForKey = isGet && isFeed ? stripKey(query as any, cursorParam) : (query as any);\n return buildCacheKey({ leaf, params: params as any, query: qForKey }) as unknown as QueryKey;\n };\n\n /**\n * Invalidate the React Query cache for this exact leaf invocation.\n * @param tuple Optional params/query tuple.\n */\n const invalidateExact = async (...tuple: ArgsTuple<L>) => {\n const queryKey = key(...tuple);\n await queryClient.invalidateQueries({ queryKey, exact: true });\n emitDebug({ type: 'invalidate', key: queryKey, exact: true });\n };\n\n /**\n * Update the cache entries for this leaf.\n * @param args Tuple whose first entry is the updater and optional params/query follow.\n */\n const setData = (...args: [Updater<DataShape<L>>, ...rest: ArgsTuple<L>]) => {\n const [updater, ...rest] = args;\n const k = key(...(rest as ArgsTuple<L>));\n if (isGet && isFeed) {\n queryClient.setQueryData<InfiniteData<InferOutput<L>> | undefined>(k, (prev) =>\n typeof updater === 'function' ? (updater as any)(prev) : (updater as any),\n );\n } else {\n queryClient.setQueryData<InferOutput<L> | undefined>(k, (prev) =>\n typeof updater === 'function' ? (updater as any)(prev) : (updater as any),\n );\n }\n emitDebug({ type: 'setData', key: k });\n };\n\n // --- Infinite GET ---\n if (isGet && isFeed) {\n const useEndpoint: BuiltInfinite<L>['useEndpoint'] = (...tuple) => {\n emitDebug({ type: 'useInfiniteGet', leaf: leafLabel });\n const a = extractArgs<L>(tuple);\n const params = (a as any)?.params as InferParams<L> | undefined;\n const query = (a as any)?.query as InferQuery<L> | undefined;\n\n // Normalize once; we’ll inject the cursor per page below.\n const { normalizedQuery, normalizedParams } = buildUrl(leaf, baseUrl, params, query);\n return useInfiniteQuery<\n InferOutput<L>, // TQueryFnData (per page)\n unknown, // TError\n InfiniteData<InferOutput<L>>, // TData (returned by the hook)\n QueryKey,\n Cursor\n >({\n ...(rqOpts as InfiniteBuildOptionsFor<L>),\n queryKey: key(...tuple),\n initialPageParam: undefined,\n getNextPageParam: (lastPage) => getNextCursor(lastPage),\n placeholderData: keepPreviousData,\n queryFn: async ({ pageParam }) => {\n const pageQuery = {\n ...(normalizedQuery as any),\n ...(pageParam ? { [cursorParam]: pageParam } : {}),\n };\n const { url } = buildUrl(leaf, baseUrl, params, pageQuery);\n const startedAt = Date.now();\n const detail = isVerboseDebug ? { params: normalizedParams, query: pageQuery } : undefined;\n emitDebug(\n decorateDebugEvent(\n { type: 'fetch', stage: 'start', method, url, leaf: leafLabel },\n detail,\n ),\n );\n try {\n const out = await fetcher<unknown>({ url, method });\n const parsed = zParse<InferOutput<L>>(out, leaf.cfg.outputSchema);\n emitDebug(\n decorateDebugEvent(\n {\n type: 'fetch',\n stage: 'success',\n method,\n url,\n leaf: leafLabel,\n durationMs: Date.now() - startedAt,\n },\n isVerboseDebug ? { params: normalizedParams, query: pageQuery, output: parsed } : undefined,\n ),\n );\n return parsed;\n } catch (error) {\n emitDebug(\n decorateDebugEvent(\n {\n type: 'fetch',\n stage: 'error',\n method,\n url,\n leaf: leafLabel,\n durationMs: Date.now() - startedAt,\n error,\n },\n detail,\n ),\n );\n throw error;\n }\n },\n // NOTE: TData is InfiniteData<T>, so we don't need a select here.\n }, queryClient);\n };\n\n return {\n key,\n invalidate: invalidateExact,\n setData: setData as any,\n useEndpoint,\n } as BuiltForLeaf<L>;\n }\n // --- Plain GET ---\n if (isGet) {\n const useEndpoint: BuiltQuery<L>['useEndpoint'] = (...tuple) => {\n emitDebug({ type: 'useGet', leaf: leafLabel });\n const a = extractArgs<L>(tuple);\n const params = (a as any)?.params as InferParams<L> | undefined;\n const query = (a as any)?.query as InferQuery<L> | undefined;\n\n const { url, normalizedQuery, normalizedParams } = buildUrl(leaf, baseUrl, params, query);\n return useQuery<InferOutput<L>, unknown, InferOutput<L>, QueryKey>({\n ...(rqOpts as QueryBuildOptionsFor<L>),\n queryKey: key(...tuple),\n placeholderData: keepPreviousData,\n queryFn: async () => {\n const startedAt = Date.now();\n const detail = isVerboseDebug\n ? { params: normalizedParams, query: normalizedQuery }\n : undefined;\n emitDebug(\n decorateDebugEvent(\n { type: 'fetch', stage: 'start', method, url, leaf: leafLabel },\n detail,\n ),\n );\n try {\n const out = await fetcher<unknown>({ url, method });\n const parsed = zParse<InferOutput<L>>(out, leaf.cfg.outputSchema);\n emitDebug(\n decorateDebugEvent(\n {\n type: 'fetch',\n stage: 'success',\n method,\n url,\n leaf: leafLabel,\n durationMs: Date.now() - startedAt,\n },\n isVerboseDebug\n ? { params: normalizedParams, query: normalizedQuery, output: parsed }\n : undefined,\n ),\n );\n return parsed;\n } catch (error) {\n emitDebug(\n decorateDebugEvent(\n {\n type: 'fetch',\n stage: 'error',\n method,\n url,\n leaf: leafLabel,\n durationMs: Date.now() - startedAt,\n error,\n },\n detail,\n ),\n );\n throw error;\n }\n },\n }, queryClient);\n };\n\n return {\n key,\n invalidate: invalidateExact,\n setData: setData as any,\n useEndpoint,\n } as BuiltForLeaf<L>;\n }\n\n // --- Mutation (POST/PUT/PATCH/DELETE) ---\n const useEndpoint: BuiltMutation<L>['useEndpoint'] = (...tuple) => {\n emitDebug({ type: 'useMutation', leaf: leafLabel });\n const a = extractArgs<L>(tuple);\n const params = (a as any)?.params as InferParams<L> | undefined;\n const query = (a as any)?.query as InferQuery<L> | undefined;\n\n const { url, normalizedQuery, normalizedParams } = buildUrl(leaf, baseUrl, params, query);\n return useMutation<InferOutput<L>, unknown, InferBody<L>, unknown>({\n ...(rqOpts as MutationBuildOptionsFor<L>),\n mutationKey: key(...tuple),\n mutationFn: async (body: InferBody<L>) => {\n const normalizedBody = zParse<InferBody<L>>(body, leaf.cfg.bodySchema);\n\n // Optional: switch to FormData if your method declares bodyFiles\n const isMultipart = Array.isArray(leaf.cfg.bodyFiles) && leaf.cfg.bodyFiles.length > 0;\n const payload = isMultipart ? toFormData(normalizedBody as any) : normalizedBody;\n\n const startedAt = Date.now();\n const detail = isVerboseDebug\n ? { params: normalizedParams, query: normalizedQuery }\n : undefined;\n emitDebug(\n decorateDebugEvent(\n {\n type: 'fetch',\n stage: 'start',\n method,\n url,\n leaf: leafLabel,\n body: payload,\n },\n detail,\n ),\n );\n try {\n const out = await fetcher<unknown>({ url, method, body: payload });\n const parsed = zParse<InferOutput<L>>(out, leaf.cfg.outputSchema);\n emitDebug(\n decorateDebugEvent(\n {\n type: 'fetch',\n stage: 'success',\n method,\n url,\n leaf: leafLabel,\n durationMs: Date.now() - startedAt,\n },\n isVerboseDebug\n ? { params: normalizedParams, query: normalizedQuery, output: parsed }\n : undefined,\n ),\n );\n return parsed;\n } catch (error) {\n emitDebug(\n decorateDebugEvent(\n {\n type: 'fetch',\n stage: 'error',\n method,\n url,\n leaf: leafLabel,\n durationMs: Date.now() - startedAt,\n body: payload,\n error,\n },\n detail,\n ),\n );\n throw error;\n }\n },\n }, queryClient);\n };\n\n return {\n key,\n invalidate: invalidateExact,\n setData: setData as any,\n useEndpoint,\n } as BuiltForLeaf<L>;\n }\n\n return {\n queryClient,\n invalidate,\n build: buildInternal as RouteClient['build'],\n };\n}\n\n// -------------------------------------------------------------------------------------\n// Multipart helper\n// -------------------------------------------------------------------------------------\nfunction toFormData(body: Record<string, any>): FormData {\n const fd = new FormData();\n for (const [k, v] of Object.entries(body ?? {})) {\n if (v == null) continue;\n if (Array.isArray(v)) v.forEach((item, i) => fd.append(`${k}[${i}]`, item as any));\n else fd.append(k, v as any);\n }\n return fd;\n}\n"],"mappings":"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","names":["useEndpoint"]}
|
package/dist/index.mjs
CHANGED
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
// src/routesV3.client.fetch.ts
|
|
4
|
-
var logger = {
|
|
5
|
-
debug: (...args) => {
|
|
6
|
-
console.log("[routesV3.client.fetch]", ...args);
|
|
7
|
-
}
|
|
8
|
-
};
|
|
9
4
|
var defaultFetcher = async (req) => {
|
|
10
5
|
const headers = { ...req.headers ?? {} };
|
|
11
6
|
const isFormData = typeof FormData !== "undefined" && req.body instanceof FormData;
|
|
@@ -13,7 +8,6 @@ var defaultFetcher = async (req) => {
|
|
|
13
8
|
headers["Content-Type"] || (headers["Content-Type"] = "application/json");
|
|
14
9
|
headers["Accept"] || (headers["Accept"] = "application/json");
|
|
15
10
|
}
|
|
16
|
-
logger.debug("fetch", req.method, req.url, isFormData ? "FormData" : req.body);
|
|
17
11
|
const res = await fetch(req.url, {
|
|
18
12
|
method: req.method,
|
|
19
13
|
headers,
|
|
@@ -77,6 +71,35 @@ function stripKey(obj, key) {
|
|
|
77
71
|
return rest;
|
|
78
72
|
}
|
|
79
73
|
var defaultGetNextCursor = (p) => p && typeof p === "object" && "nextCursor" in p ? p.nextCursor : void 0;
|
|
74
|
+
var noopDebugLogger = () => {
|
|
75
|
+
};
|
|
76
|
+
var defaultDebugLogger = (event) => {
|
|
77
|
+
if (typeof console === "undefined") return;
|
|
78
|
+
const fn = console.debug ?? console.log;
|
|
79
|
+
fn?.call(console, "[rrroutes-client]", event);
|
|
80
|
+
};
|
|
81
|
+
function createDebugEmitter(option) {
|
|
82
|
+
const disabled = { emit: noopDebugLogger, mode: "minimal" };
|
|
83
|
+
if (!option) {
|
|
84
|
+
return disabled;
|
|
85
|
+
}
|
|
86
|
+
if (option === true || option === "minimal") {
|
|
87
|
+
return { emit: defaultDebugLogger, mode: "minimal" };
|
|
88
|
+
}
|
|
89
|
+
if (option === "complete") {
|
|
90
|
+
return { emit: defaultDebugLogger, mode: "complete" };
|
|
91
|
+
}
|
|
92
|
+
if (typeof option === "function") {
|
|
93
|
+
return { emit: option, mode: "minimal" };
|
|
94
|
+
}
|
|
95
|
+
if (option.enabled === false) {
|
|
96
|
+
return { emit: noopDebugLogger, mode: option.mode ?? "minimal" };
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
emit: option.logger ?? defaultDebugLogger,
|
|
100
|
+
mode: option.mode ?? "minimal"
|
|
101
|
+
};
|
|
102
|
+
}
|
|
80
103
|
function extractArgs(args) {
|
|
81
104
|
return args[0];
|
|
82
105
|
}
|
|
@@ -85,7 +108,7 @@ function buildUrl(leaf, baseUrl, params, query) {
|
|
|
85
108
|
const normalizedQuery = zParse(query, leaf.cfg.querySchema);
|
|
86
109
|
const path = compilePath(leaf.path, normalizedParams ?? {});
|
|
87
110
|
const url = `${baseUrl ?? ""}${path}${toSearchString(normalizedQuery)}`;
|
|
88
|
-
return { url, normalizedQuery };
|
|
111
|
+
return { url, normalizedQuery, normalizedParams };
|
|
89
112
|
}
|
|
90
113
|
function createRouteClient(opts) {
|
|
91
114
|
const queryClient = opts.queryClient;
|
|
@@ -93,12 +116,23 @@ function createRouteClient(opts) {
|
|
|
93
116
|
const baseUrl = opts.baseUrl;
|
|
94
117
|
const cursorParam = opts.cursorParam ?? "cursor";
|
|
95
118
|
const getNextCursor = opts.getNextCursor ?? defaultGetNextCursor;
|
|
119
|
+
const { emit: emitDebug, mode: debugMode } = createDebugEmitter(opts.debug);
|
|
120
|
+
const isVerboseDebug = debugMode === "complete";
|
|
121
|
+
const decorateDebugEvent = (event, details) => {
|
|
122
|
+
if (!isVerboseDebug || !details) return event;
|
|
123
|
+
return { ...event, ...details };
|
|
124
|
+
};
|
|
96
125
|
async function invalidate(prefix, exact = false) {
|
|
97
|
-
|
|
126
|
+
const queryKey = prefix;
|
|
127
|
+
await queryClient.invalidateQueries({ queryKey, exact });
|
|
128
|
+
emitDebug({ type: "invalidate", key: queryKey, exact });
|
|
98
129
|
}
|
|
99
130
|
function buildInternal(leaf, rqOpts) {
|
|
100
131
|
const isGet = leaf.method === "get";
|
|
101
132
|
const isFeed = !!leaf.cfg.feed;
|
|
133
|
+
const method = toUpper(leaf.method);
|
|
134
|
+
const leafLabel = `${leaf.method.toUpperCase()} ${String(leaf.path)}`;
|
|
135
|
+
emitDebug({ type: "build", leaf: leafLabel });
|
|
102
136
|
const key = (...tuple) => {
|
|
103
137
|
const a = extractArgs(tuple);
|
|
104
138
|
const params = a?.params;
|
|
@@ -106,7 +140,11 @@ function createRouteClient(opts) {
|
|
|
106
140
|
const qForKey = isGet && isFeed ? stripKey(query, cursorParam) : query;
|
|
107
141
|
return buildCacheKey({ leaf, params, query: qForKey });
|
|
108
142
|
};
|
|
109
|
-
const invalidateExact = (...tuple) =>
|
|
143
|
+
const invalidateExact = async (...tuple) => {
|
|
144
|
+
const queryKey = key(...tuple);
|
|
145
|
+
await queryClient.invalidateQueries({ queryKey, exact: true });
|
|
146
|
+
emitDebug({ type: "invalidate", key: queryKey, exact: true });
|
|
147
|
+
};
|
|
110
148
|
const setData = (...args) => {
|
|
111
149
|
const [updater, ...rest] = args;
|
|
112
150
|
const k = key(...rest);
|
|
@@ -121,13 +159,15 @@ function createRouteClient(opts) {
|
|
|
121
159
|
(prev) => typeof updater === "function" ? updater(prev) : updater
|
|
122
160
|
);
|
|
123
161
|
}
|
|
162
|
+
emitDebug({ type: "setData", key: k });
|
|
124
163
|
};
|
|
125
164
|
if (isGet && isFeed) {
|
|
126
165
|
const useEndpoint2 = (...tuple) => {
|
|
166
|
+
emitDebug({ type: "useInfiniteGet", leaf: leafLabel });
|
|
127
167
|
const a = extractArgs(tuple);
|
|
128
168
|
const params = a?.params;
|
|
129
169
|
const query = a?.query;
|
|
130
|
-
const { normalizedQuery } = buildUrl(leaf, baseUrl, params, query);
|
|
170
|
+
const { normalizedQuery, normalizedParams } = buildUrl(leaf, baseUrl, params, query);
|
|
131
171
|
return useInfiniteQuery({
|
|
132
172
|
...rqOpts,
|
|
133
173
|
queryKey: key(...tuple),
|
|
@@ -140,11 +180,51 @@ function createRouteClient(opts) {
|
|
|
140
180
|
...pageParam ? { [cursorParam]: pageParam } : {}
|
|
141
181
|
};
|
|
142
182
|
const { url } = buildUrl(leaf, baseUrl, params, pageQuery);
|
|
143
|
-
const
|
|
144
|
-
|
|
183
|
+
const startedAt = Date.now();
|
|
184
|
+
const detail = isVerboseDebug ? { params: normalizedParams, query: pageQuery } : void 0;
|
|
185
|
+
emitDebug(
|
|
186
|
+
decorateDebugEvent(
|
|
187
|
+
{ type: "fetch", stage: "start", method, url, leaf: leafLabel },
|
|
188
|
+
detail
|
|
189
|
+
)
|
|
190
|
+
);
|
|
191
|
+
try {
|
|
192
|
+
const out = await fetcher({ url, method });
|
|
193
|
+
const parsed = zParse(out, leaf.cfg.outputSchema);
|
|
194
|
+
emitDebug(
|
|
195
|
+
decorateDebugEvent(
|
|
196
|
+
{
|
|
197
|
+
type: "fetch",
|
|
198
|
+
stage: "success",
|
|
199
|
+
method,
|
|
200
|
+
url,
|
|
201
|
+
leaf: leafLabel,
|
|
202
|
+
durationMs: Date.now() - startedAt
|
|
203
|
+
},
|
|
204
|
+
isVerboseDebug ? { params: normalizedParams, query: pageQuery, output: parsed } : void 0
|
|
205
|
+
)
|
|
206
|
+
);
|
|
207
|
+
return parsed;
|
|
208
|
+
} catch (error) {
|
|
209
|
+
emitDebug(
|
|
210
|
+
decorateDebugEvent(
|
|
211
|
+
{
|
|
212
|
+
type: "fetch",
|
|
213
|
+
stage: "error",
|
|
214
|
+
method,
|
|
215
|
+
url,
|
|
216
|
+
leaf: leafLabel,
|
|
217
|
+
durationMs: Date.now() - startedAt,
|
|
218
|
+
error
|
|
219
|
+
},
|
|
220
|
+
detail
|
|
221
|
+
)
|
|
222
|
+
);
|
|
223
|
+
throw error;
|
|
224
|
+
}
|
|
145
225
|
}
|
|
146
226
|
// NOTE: TData is InfiniteData<T>, so we don't need a select here.
|
|
147
|
-
});
|
|
227
|
+
}, queryClient);
|
|
148
228
|
};
|
|
149
229
|
return {
|
|
150
230
|
key,
|
|
@@ -155,19 +235,60 @@ function createRouteClient(opts) {
|
|
|
155
235
|
}
|
|
156
236
|
if (isGet) {
|
|
157
237
|
const useEndpoint2 = (...tuple) => {
|
|
238
|
+
emitDebug({ type: "useGet", leaf: leafLabel });
|
|
158
239
|
const a = extractArgs(tuple);
|
|
159
240
|
const params = a?.params;
|
|
160
241
|
const query = a?.query;
|
|
161
|
-
const { url } = buildUrl(leaf, baseUrl, params, query);
|
|
242
|
+
const { url, normalizedQuery, normalizedParams } = buildUrl(leaf, baseUrl, params, query);
|
|
162
243
|
return useQuery({
|
|
163
244
|
...rqOpts,
|
|
164
245
|
queryKey: key(...tuple),
|
|
165
246
|
placeholderData: keepPreviousData,
|
|
166
247
|
queryFn: async () => {
|
|
167
|
-
const
|
|
168
|
-
|
|
248
|
+
const startedAt = Date.now();
|
|
249
|
+
const detail = isVerboseDebug ? { params: normalizedParams, query: normalizedQuery } : void 0;
|
|
250
|
+
emitDebug(
|
|
251
|
+
decorateDebugEvent(
|
|
252
|
+
{ type: "fetch", stage: "start", method, url, leaf: leafLabel },
|
|
253
|
+
detail
|
|
254
|
+
)
|
|
255
|
+
);
|
|
256
|
+
try {
|
|
257
|
+
const out = await fetcher({ url, method });
|
|
258
|
+
const parsed = zParse(out, leaf.cfg.outputSchema);
|
|
259
|
+
emitDebug(
|
|
260
|
+
decorateDebugEvent(
|
|
261
|
+
{
|
|
262
|
+
type: "fetch",
|
|
263
|
+
stage: "success",
|
|
264
|
+
method,
|
|
265
|
+
url,
|
|
266
|
+
leaf: leafLabel,
|
|
267
|
+
durationMs: Date.now() - startedAt
|
|
268
|
+
},
|
|
269
|
+
isVerboseDebug ? { params: normalizedParams, query: normalizedQuery, output: parsed } : void 0
|
|
270
|
+
)
|
|
271
|
+
);
|
|
272
|
+
return parsed;
|
|
273
|
+
} catch (error) {
|
|
274
|
+
emitDebug(
|
|
275
|
+
decorateDebugEvent(
|
|
276
|
+
{
|
|
277
|
+
type: "fetch",
|
|
278
|
+
stage: "error",
|
|
279
|
+
method,
|
|
280
|
+
url,
|
|
281
|
+
leaf: leafLabel,
|
|
282
|
+
durationMs: Date.now() - startedAt,
|
|
283
|
+
error
|
|
284
|
+
},
|
|
285
|
+
detail
|
|
286
|
+
)
|
|
287
|
+
);
|
|
288
|
+
throw error;
|
|
289
|
+
}
|
|
169
290
|
}
|
|
170
|
-
});
|
|
291
|
+
}, queryClient);
|
|
171
292
|
};
|
|
172
293
|
return {
|
|
173
294
|
key,
|
|
@@ -177,10 +298,11 @@ function createRouteClient(opts) {
|
|
|
177
298
|
};
|
|
178
299
|
}
|
|
179
300
|
const useEndpoint = (...tuple) => {
|
|
301
|
+
emitDebug({ type: "useMutation", leaf: leafLabel });
|
|
180
302
|
const a = extractArgs(tuple);
|
|
181
303
|
const params = a?.params;
|
|
182
304
|
const query = a?.query;
|
|
183
|
-
const { url } = buildUrl(leaf, baseUrl, params, query);
|
|
305
|
+
const { url, normalizedQuery, normalizedParams } = buildUrl(leaf, baseUrl, params, query);
|
|
184
306
|
return useMutation({
|
|
185
307
|
...rqOpts,
|
|
186
308
|
mutationKey: key(...tuple),
|
|
@@ -188,10 +310,58 @@ function createRouteClient(opts) {
|
|
|
188
310
|
const normalizedBody = zParse(body, leaf.cfg.bodySchema);
|
|
189
311
|
const isMultipart = Array.isArray(leaf.cfg.bodyFiles) && leaf.cfg.bodyFiles.length > 0;
|
|
190
312
|
const payload = isMultipart ? toFormData(normalizedBody) : normalizedBody;
|
|
191
|
-
const
|
|
192
|
-
|
|
313
|
+
const startedAt = Date.now();
|
|
314
|
+
const detail = isVerboseDebug ? { params: normalizedParams, query: normalizedQuery } : void 0;
|
|
315
|
+
emitDebug(
|
|
316
|
+
decorateDebugEvent(
|
|
317
|
+
{
|
|
318
|
+
type: "fetch",
|
|
319
|
+
stage: "start",
|
|
320
|
+
method,
|
|
321
|
+
url,
|
|
322
|
+
leaf: leafLabel,
|
|
323
|
+
body: payload
|
|
324
|
+
},
|
|
325
|
+
detail
|
|
326
|
+
)
|
|
327
|
+
);
|
|
328
|
+
try {
|
|
329
|
+
const out = await fetcher({ url, method, body: payload });
|
|
330
|
+
const parsed = zParse(out, leaf.cfg.outputSchema);
|
|
331
|
+
emitDebug(
|
|
332
|
+
decorateDebugEvent(
|
|
333
|
+
{
|
|
334
|
+
type: "fetch",
|
|
335
|
+
stage: "success",
|
|
336
|
+
method,
|
|
337
|
+
url,
|
|
338
|
+
leaf: leafLabel,
|
|
339
|
+
durationMs: Date.now() - startedAt
|
|
340
|
+
},
|
|
341
|
+
isVerboseDebug ? { params: normalizedParams, query: normalizedQuery, output: parsed } : void 0
|
|
342
|
+
)
|
|
343
|
+
);
|
|
344
|
+
return parsed;
|
|
345
|
+
} catch (error) {
|
|
346
|
+
emitDebug(
|
|
347
|
+
decorateDebugEvent(
|
|
348
|
+
{
|
|
349
|
+
type: "fetch",
|
|
350
|
+
stage: "error",
|
|
351
|
+
method,
|
|
352
|
+
url,
|
|
353
|
+
leaf: leafLabel,
|
|
354
|
+
durationMs: Date.now() - startedAt,
|
|
355
|
+
body: payload,
|
|
356
|
+
error
|
|
357
|
+
},
|
|
358
|
+
detail
|
|
359
|
+
)
|
|
360
|
+
);
|
|
361
|
+
throw error;
|
|
362
|
+
}
|
|
193
363
|
}
|
|
194
|
-
});
|
|
364
|
+
}, queryClient);
|
|
195
365
|
};
|
|
196
366
|
return {
|
|
197
367
|
key,
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/routesV3.client.fetch.ts","../src/routesV3.client.index.ts"],"sourcesContent":["// routesV3.client.fetch.ts\n\nimport { Fetcher, FetchInput } from './routesV3.client.types';\n\nconst logger = {\n debug: (...args: any[]) => {\n // Uncomment to enable debug logging\n console.log('[routesV3.client.fetch]', ...args);\n },\n};\n\n/**\n * Default fetch implementation used by the route client helper.\n * @param req Normalized request information (URL, method, body, headers).\n * @returns Parsed JSON (or text fallback) from the server response.\n */\nexport const defaultFetcher: Fetcher = async <T>(req: FetchInput): Promise<T> => {\n const headers: Record<string, string> = { ...(req.headers ?? {}) };\n const isFormData = typeof FormData !== 'undefined' && req.body instanceof FormData;\n if (!isFormData) {\n headers['Content-Type'] ||= 'application/json';\n headers['Accept'] ||= 'application/json';\n }\n\n logger.debug('fetch', req.method, req.url, isFormData ? 'FormData' : req.body);\n const res = await fetch(req.url, {\n method: req.method,\n headers,\n body: isFormData ? (req.body as any) : req.body == null ? undefined : JSON.stringify(req.body),\n });\n\n const text = await res.text();\n if (!res.ok) {\n const snippet = text.slice(0, 400);\n throw new Error(`[${res.status}] ${res.statusText} — ${snippet}`);\n }\n\n try {\n return JSON.parse(text) as T;\n } catch {\n return text as unknown as T;\n }\n};\n","// routesV3.client.ts\nimport {\n keepPreviousData,\n useInfiniteQuery,\n useMutation,\n useQuery,\n type InfiniteData,\n type QueryKey,\n} from '@tanstack/react-query';\nimport type { ZodType } from 'zod';\nimport {\n HttpMethod,\n buildCacheKey,\n compilePath,\n} from '@emeryld/rrroutes-contract';\nimport type {\n AnyLeaf,\n InferBody,\n InferOutput,\n InferParams,\n InferQuery,\n} from '@emeryld/rrroutes-contract';\nimport { defaultFetcher } from './routesV3.client.fetch';\nimport type {\n ArgsFor,\n ArgsTuple,\n BuiltForLeaf,\n BuiltInfinite,\n BuiltMutation,\n BuiltQuery,\n Cursor,\n DataShape,\n InfiniteBuildOptionsFor,\n MutationBuildOptionsFor,\n QueryBuildOptionsFor,\n RouteClient,\n RouteClientOptions,\n Updater,\n} from './routesV3.client.types';\n\n// -------------------------------------------------------------------------------------\n// Tiny helpers\n// -------------------------------------------------------------------------------------\n/**\n * Convert an HTTP method to uppercase (as expected by fetch).\n * @param m Lowercase HTTP method.\n * @returns Uppercase method string.\n */\nconst toUpper = (m: HttpMethod): Uppercase<HttpMethod> => m.toUpperCase() as Uppercase<HttpMethod>;\n\n/**\n * Parse the given value with the supplied schema (if present).\n * @param value Raw value to validate.\n * @param schema Optional Zod schema used for validation/coercion.\n * @returns The validated or original value.\n */\nfunction zParse<T>(value: unknown, schema?: ZodType): T {\n return schema ? (schema.parse(value) as T) : (value as T);\n}\n\n/**\n * Serialize a query object into a search string.\n * @param query Query params object (possibly undefined).\n * @returns Query string prefixed with `?`, or empty string.\n */\nfunction toSearchString(query: Record<string, unknown> | undefined) {\n if (!query) return '';\n const params = new URLSearchParams();\n for (const [k, v] of Object.entries(query)) {\n if (v == null) continue;\n if (Array.isArray(v)) {\n v.forEach((x) => {\n if (x == null) return;\n if (typeof x === 'object') {\n params.append(k, JSON.stringify(x));\n } else {\n params.append(k, String(x));\n }\n });\n continue;\n }\n if (typeof v === 'object') {\n params.set(k, JSON.stringify(v));\n continue;\n }\n params.set(k, String(v));\n }\n const s = params.toString();\n return s ? `?${s}` : '';\n}\n\n/**\n * Remove the given key from an object (used to drop cursors from cache keys).\n * @param obj Source object.\n * @param key Property name to omit.\n * @returns Copy of the object without the specified key.\n */\nfunction stripKey<Q extends Record<string, unknown> | undefined>(obj: Q, key: string): Q {\n if (!obj) return obj;\n const { [key]: _omit, ...rest } = obj as any;\n return rest as Q;\n}\n\n/**\n * Default cursor extractor used by infinite queries.\n * @param p Page result returned from the server.\n * @returns Next cursor string, if present.\n */\nconst defaultGetNextCursor = (p: unknown): Cursor =>\n p && typeof p === 'object' && 'nextCursor' in p ? (p as any).nextCursor : undefined;\n\n// Split the variadic tuple at runtime\n/**\n * Extract the optional argument object from a variadic tuple.\n * @param args Tuple passed to a built endpoint helper.\n * @returns The argument object if present.\n */\nfunction extractArgs<L extends AnyLeaf>(args: ArgsTuple<L>): ArgsFor<L> | undefined {\n // At runtime ArgsTuple<L> is either [] or [obj]; we just pick the first if present.\n return (args as unknown as any[])[0] as any;\n}\n\n/**\n * Normalize params and query values, then construct a request URL for the given leaf.\n * @param leaf Leaf describing the endpoint.\n * @param baseUrl Optional base URL prepended to the path.\n * @param params Route parameters supplied by the caller.\n * @param query Query parameters supplied by the caller.\n * @returns Object containing the composed URL and normalized query payload.\n */\nfunction buildUrl<L extends AnyLeaf>(\n leaf: L,\n baseUrl: string,\n params: InferParams<L> | undefined,\n query: InferQuery<L> | undefined,\n) {\n const normalizedParams = zParse<InferParams<L>>(params, leaf.cfg.paramsSchema);\n const normalizedQuery = zParse<InferQuery<L>>(query, leaf.cfg.querySchema);\n const path = compilePath<L['path']>(leaf.path, (normalizedParams ?? {}) as any);\n const url = `${baseUrl ?? ''}${path}${toSearchString(normalizedQuery as any)}`;\n return { url, normalizedQuery };\n}\n\n// -------------------------------------------------------------------------------------\n// Client factory\n// -------------------------------------------------------------------------------------\n/**\n * Construct typed React Query helpers backed by a routes-v3 registry leaf.\n * @param opts Route client configuration (query client, fetcher overrides, etc).\n * @returns Object that can build endpoint hooks/mutations from leaves.\n */\nexport function createRouteClient(opts: RouteClientOptions): RouteClient {\n const queryClient = opts.queryClient;\n const fetcher = opts.fetcher ?? defaultFetcher;\n const baseUrl = opts.baseUrl;\n const cursorParam = opts.cursorParam ?? 'cursor';\n const getNextCursor = opts.getNextCursor ?? defaultGetNextCursor;\n\n /**\n * Invalidate a set of queries sharing the given prefix.\n * @param prefix Key parts shared by matching endpoints.\n * @param exact When true, invalidate only exact key matches.\n */\n async function invalidate(prefix: string[], exact = false) {\n await queryClient.invalidateQueries({ queryKey: prefix as unknown as QueryKey, exact });\n }\n\n /**\n * Build the client surface for a single leaf (query/mutation/infinite query).\n * @param leaf Leaf describing the endpoint.\n * @param rqOpts Optional React Query configuration.\n * @returns Helper object exposing key/invalidate/setData/useEndpoint.\n */\n function buildInternal<L extends AnyLeaf>(\n leaf: L,\n rqOpts?: QueryBuildOptionsFor<L> | InfiniteBuildOptionsFor<L> | MutationBuildOptionsFor<L>,\n ): BuiltForLeaf<L> {\n const isGet = leaf.method === 'get';\n const isFeed = !!leaf.cfg.feed;\n\n // --- key/invalidate/setData shared helpers ---\n const key = (...tuple: ArgsTuple<L>): QueryKey => {\n const a = extractArgs<L>(tuple);\n const params = (a as any)?.params as InferParams<L> | undefined;\n const query = (a as any)?.query as InferQuery<L> | undefined;\n const qForKey = isGet && isFeed ? stripKey(query as any, cursorParam) : (query as any);\n return buildCacheKey({ leaf, params: params as any, query: qForKey }) as unknown as QueryKey;\n };\n\n /**\n * Invalidate the React Query cache for this exact leaf invocation.\n * @param tuple Optional params/query tuple.\n */\n const invalidateExact = (...tuple: ArgsTuple<L>) =>\n queryClient.invalidateQueries({ queryKey: key(...tuple), exact: true });\n\n /**\n * Update the cache entries for this leaf.\n * @param args Tuple whose first entry is the updater and optional params/query follow.\n */\n const setData = (...args: [Updater<DataShape<L>>, ...rest: ArgsTuple<L>]) => {\n const [updater, ...rest] = args;\n const k = key(...(rest as ArgsTuple<L>));\n if (isGet && isFeed) {\n queryClient.setQueryData<InfiniteData<InferOutput<L>> | undefined>(k, (prev) =>\n typeof updater === 'function' ? (updater as any)(prev) : (updater as any),\n );\n } else {\n queryClient.setQueryData<InferOutput<L> | undefined>(k, (prev) =>\n typeof updater === 'function' ? (updater as any)(prev) : (updater as any),\n );\n }\n };\n\n // --- Infinite GET ---\n if (isGet && isFeed) {\n const useEndpoint: BuiltInfinite<L>['useEndpoint'] = (...tuple) => {\n const a = extractArgs<L>(tuple);\n const params = (a as any)?.params as InferParams<L> | undefined;\n const query = (a as any)?.query as InferQuery<L> | undefined;\n\n // Normalize once; we’ll inject the cursor per page below.\n const { normalizedQuery } = buildUrl(leaf, baseUrl, params, query);\n return useInfiniteQuery<\n InferOutput<L>, // TQueryFnData (per page)\n unknown, // TError\n InfiniteData<InferOutput<L>>, // TData (returned by the hook)\n QueryKey,\n Cursor\n >({\n ...(rqOpts as InfiniteBuildOptionsFor<L>),\n queryKey: key(...tuple),\n initialPageParam: undefined,\n getNextPageParam: (lastPage) => getNextCursor(lastPage),\n placeholderData: keepPreviousData,\n queryFn: async ({ pageParam }) => {\n const pageQuery = {\n ...(normalizedQuery as any),\n ...(pageParam ? { [cursorParam]: pageParam } : {}),\n };\n const { url } = buildUrl(leaf, baseUrl, params, pageQuery);\n const out = await fetcher<unknown>({ url, method: toUpper(leaf.method) });\n return zParse<InferOutput<L>>(out, leaf.cfg.outputSchema);\n },\n // NOTE: TData is InfiniteData<T>, so we don't need a select here.\n });\n };\n\n return {\n key,\n invalidate: invalidateExact,\n setData: setData as any,\n useEndpoint,\n } as BuiltForLeaf<L>;\n }\n\n // --- Plain GET ---\n if (isGet) {\n const useEndpoint: BuiltQuery<L>['useEndpoint'] = (...tuple) => {\n const a = extractArgs<L>(tuple);\n const params = (a as any)?.params as InferParams<L> | undefined;\n const query = (a as any)?.query as InferQuery<L> | undefined;\n\n const { url } = buildUrl(leaf, baseUrl, params, query);\n return useQuery<InferOutput<L>, unknown, InferOutput<L>, QueryKey>({\n ...(rqOpts as QueryBuildOptionsFor<L>),\n queryKey: key(...tuple),\n placeholderData: keepPreviousData,\n queryFn: async () => {\n const out = await fetcher<unknown>({ url, method: toUpper(leaf.method) });\n return zParse<InferOutput<L>>(out, leaf.cfg.outputSchema);\n },\n });\n };\n\n return {\n key,\n invalidate: invalidateExact,\n setData: setData as any,\n useEndpoint,\n } as BuiltForLeaf<L>;\n }\n\n // --- Mutation (POST/PUT/PATCH/DELETE) ---\n const useEndpoint: BuiltMutation<L>['useEndpoint'] = (...tuple) => {\n const a = extractArgs<L>(tuple);\n const params = (a as any)?.params as InferParams<L> | undefined;\n const query = (a as any)?.query as InferQuery<L> | undefined;\n\n const { url } = buildUrl(leaf, baseUrl, params, query);\n return useMutation<InferOutput<L>, unknown, InferBody<L>, unknown>({\n ...(rqOpts as MutationBuildOptionsFor<L>),\n mutationKey: key(...tuple),\n mutationFn: async (body: InferBody<L>) => {\n const normalizedBody = zParse<InferBody<L>>(body, leaf.cfg.bodySchema);\n\n // Optional: switch to FormData if your method declares bodyFiles\n const isMultipart = Array.isArray(leaf.cfg.bodyFiles) && leaf.cfg.bodyFiles.length > 0;\n const payload = isMultipart ? toFormData(normalizedBody as any) : normalizedBody;\n\n const out = await fetcher<unknown>({ url, method: toUpper(leaf.method), body: payload });\n return zParse<InferOutput<L>>(out, leaf.cfg.outputSchema);\n },\n });\n };\n\n return {\n key,\n invalidate: invalidateExact,\n setData: setData as any,\n useEndpoint,\n } as BuiltForLeaf<L>;\n }\n\n return {\n queryClient,\n invalidate,\n build: buildInternal as RouteClient['build'],\n };\n}\n\n// -------------------------------------------------------------------------------------\n// Multipart helper\n// -------------------------------------------------------------------------------------\nfunction toFormData(body: Record<string, any>): FormData {\n const fd = new FormData();\n for (const [k, v] of Object.entries(body ?? {})) {\n if (v == null) continue;\n if (Array.isArray(v)) v.forEach((item, i) => fd.append(`${k}[${i}]`, item as any));\n else fd.append(k, v as any);\n }\n return fd;\n}\n"],"mappings":"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","names":["useEndpoint"]}
|
|
1
|
+
{"version":3,"sources":["../src/routesV3.client.fetch.ts","../src/routesV3.client.index.ts"],"sourcesContent":["// routesV3.client.fetch.ts\n\nimport { Fetcher, FetchInput } from './routesV3.client.types';\n\n/**\n * Default fetch implementation used by the route client helper.\n * @param req Normalized request information (URL, method, body, headers).\n * @returns Parsed JSON (or text fallback) from the server response.\n */\nexport const defaultFetcher: Fetcher = async <T>(req: FetchInput): Promise<T> => {\n const headers: Record<string, string> = { ...(req.headers ?? {}) };\n const isFormData = typeof FormData !== 'undefined' && req.body instanceof FormData;\n if (!isFormData) {\n headers['Content-Type'] ||= 'application/json';\n headers['Accept'] ||= 'application/json';\n }\n\n const res = await fetch(req.url, {\n method: req.method,\n headers,\n body: isFormData ? (req.body as any) : req.body == null ? undefined : JSON.stringify(req.body),\n });\n\n const text = await res.text();\n if (!res.ok) {\n const snippet = text.slice(0, 400);\n throw new Error(`[${res.status}] ${res.statusText} — ${snippet}`);\n }\n\n try {\n return JSON.parse(text) as T;\n } catch {\n return text as unknown as T;\n }\n};\n","// routesV3.client.ts\nimport {\n keepPreviousData,\n useInfiniteQuery,\n useMutation,\n useQuery,\n type InfiniteData,\n type QueryKey,\n} from '@tanstack/react-query';\nimport type { ZodType } from 'zod';\nimport {\n HttpMethod,\n buildCacheKey,\n compilePath,\n} from '@emeryld/rrroutes-contract';\nimport type {\n AnyLeaf,\n InferBody,\n InferOutput,\n InferParams,\n InferQuery,\n} from '@emeryld/rrroutes-contract';\nimport { defaultFetcher } from './routesV3.client.fetch';\nimport type {\n ArgsFor,\n ArgsTuple,\n BuiltForLeaf,\n BuiltInfinite,\n BuiltMutation,\n BuiltQuery,\n Cursor,\n DataShape,\n InfiniteBuildOptionsFor,\n MutationBuildOptionsFor,\n QueryBuildOptionsFor,\n RouteClient,\n RouteClientOptions,\n RouteClientDebugEvent,\n RouteClientDebugLogger,\n RouteClientDebugMode,\n RouteClientDebugOptions,\n Updater,\n} from './routesV3.client.types';\n\n// -------------------------------------------------------------------------------------\n// Tiny helpers\n// -------------------------------------------------------------------------------------\n/**\n * Convert an HTTP method to uppercase (as expected by fetch).\n * @param m Lowercase HTTP method.\n * @returns Uppercase method string.\n */\nconst toUpper = (m: HttpMethod): Uppercase<HttpMethod> => m.toUpperCase() as Uppercase<HttpMethod>;\n\n/**\n * Parse the given value with the supplied schema (if present).\n * @param value Raw value to validate.\n * @param schema Optional Zod schema used for validation/coercion.\n * @returns The validated or original value.\n */\nfunction zParse<T>(value: unknown, schema?: ZodType): T {\n return schema ? (schema.parse(value) as T) : (value as T);\n}\n\n/**\n * Serialize a query object into a search string.\n * @param query Query params object (possibly undefined).\n * @returns Query string prefixed with `?`, or empty string.\n */\nfunction toSearchString(query: Record<string, unknown> | undefined) {\n if (!query) return '';\n const params = new URLSearchParams();\n for (const [k, v] of Object.entries(query)) {\n if (v == null) continue;\n if (Array.isArray(v)) {\n v.forEach((x) => {\n if (x == null) return;\n if (typeof x === 'object') {\n params.append(k, JSON.stringify(x));\n } else {\n params.append(k, String(x));\n }\n });\n continue;\n }\n if (typeof v === 'object') {\n params.set(k, JSON.stringify(v));\n continue;\n }\n params.set(k, String(v));\n }\n const s = params.toString();\n return s ? `?${s}` : '';\n}\n\n/**\n * Remove the given key from an object (used to drop cursors from cache keys).\n * @param obj Source object.\n * @param key Property name to omit.\n * @returns Copy of the object without the specified key.\n */\nfunction stripKey<Q extends Record<string, unknown> | undefined>(obj: Q, key: string): Q {\n if (!obj) return obj;\n const { [key]: _omit, ...rest } = obj as any;\n return rest as Q;\n}\n\n/**\n * Default cursor extractor used by infinite queries.\n * @param p Page result returned from the server.\n * @returns Next cursor string, if present.\n */\nconst defaultGetNextCursor = (p: unknown): Cursor =>\n p && typeof p === 'object' && 'nextCursor' in p ? (p as any).nextCursor : undefined;\n\n// Debug logging --------------------------------------------------------------\nconst noopDebugLogger: RouteClientDebugLogger = () => {};\n\nconst defaultDebugLogger: RouteClientDebugLogger = (event: RouteClientDebugEvent) => {\n if (typeof console === 'undefined') return;\n const fn = console.debug ?? console.log;\n fn?.call(console, '[rrroutes-client]', event);\n};\n\ntype DebugEmitter = {\n emit: RouteClientDebugLogger;\n mode: RouteClientDebugMode;\n};\n\nfunction createDebugEmitter(option?: RouteClientDebugOptions): DebugEmitter {\n const disabled: DebugEmitter = { emit: noopDebugLogger, mode: 'minimal' };\n\n if (!option) {\n return disabled;\n }\n if (option === true || option === 'minimal') {\n return { emit: defaultDebugLogger, mode: 'minimal' };\n }\n if (option === 'complete') {\n return { emit: defaultDebugLogger, mode: 'complete' };\n }\n if (typeof option === 'function') {\n return { emit: option, mode: 'minimal' };\n }\n if (option.enabled === false) {\n return { emit: noopDebugLogger, mode: option.mode ?? 'minimal' };\n }\n return {\n emit: option.logger ?? defaultDebugLogger,\n mode: option.mode ?? 'minimal',\n };\n}\n\n// Split the variadic tuple at runtime\n/**\n * Extract the optional argument object from a variadic tuple.\n * @param args Tuple passed to a built endpoint helper.\n * @returns The argument object if present.\n */\nfunction extractArgs<L extends AnyLeaf>(args: ArgsTuple<L>): ArgsFor<L> | undefined {\n // At runtime ArgsTuple<L> is either [] or [obj]; we just pick the first if present.\n return (args as unknown as any[])[0] as any;\n}\n\n/**\n * Normalize params and query values, then construct a request URL for the given leaf.\n * @param leaf Leaf describing the endpoint.\n * @param baseUrl Optional base URL prepended to the path.\n * @param params Route parameters supplied by the caller.\n * @param query Query parameters supplied by the caller.\n * @returns Object containing the composed URL plus normalized params/query payloads.\n */\nfunction buildUrl<L extends AnyLeaf>(\n leaf: L,\n baseUrl: string,\n params: InferParams<L> | undefined,\n query: InferQuery<L> | undefined,\n) {\n const normalizedParams = zParse<InferParams<L>>(params, leaf.cfg.paramsSchema);\n const normalizedQuery = zParse<InferQuery<L>>(query, leaf.cfg.querySchema);\n const path = compilePath<L['path']>(leaf.path, (normalizedParams ?? {}) as any);\n const url = `${baseUrl ?? ''}${path}${toSearchString(normalizedQuery as any)}`;\n return { url, normalizedQuery, normalizedParams };\n}\n\n// -------------------------------------------------------------------------------------\n// Client factory\n// -------------------------------------------------------------------------------------\n/**\n * Construct typed React Query helpers backed by a routes-v3 registry leaf.\n * @param opts Route client configuration (query client, fetcher overrides, etc).\n * @returns Object that can build endpoint hooks/mutations from leaves.\n */\nexport function createRouteClient(opts: RouteClientOptions): RouteClient {\n const queryClient = opts.queryClient;\n const fetcher = opts.fetcher ?? defaultFetcher;\n const baseUrl = opts.baseUrl;\n const cursorParam = opts.cursorParam ?? 'cursor';\n const getNextCursor = opts.getNextCursor ?? defaultGetNextCursor;\n const { emit: emitDebug, mode: debugMode } = createDebugEmitter(opts.debug);\n const isVerboseDebug = debugMode === 'complete';\n const decorateDebugEvent = <T extends RouteClientDebugEvent>(\n event: T,\n details?: Partial<RouteClientDebugEvent>,\n ): RouteClientDebugEvent => {\n if (!isVerboseDebug || !details) return event;\n return { ...event, ...details } as RouteClientDebugEvent;\n };\n\n /**\n * Invalidate a set of queries sharing the given prefix.\n * @param prefix Key parts shared by matching endpoints.\n * @param exact When true, invalidate only exact key matches.\n */\n async function invalidate(prefix: string[], exact = false) {\n const queryKey = prefix as unknown as QueryKey;\n await queryClient.invalidateQueries({ queryKey, exact });\n emitDebug({ type: 'invalidate', key: queryKey, exact });\n }\n\n /**\n * Build the client surface for a single leaf (query/mutation/infinite query).\n * @param leaf Leaf describing the endpoint.\n * @param rqOpts Optional React Query configuration.\n * @returns Helper object exposing key/invalidate/setData/useEndpoint.\n */\n function buildInternal<L extends AnyLeaf>(\n leaf: L,\n rqOpts?: QueryBuildOptionsFor<L> | InfiniteBuildOptionsFor<L> | MutationBuildOptionsFor<L>,\n ): BuiltForLeaf<L> {\n const isGet = leaf.method === 'get';\n const isFeed = !!leaf.cfg.feed;\n const method = toUpper(leaf.method);\n const leafLabel = `${leaf.method.toUpperCase()} ${String(leaf.path)}`;\n emitDebug({ type: 'build', leaf: leafLabel });\n\n // --- key/invalidate/setData shared helpers ---\n const key = (...tuple: ArgsTuple<L>): QueryKey => {\n const a = extractArgs<L>(tuple);\n const params = (a as any)?.params as InferParams<L> | undefined;\n const query = (a as any)?.query as InferQuery<L> | undefined;\n const qForKey = isGet && isFeed ? stripKey(query as any, cursorParam) : (query as any);\n return buildCacheKey({ leaf, params: params as any, query: qForKey }) as unknown as QueryKey;\n };\n\n /**\n * Invalidate the React Query cache for this exact leaf invocation.\n * @param tuple Optional params/query tuple.\n */\n const invalidateExact = async (...tuple: ArgsTuple<L>) => {\n const queryKey = key(...tuple);\n await queryClient.invalidateQueries({ queryKey, exact: true });\n emitDebug({ type: 'invalidate', key: queryKey, exact: true });\n };\n\n /**\n * Update the cache entries for this leaf.\n * @param args Tuple whose first entry is the updater and optional params/query follow.\n */\n const setData = (...args: [Updater<DataShape<L>>, ...rest: ArgsTuple<L>]) => {\n const [updater, ...rest] = args;\n const k = key(...(rest as ArgsTuple<L>));\n if (isGet && isFeed) {\n queryClient.setQueryData<InfiniteData<InferOutput<L>> | undefined>(k, (prev) =>\n typeof updater === 'function' ? (updater as any)(prev) : (updater as any),\n );\n } else {\n queryClient.setQueryData<InferOutput<L> | undefined>(k, (prev) =>\n typeof updater === 'function' ? (updater as any)(prev) : (updater as any),\n );\n }\n emitDebug({ type: 'setData', key: k });\n };\n\n // --- Infinite GET ---\n if (isGet && isFeed) {\n const useEndpoint: BuiltInfinite<L>['useEndpoint'] = (...tuple) => {\n emitDebug({ type: 'useInfiniteGet', leaf: leafLabel });\n const a = extractArgs<L>(tuple);\n const params = (a as any)?.params as InferParams<L> | undefined;\n const query = (a as any)?.query as InferQuery<L> | undefined;\n\n // Normalize once; we’ll inject the cursor per page below.\n const { normalizedQuery, normalizedParams } = buildUrl(leaf, baseUrl, params, query);\n return useInfiniteQuery<\n InferOutput<L>, // TQueryFnData (per page)\n unknown, // TError\n InfiniteData<InferOutput<L>>, // TData (returned by the hook)\n QueryKey,\n Cursor\n >({\n ...(rqOpts as InfiniteBuildOptionsFor<L>),\n queryKey: key(...tuple),\n initialPageParam: undefined,\n getNextPageParam: (lastPage) => getNextCursor(lastPage),\n placeholderData: keepPreviousData,\n queryFn: async ({ pageParam }) => {\n const pageQuery = {\n ...(normalizedQuery as any),\n ...(pageParam ? { [cursorParam]: pageParam } : {}),\n };\n const { url } = buildUrl(leaf, baseUrl, params, pageQuery);\n const startedAt = Date.now();\n const detail = isVerboseDebug ? { params: normalizedParams, query: pageQuery } : undefined;\n emitDebug(\n decorateDebugEvent(\n { type: 'fetch', stage: 'start', method, url, leaf: leafLabel },\n detail,\n ),\n );\n try {\n const out = await fetcher<unknown>({ url, method });\n const parsed = zParse<InferOutput<L>>(out, leaf.cfg.outputSchema);\n emitDebug(\n decorateDebugEvent(\n {\n type: 'fetch',\n stage: 'success',\n method,\n url,\n leaf: leafLabel,\n durationMs: Date.now() - startedAt,\n },\n isVerboseDebug ? { params: normalizedParams, query: pageQuery, output: parsed } : undefined,\n ),\n );\n return parsed;\n } catch (error) {\n emitDebug(\n decorateDebugEvent(\n {\n type: 'fetch',\n stage: 'error',\n method,\n url,\n leaf: leafLabel,\n durationMs: Date.now() - startedAt,\n error,\n },\n detail,\n ),\n );\n throw error;\n }\n },\n // NOTE: TData is InfiniteData<T>, so we don't need a select here.\n }, queryClient);\n };\n\n return {\n key,\n invalidate: invalidateExact,\n setData: setData as any,\n useEndpoint,\n } as BuiltForLeaf<L>;\n }\n // --- Plain GET ---\n if (isGet) {\n const useEndpoint: BuiltQuery<L>['useEndpoint'] = (...tuple) => {\n emitDebug({ type: 'useGet', leaf: leafLabel });\n const a = extractArgs<L>(tuple);\n const params = (a as any)?.params as InferParams<L> | undefined;\n const query = (a as any)?.query as InferQuery<L> | undefined;\n\n const { url, normalizedQuery, normalizedParams } = buildUrl(leaf, baseUrl, params, query);\n return useQuery<InferOutput<L>, unknown, InferOutput<L>, QueryKey>({\n ...(rqOpts as QueryBuildOptionsFor<L>),\n queryKey: key(...tuple),\n placeholderData: keepPreviousData,\n queryFn: async () => {\n const startedAt = Date.now();\n const detail = isVerboseDebug\n ? { params: normalizedParams, query: normalizedQuery }\n : undefined;\n emitDebug(\n decorateDebugEvent(\n { type: 'fetch', stage: 'start', method, url, leaf: leafLabel },\n detail,\n ),\n );\n try {\n const out = await fetcher<unknown>({ url, method });\n const parsed = zParse<InferOutput<L>>(out, leaf.cfg.outputSchema);\n emitDebug(\n decorateDebugEvent(\n {\n type: 'fetch',\n stage: 'success',\n method,\n url,\n leaf: leafLabel,\n durationMs: Date.now() - startedAt,\n },\n isVerboseDebug\n ? { params: normalizedParams, query: normalizedQuery, output: parsed }\n : undefined,\n ),\n );\n return parsed;\n } catch (error) {\n emitDebug(\n decorateDebugEvent(\n {\n type: 'fetch',\n stage: 'error',\n method,\n url,\n leaf: leafLabel,\n durationMs: Date.now() - startedAt,\n error,\n },\n detail,\n ),\n );\n throw error;\n }\n },\n }, queryClient);\n };\n\n return {\n key,\n invalidate: invalidateExact,\n setData: setData as any,\n useEndpoint,\n } as BuiltForLeaf<L>;\n }\n\n // --- Mutation (POST/PUT/PATCH/DELETE) ---\n const useEndpoint: BuiltMutation<L>['useEndpoint'] = (...tuple) => {\n emitDebug({ type: 'useMutation', leaf: leafLabel });\n const a = extractArgs<L>(tuple);\n const params = (a as any)?.params as InferParams<L> | undefined;\n const query = (a as any)?.query as InferQuery<L> | undefined;\n\n const { url, normalizedQuery, normalizedParams } = buildUrl(leaf, baseUrl, params, query);\n return useMutation<InferOutput<L>, unknown, InferBody<L>, unknown>({\n ...(rqOpts as MutationBuildOptionsFor<L>),\n mutationKey: key(...tuple),\n mutationFn: async (body: InferBody<L>) => {\n const normalizedBody = zParse<InferBody<L>>(body, leaf.cfg.bodySchema);\n\n // Optional: switch to FormData if your method declares bodyFiles\n const isMultipart = Array.isArray(leaf.cfg.bodyFiles) && leaf.cfg.bodyFiles.length > 0;\n const payload = isMultipart ? toFormData(normalizedBody as any) : normalizedBody;\n\n const startedAt = Date.now();\n const detail = isVerboseDebug\n ? { params: normalizedParams, query: normalizedQuery }\n : undefined;\n emitDebug(\n decorateDebugEvent(\n {\n type: 'fetch',\n stage: 'start',\n method,\n url,\n leaf: leafLabel,\n body: payload,\n },\n detail,\n ),\n );\n try {\n const out = await fetcher<unknown>({ url, method, body: payload });\n const parsed = zParse<InferOutput<L>>(out, leaf.cfg.outputSchema);\n emitDebug(\n decorateDebugEvent(\n {\n type: 'fetch',\n stage: 'success',\n method,\n url,\n leaf: leafLabel,\n durationMs: Date.now() - startedAt,\n },\n isVerboseDebug\n ? { params: normalizedParams, query: normalizedQuery, output: parsed }\n : undefined,\n ),\n );\n return parsed;\n } catch (error) {\n emitDebug(\n decorateDebugEvent(\n {\n type: 'fetch',\n stage: 'error',\n method,\n url,\n leaf: leafLabel,\n durationMs: Date.now() - startedAt,\n body: payload,\n error,\n },\n detail,\n ),\n );\n throw error;\n }\n },\n }, queryClient);\n };\n\n return {\n key,\n invalidate: invalidateExact,\n setData: setData as any,\n useEndpoint,\n } as BuiltForLeaf<L>;\n }\n\n return {\n queryClient,\n invalidate,\n build: buildInternal as RouteClient['build'],\n };\n}\n\n// -------------------------------------------------------------------------------------\n// Multipart helper\n// -------------------------------------------------------------------------------------\nfunction toFormData(body: Record<string, any>): FormData {\n const fd = new FormData();\n for (const [k, v] of Object.entries(body ?? {})) {\n if (v == null) continue;\n if (Array.isArray(v)) v.forEach((item, i) => fd.append(`${k}[${i}]`, item as any));\n else fd.append(k, v as any);\n }\n return fd;\n}\n"],"mappings":"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","names":["useEndpoint"]}
|
|
@@ -17,6 +17,40 @@ export type FetchInput = {
|
|
|
17
17
|
};
|
|
18
18
|
/** Default signature for HTTP fetch implementations. */
|
|
19
19
|
export type Fetcher = <T>(req: FetchInput) => Promise<T>;
|
|
20
|
+
/** Debug verbosity levels supported by the client. */
|
|
21
|
+
export type RouteClientDebugMode = 'minimal' | 'complete';
|
|
22
|
+
/** Discrete debug events emitted by the client internals. */
|
|
23
|
+
export type RouteClientDebugEvent = {
|
|
24
|
+
type: 'fetch';
|
|
25
|
+
stage: 'start' | 'success' | 'error';
|
|
26
|
+
method: Uppercase<HttpMethod>;
|
|
27
|
+
url: string;
|
|
28
|
+
leaf: string;
|
|
29
|
+
durationMs?: number;
|
|
30
|
+
body?: unknown;
|
|
31
|
+
params?: unknown;
|
|
32
|
+
query?: unknown;
|
|
33
|
+
output?: unknown;
|
|
34
|
+
error?: unknown;
|
|
35
|
+
} | {
|
|
36
|
+
type: 'invalidate';
|
|
37
|
+
key: QueryKey;
|
|
38
|
+
exact: boolean;
|
|
39
|
+
} | {
|
|
40
|
+
type: 'setData';
|
|
41
|
+
key: QueryKey;
|
|
42
|
+
} | {
|
|
43
|
+
type: 'build' | 'useInfiniteGet' | 'useGet' | 'useMutation';
|
|
44
|
+
leaf: string;
|
|
45
|
+
};
|
|
46
|
+
/** Logger signature invoked when debug logging is enabled. */
|
|
47
|
+
export type RouteClientDebugLogger = (event: RouteClientDebugEvent) => void;
|
|
48
|
+
/** Toggle or customize debug logging produced by the client. */
|
|
49
|
+
export type RouteClientDebugOptions = boolean | RouteClientDebugLogger | RouteClientDebugMode | {
|
|
50
|
+
enabled?: boolean;
|
|
51
|
+
logger?: RouteClientDebugLogger;
|
|
52
|
+
mode?: RouteClientDebugMode;
|
|
53
|
+
};
|
|
20
54
|
/** Configuration passed to `createRouteClient`. */
|
|
21
55
|
export type RouteClientOptions = {
|
|
22
56
|
/** Base URL prepended to every route (optional). */
|
|
@@ -29,6 +63,8 @@ export type RouteClientOptions = {
|
|
|
29
63
|
cursorParam?: string;
|
|
30
64
|
/** Function that extracts the next cursor from an infinite query page. */
|
|
31
65
|
getNextCursor?: (page: unknown) => Cursor;
|
|
66
|
+
/** Optional debug logger for verbose insights (supports minimal/complete modes). */
|
|
67
|
+
debug?: RouteClientDebugOptions;
|
|
32
68
|
};
|
|
33
69
|
type OutputOf<L extends AnyLeaf> = InferOutput<L>;
|
|
34
70
|
type ParamsOf<L extends AnyLeaf> = InferParams<L>;
|