@veloxts/client 0.6.64 → 0.6.65
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/CHANGELOG.md +6 -0
- package/dist/react/proxy-hooks.js +190 -11
- package/dist/react/proxy-types.d.ts +27 -1
- package/dist/types.d.ts +15 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -31,6 +31,51 @@ import { buildQueryKey } from './utils.js';
|
|
|
31
31
|
// ============================================================================
|
|
32
32
|
// Query/Mutation Detection
|
|
33
33
|
// ============================================================================
|
|
34
|
+
/**
|
|
35
|
+
* Query procedure naming prefixes
|
|
36
|
+
* Procedures starting with these prefixes are treated as queries.
|
|
37
|
+
*/
|
|
38
|
+
const QUERY_PREFIXES = ['get', 'list', 'find'];
|
|
39
|
+
/**
|
|
40
|
+
* Mutation procedure naming prefixes
|
|
41
|
+
* Procedures starting with these prefixes are treated as mutations.
|
|
42
|
+
*/
|
|
43
|
+
const MUTATION_PREFIXES = ['create', 'add', 'update', 'edit', 'patch', 'delete', 'remove'];
|
|
44
|
+
/**
|
|
45
|
+
* Map of common alternative prefixes to their standard equivalents
|
|
46
|
+
* Used to provide helpful suggestions when non-standard names are detected.
|
|
47
|
+
*/
|
|
48
|
+
const SIMILAR_PATTERNS = {
|
|
49
|
+
fetch: { type: 'query', suggest: 'list or get' },
|
|
50
|
+
retrieve: { type: 'query', suggest: 'get' },
|
|
51
|
+
obtain: { type: 'query', suggest: 'get' },
|
|
52
|
+
load: { type: 'query', suggest: 'list or get' },
|
|
53
|
+
read: { type: 'query', suggest: 'get' },
|
|
54
|
+
query: { type: 'query', suggest: 'list, get, or find' },
|
|
55
|
+
search: { type: 'query', suggest: 'find' },
|
|
56
|
+
new: { type: 'mutation', suggest: 'create' },
|
|
57
|
+
insert: { type: 'mutation', suggest: 'create' },
|
|
58
|
+
make: { type: 'mutation', suggest: 'create' },
|
|
59
|
+
modify: { type: 'mutation', suggest: 'update or patch' },
|
|
60
|
+
change: { type: 'mutation', suggest: 'update or patch' },
|
|
61
|
+
set: { type: 'mutation', suggest: 'update' },
|
|
62
|
+
destroy: { type: 'mutation', suggest: 'delete' },
|
|
63
|
+
drop: { type: 'mutation', suggest: 'delete' },
|
|
64
|
+
erase: { type: 'mutation', suggest: 'delete' },
|
|
65
|
+
trash: { type: 'mutation', suggest: 'delete' },
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* Detects if a procedure name uses a similar (non-standard) pattern
|
|
69
|
+
* @returns The similar pattern info if found, null otherwise
|
|
70
|
+
*/
|
|
71
|
+
function detectSimilarPattern(procedureName) {
|
|
72
|
+
for (const [prefix, info] of Object.entries(SIMILAR_PATTERNS)) {
|
|
73
|
+
if (procedureName.toLowerCase().startsWith(prefix)) {
|
|
74
|
+
return { prefix, ...info };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
34
79
|
/**
|
|
35
80
|
* Determines if a procedure is a query based on naming convention
|
|
36
81
|
*
|
|
@@ -46,8 +91,86 @@ import { buildQueryKey } from './utils.js';
|
|
|
46
91
|
* @returns true if the procedure is a query, false for mutation
|
|
47
92
|
*/
|
|
48
93
|
function isQueryProcedure(procedureName) {
|
|
49
|
-
|
|
50
|
-
|
|
94
|
+
return QUERY_PREFIXES.some((prefix) => procedureName.startsWith(prefix));
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Extracts the entity name from a procedure name by stripping common prefixes
|
|
98
|
+
* @example extractEntityName('fetchUsers') returns 'Users'
|
|
99
|
+
*/
|
|
100
|
+
function extractEntityName(procedureName) {
|
|
101
|
+
// Check for standard prefixes first
|
|
102
|
+
for (const prefix of [...QUERY_PREFIXES, ...MUTATION_PREFIXES]) {
|
|
103
|
+
if (procedureName.startsWith(prefix)) {
|
|
104
|
+
return procedureName.slice(prefix.length);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// Check for similar patterns
|
|
108
|
+
for (const prefix of Object.keys(SIMILAR_PATTERNS)) {
|
|
109
|
+
if (procedureName.toLowerCase().startsWith(prefix)) {
|
|
110
|
+
return procedureName.slice(prefix.length);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// Capitalize first letter for generic case
|
|
114
|
+
return procedureName.charAt(0).toUpperCase() + procedureName.slice(1);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Creates a helpful error message for naming convention violations
|
|
118
|
+
*
|
|
119
|
+
* Provides context-aware suggestions based on:
|
|
120
|
+
* - Whether the procedure uses a similar (non-standard) pattern like 'fetch'
|
|
121
|
+
* - What the developer likely intended based on the method they called
|
|
122
|
+
*
|
|
123
|
+
* @param procedureName - The procedure name that was accessed incorrectly
|
|
124
|
+
* @param attemptedMethod - The method that was called (e.g., 'useQuery', 'useMutation')
|
|
125
|
+
* @param isQuery - Whether the procedure is detected as a query
|
|
126
|
+
*/
|
|
127
|
+
function createNamingConventionError(procedureName, attemptedMethod, isQuery) {
|
|
128
|
+
const queryMethods = 'useQuery, useSuspenseQuery, getQueryKey, invalidate, prefetch, setData, getData';
|
|
129
|
+
const mutationMethods = 'useMutation';
|
|
130
|
+
const similarPattern = detectSimilarPattern(procedureName);
|
|
131
|
+
const entityName = extractEntityName(procedureName);
|
|
132
|
+
if (isQuery) {
|
|
133
|
+
// Called mutation method on a query procedure
|
|
134
|
+
const suggestions = MUTATION_PREFIXES.slice(0, 3).map((p) => `${p}${entityName}`);
|
|
135
|
+
return new Error(`Cannot call ${attemptedMethod}() on query procedure "${procedureName}".\n\n` +
|
|
136
|
+
`VeloxTS Naming Convention:\n` +
|
|
137
|
+
` • Query procedures must start with: ${QUERY_PREFIXES.join(', ')}\n` +
|
|
138
|
+
` • Mutation procedures must start with: ${MUTATION_PREFIXES.join(', ')}\n\n` +
|
|
139
|
+
`"${procedureName}" starts with a query prefix, so only these methods are available:\n` +
|
|
140
|
+
` ${queryMethods}\n\n` +
|
|
141
|
+
`If this should be a mutation, rename it to one of:\n` +
|
|
142
|
+
` ${suggestions.join(', ')}`);
|
|
143
|
+
}
|
|
144
|
+
// Called query method on a mutation procedure
|
|
145
|
+
// Check if using a similar pattern (e.g., 'fetchUsers' instead of 'listUsers')
|
|
146
|
+
if (similarPattern) {
|
|
147
|
+
const querySuggestions = similarPattern.suggest
|
|
148
|
+
.split(/,?\s+or\s+|,\s*/)
|
|
149
|
+
.map((s) => s.trim())
|
|
150
|
+
.filter(Boolean)
|
|
151
|
+
.map((prefix) => `${prefix}${entityName}`);
|
|
152
|
+
return new Error(`Cannot call ${attemptedMethod}() on procedure "${procedureName}".\n\n` +
|
|
153
|
+
`The prefix "${similarPattern.prefix}" is not a standard VeloxTS naming convention.\n\n` +
|
|
154
|
+
`Did you mean to use "${similarPattern.suggest}" instead?\n` +
|
|
155
|
+
` Suggested name${querySuggestions.length > 1 ? 's' : ''}: ${querySuggestions.join(', ')}\n\n` +
|
|
156
|
+
`VeloxTS Naming Convention:\n` +
|
|
157
|
+
` • Query procedures: ${QUERY_PREFIXES.join(', ')}\n` +
|
|
158
|
+
` • Mutation procedures: ${MUTATION_PREFIXES.join(', ')}\n\n` +
|
|
159
|
+
`This matters because procedure names determine REST routes:\n` +
|
|
160
|
+
` • "listUsers" → GET /users (collection)\n` +
|
|
161
|
+
` • "getUser" → GET /users/:id (single resource)\n` +
|
|
162
|
+
` • "fetchUsers" → POST /users (treated as mutation!)`);
|
|
163
|
+
}
|
|
164
|
+
// Generic mutation procedure, no similar pattern detected
|
|
165
|
+
const querySuggestions = QUERY_PREFIXES.map((p) => `${p}${entityName}`);
|
|
166
|
+
return new Error(`Cannot call ${attemptedMethod}() on mutation procedure "${procedureName}".\n\n` +
|
|
167
|
+
`VeloxTS Naming Convention:\n` +
|
|
168
|
+
` • Query procedures must start with: ${QUERY_PREFIXES.join(', ')}\n` +
|
|
169
|
+
` • Mutation procedures must start with: ${MUTATION_PREFIXES.join(', ')}\n\n` +
|
|
170
|
+
`"${procedureName}" does not start with a query prefix, so only these methods are available:\n` +
|
|
171
|
+
` ${mutationMethods}\n\n` +
|
|
172
|
+
`If this should be a query, rename it to one of:\n` +
|
|
173
|
+
` ${querySuggestions.join(', ')}`);
|
|
51
174
|
}
|
|
52
175
|
/**
|
|
53
176
|
* Determines the mutation type from procedure name
|
|
@@ -170,7 +293,8 @@ async function performAutoInvalidation(queryClient, namespace, procedureName, in
|
|
|
170
293
|
* @param getClient - Factory function to get the client (called inside hooks)
|
|
171
294
|
*/
|
|
172
295
|
function createQueryProcedureProxy(namespace, procedureName, getClient) {
|
|
173
|
-
|
|
296
|
+
// Add useMutation stub that throws helpful error
|
|
297
|
+
const proxy = {
|
|
174
298
|
useQuery(...args) {
|
|
175
299
|
const [input, options] = args;
|
|
176
300
|
const client = getClient();
|
|
@@ -229,7 +353,12 @@ function createQueryProcedureProxy(namespace, procedureName, getClient) {
|
|
|
229
353
|
const queryKey = buildQueryKey(namespace, procedureName, input);
|
|
230
354
|
return queryClient.getQueryData(queryKey);
|
|
231
355
|
},
|
|
356
|
+
// Stub that throws helpful error when called on a query procedure
|
|
357
|
+
useMutation() {
|
|
358
|
+
throw createNamingConventionError(procedureName, 'useMutation', true);
|
|
359
|
+
},
|
|
232
360
|
};
|
|
361
|
+
return proxy;
|
|
233
362
|
}
|
|
234
363
|
/**
|
|
235
364
|
* Creates a mutation procedure proxy with hook methods and auto-invalidation
|
|
@@ -244,13 +373,13 @@ function createQueryProcedureProxy(namespace, procedureName, getClient) {
|
|
|
244
373
|
* @param getClient - Factory function to get the client (called inside hooks)
|
|
245
374
|
*/
|
|
246
375
|
function createMutationProcedureProxy(namespace, procedureName, getClient) {
|
|
247
|
-
|
|
376
|
+
// Add query method stubs that throw helpful errors
|
|
377
|
+
const proxy = {
|
|
248
378
|
useMutation(options) {
|
|
249
379
|
const client = getClient();
|
|
250
380
|
const queryClient = useReactQueryClient();
|
|
251
381
|
// Extract auto-invalidation configuration
|
|
252
|
-
const
|
|
253
|
-
const autoInvalidateOption = typedOptions?.autoInvalidate;
|
|
382
|
+
const autoInvalidateOption = options?.autoInvalidate;
|
|
254
383
|
const autoInvalidateEnabled = autoInvalidateOption !== false;
|
|
255
384
|
const autoInvalidateConfig = typeof autoInvalidateOption === 'object' ? autoInvalidateOption : undefined;
|
|
256
385
|
// Store original onSuccess to call after auto-invalidation
|
|
@@ -274,11 +403,58 @@ function createMutationProcedureProxy(namespace, procedureName, getClient) {
|
|
|
274
403
|
},
|
|
275
404
|
});
|
|
276
405
|
},
|
|
406
|
+
// Stubs that throw helpful errors when called on a mutation procedure
|
|
407
|
+
useQuery() {
|
|
408
|
+
throw createNamingConventionError(procedureName, 'useQuery', false);
|
|
409
|
+
},
|
|
410
|
+
useSuspenseQuery() {
|
|
411
|
+
throw createNamingConventionError(procedureName, 'useSuspenseQuery', false);
|
|
412
|
+
},
|
|
413
|
+
getQueryKey() {
|
|
414
|
+
throw createNamingConventionError(procedureName, 'getQueryKey', false);
|
|
415
|
+
},
|
|
416
|
+
invalidate() {
|
|
417
|
+
throw createNamingConventionError(procedureName, 'invalidate', false);
|
|
418
|
+
},
|
|
419
|
+
prefetch() {
|
|
420
|
+
throw createNamingConventionError(procedureName, 'prefetch', false);
|
|
421
|
+
},
|
|
422
|
+
setData() {
|
|
423
|
+
throw createNamingConventionError(procedureName, 'setData', false);
|
|
424
|
+
},
|
|
425
|
+
getData() {
|
|
426
|
+
throw createNamingConventionError(procedureName, 'getData', false);
|
|
427
|
+
},
|
|
277
428
|
};
|
|
429
|
+
return proxy;
|
|
278
430
|
}
|
|
279
431
|
// ============================================================================
|
|
280
432
|
// Namespace Proxy
|
|
281
433
|
// ============================================================================
|
|
434
|
+
/**
|
|
435
|
+
* Determines procedure type using routes metadata or naming convention
|
|
436
|
+
*
|
|
437
|
+
* Priority:
|
|
438
|
+
* 1. If routes has explicit `kind` for this procedure, use it
|
|
439
|
+
* 2. Otherwise, fall back to naming convention heuristic
|
|
440
|
+
*
|
|
441
|
+
* @param namespace - The procedure namespace
|
|
442
|
+
* @param procedureName - The procedure name
|
|
443
|
+
* @param routes - Optional route metadata from backend
|
|
444
|
+
* @returns true if the procedure is a query, false for mutation
|
|
445
|
+
*/
|
|
446
|
+
function isProcedureQuery(namespace, procedureName, routes) {
|
|
447
|
+
// Check routes for explicit kind first
|
|
448
|
+
const routeEntry = routes?.[namespace]?.[procedureName];
|
|
449
|
+
if (routeEntry) {
|
|
450
|
+
// RouteEntry can be object with kind, or legacy string (path only)
|
|
451
|
+
if (typeof routeEntry === 'object' && 'kind' in routeEntry) {
|
|
452
|
+
return routeEntry.kind === 'query';
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
// Fall back to naming convention
|
|
456
|
+
return isQueryProcedure(procedureName);
|
|
457
|
+
}
|
|
282
458
|
/**
|
|
283
459
|
* Creates a proxy for a namespace that returns procedure proxies
|
|
284
460
|
*
|
|
@@ -287,8 +463,9 @@ function createMutationProcedureProxy(namespace, procedureName, getClient) {
|
|
|
287
463
|
*
|
|
288
464
|
* @param namespace - The namespace name (e.g., 'users')
|
|
289
465
|
* @param getClient - Factory function to get the client
|
|
466
|
+
* @param routes - Optional route metadata for explicit kind detection
|
|
290
467
|
*/
|
|
291
|
-
function createNamespaceProxy(namespace, getClient) {
|
|
468
|
+
function createNamespaceProxy(namespace, getClient, routes) {
|
|
292
469
|
// Cache procedure proxies to avoid recreating on every access
|
|
293
470
|
const procedureCache = new Map();
|
|
294
471
|
return new Proxy({}, {
|
|
@@ -298,8 +475,8 @@ function createNamespaceProxy(namespace, getClient) {
|
|
|
298
475
|
if (cached) {
|
|
299
476
|
return cached;
|
|
300
477
|
}
|
|
301
|
-
// Create new procedure proxy based on naming convention
|
|
302
|
-
const procedureProxy =
|
|
478
|
+
// Create new procedure proxy based on routes metadata or naming convention
|
|
479
|
+
const procedureProxy = isProcedureQuery(namespace, procedureName, routes)
|
|
303
480
|
? createQueryProcedureProxy(namespace, procedureName, getClient)
|
|
304
481
|
: createMutationProcedureProxy(namespace, procedureName, getClient);
|
|
305
482
|
// Cache for future access
|
|
@@ -380,6 +557,8 @@ export function createVeloxHooks(config) {
|
|
|
380
557
|
const { client: contextClient } = useVeloxContext();
|
|
381
558
|
return config?.client ?? contextClient;
|
|
382
559
|
};
|
|
560
|
+
// Extract routes for explicit kind detection
|
|
561
|
+
const routes = config?.routes;
|
|
383
562
|
// Create the root proxy
|
|
384
563
|
return new Proxy({}, {
|
|
385
564
|
get(_target, namespace) {
|
|
@@ -388,8 +567,8 @@ export function createVeloxHooks(config) {
|
|
|
388
567
|
if (cached) {
|
|
389
568
|
return cached;
|
|
390
569
|
}
|
|
391
|
-
// Create new namespace proxy
|
|
392
|
-
const namespaceProxy = createNamespaceProxy(namespace, getClient);
|
|
570
|
+
// Create new namespace proxy with routes for kind detection
|
|
571
|
+
const namespaceProxy = createNamespaceProxy(namespace, getClient, routes);
|
|
393
572
|
// Cache for future access
|
|
394
573
|
namespaceCache.set(namespace, namespaceProxy);
|
|
395
574
|
return namespaceProxy;
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* @module @veloxts/client/react/proxy-types
|
|
8
8
|
*/
|
|
9
9
|
import type { QueryClient, UseMutationOptions, UseMutationResult, UseQueryOptions, UseQueryResult, UseSuspenseQueryOptions, UseSuspenseQueryResult } from '@tanstack/react-query';
|
|
10
|
-
import type { ClientFromRouter, ClientProcedure, ProcedureCollection, ProcedureRecord } from '../types.js';
|
|
10
|
+
import type { ClientFromRouter, ClientProcedure, ProcedureCollection, ProcedureRecord, RouteMap } from '../types.js';
|
|
11
11
|
import type { VeloxQueryKey } from './types.js';
|
|
12
12
|
/**
|
|
13
13
|
* Hook methods available for query procedures
|
|
@@ -385,6 +385,32 @@ export interface VeloxHooksConfig<TRouter = unknown> {
|
|
|
385
385
|
* ```
|
|
386
386
|
*/
|
|
387
387
|
client?: ClientFromRouter<TRouter>;
|
|
388
|
+
/**
|
|
389
|
+
* Optional: route metadata from backend
|
|
390
|
+
*
|
|
391
|
+
* When provided, the `kind` field in route entries overrides the naming
|
|
392
|
+
* convention heuristic for determining query vs mutation. This is useful
|
|
393
|
+
* for procedures that don't follow naming conventions.
|
|
394
|
+
*
|
|
395
|
+
* Generate this using `extractRoutes()` from `@veloxts/router`:
|
|
396
|
+
*
|
|
397
|
+
* @example
|
|
398
|
+
* ```typescript
|
|
399
|
+
* // Backend: api/index.ts
|
|
400
|
+
* import { extractRoutes } from '@veloxts/router';
|
|
401
|
+
* export const routes = extractRoutes([userProcedures, authProcedures]);
|
|
402
|
+
*
|
|
403
|
+
* // Frontend: api.ts
|
|
404
|
+
* import { createVeloxHooks } from '@veloxts/client/react';
|
|
405
|
+
* import { routes } from '../../api/src/index.js';
|
|
406
|
+
*
|
|
407
|
+
* export const api = createVeloxHooks<AppRouter>({ routes });
|
|
408
|
+
*
|
|
409
|
+
* // Now non-conventional procedures work correctly:
|
|
410
|
+
* api.health.check.useQuery({}); // Works even though 'check' is not a query prefix
|
|
411
|
+
* ```
|
|
412
|
+
*/
|
|
413
|
+
routes?: RouteMap;
|
|
388
414
|
}
|
|
389
415
|
/**
|
|
390
416
|
* Generic client type for internal use
|
package/dist/types.d.ts
CHANGED
|
@@ -119,11 +119,25 @@ export type ClientFromRouter<TRouter> = {
|
|
|
119
119
|
[K in keyof TRouter]: TRouter[K] extends ProcedureCollection ? ClientFromCollection<TRouter[K]> : never;
|
|
120
120
|
};
|
|
121
121
|
/**
|
|
122
|
-
* A single route entry with method and
|
|
122
|
+
* A single route entry with method, path, and procedure kind
|
|
123
|
+
*
|
|
124
|
+
* The `kind` field enables explicit query/mutation type annotation,
|
|
125
|
+
* overriding the naming convention heuristic when procedures don't
|
|
126
|
+
* follow standard prefixes (get*, list*, create*, etc.).
|
|
123
127
|
*/
|
|
124
128
|
export interface RouteEntry {
|
|
125
129
|
method: HttpMethod;
|
|
126
130
|
path: string;
|
|
131
|
+
/**
|
|
132
|
+
* Explicit procedure type annotation
|
|
133
|
+
*
|
|
134
|
+
* When provided, this overrides the naming convention detection:
|
|
135
|
+
* - `'query'` → enables useQuery, useSuspenseQuery, getQueryKey, etc.
|
|
136
|
+
* - `'mutation'` → enables useMutation
|
|
137
|
+
*
|
|
138
|
+
* Use when procedure names don't follow conventions (e.g., `fetchUsers`, `process`)
|
|
139
|
+
*/
|
|
140
|
+
kind?: 'query' | 'mutation';
|
|
127
141
|
}
|
|
128
142
|
/**
|
|
129
143
|
* Maps procedure names to their REST endpoint configuration.
|