@veloxts/client 0.6.63 → 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 CHANGED
@@ -1,5 +1,17 @@
1
1
  # @veloxts/client
2
2
 
3
+ ## 0.6.65
4
+
5
+ ### Patch Changes
6
+
7
+ - improve ai integration and simplify api router definition
8
+
9
+ ## 0.6.64
10
+
11
+ ### Patch Changes
12
+
13
+ - fix(create): add @veloxts/velox and @veloxts/mcp to root package.json
14
+
3
15
  ## 0.6.63
4
16
 
5
17
  ### Patch Changes
@@ -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
- const queryPrefixes = ['get', 'list', 'find'];
50
- return queryPrefixes.some((prefix) => procedureName.startsWith(prefix));
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
- return {
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
- return {
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 typedOptions = options;
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 = isQueryProcedure(procedureName)
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 path
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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@veloxts/client",
3
- "version": "0.6.63",
3
+ "version": "0.6.65",
4
4
  "description": "Type-safe frontend API client for VeloxTS framework",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",