@pattern-stack/frontend-patterns 0.2.0-alpha.1 → 0.2.0-alpha.11

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.
Files changed (165) hide show
  1. package/dist/atoms/components/core/Badge/Badge.d.ts +1 -1
  2. package/dist/atoms/components/data/DataTable/ColumnFilterDropdown.d.ts +32 -0
  3. package/dist/atoms/components/data/DataTable/ColumnFilterDropdown.d.ts.map +1 -0
  4. package/dist/atoms/components/data/DataTable/ColumnVisibilityToggle.d.ts +32 -0
  5. package/dist/atoms/components/data/DataTable/ColumnVisibilityToggle.d.ts.map +1 -0
  6. package/dist/atoms/components/data/DataTable/DataTable.d.ts +5 -2
  7. package/dist/atoms/components/data/DataTable/DataTable.d.ts.map +1 -1
  8. package/dist/atoms/components/data/DataTable/DataTable.expansion.d.ts +91 -0
  9. package/dist/atoms/components/data/DataTable/DataTable.expansion.d.ts.map +1 -0
  10. package/dist/atoms/components/data/DataTable/DataTable.filters.d.ts +271 -0
  11. package/dist/atoms/components/data/DataTable/DataTable.filters.d.ts.map +1 -0
  12. package/dist/atoms/components/data/DataTable/DataTable.types.d.ts +155 -5
  13. package/dist/atoms/components/data/DataTable/DataTable.types.d.ts.map +1 -1
  14. package/dist/atoms/components/data/DataTable/ExpandButton.d.ts +37 -0
  15. package/dist/atoms/components/data/DataTable/ExpandButton.d.ts.map +1 -0
  16. package/dist/atoms/components/data/DataTable/FilterPill.d.ts +25 -0
  17. package/dist/atoms/components/data/DataTable/FilterPill.d.ts.map +1 -0
  18. package/dist/atoms/components/data/DataTable/QuickFilterBar.d.ts +35 -0
  19. package/dist/atoms/components/data/DataTable/QuickFilterBar.d.ts.map +1 -0
  20. package/dist/atoms/components/data/DataTable/filters/BooleanFilterEditor.d.ts +10 -0
  21. package/dist/atoms/components/data/DataTable/filters/BooleanFilterEditor.d.ts.map +1 -0
  22. package/dist/atoms/components/data/DataTable/filters/DateFilterEditor.d.ts +11 -0
  23. package/dist/atoms/components/data/DataTable/filters/DateFilterEditor.d.ts.map +1 -0
  24. package/dist/atoms/components/data/DataTable/filters/MultiSelectFilterEditor.d.ts +10 -0
  25. package/dist/atoms/components/data/DataTable/filters/MultiSelectFilterEditor.d.ts.map +1 -0
  26. package/dist/atoms/components/data/DataTable/filters/NumberFilterEditor.d.ts +10 -0
  27. package/dist/atoms/components/data/DataTable/filters/NumberFilterEditor.d.ts.map +1 -0
  28. package/dist/atoms/components/data/DataTable/filters/SelectFilterEditor.d.ts +10 -0
  29. package/dist/atoms/components/data/DataTable/filters/SelectFilterEditor.d.ts.map +1 -0
  30. package/dist/atoms/components/data/DataTable/filters/TextFilterEditor.d.ts +10 -0
  31. package/dist/atoms/components/data/DataTable/filters/TextFilterEditor.d.ts.map +1 -0
  32. package/dist/atoms/components/data/DataTable/filters/index.d.ts +14 -0
  33. package/dist/atoms/components/data/DataTable/filters/index.d.ts.map +1 -0
  34. package/dist/atoms/components/data/DataTable/index.d.ts +9 -0
  35. package/dist/atoms/components/data/DataTable/index.d.ts.map +1 -1
  36. package/dist/atoms/components/data/ProgressBar/ProgressBar.d.ts +1 -1
  37. package/dist/atoms/components/data/ProgressBar/ProgressBar.d.ts.map +1 -1
  38. package/dist/atoms/components/data/index.d.ts +3 -2
  39. package/dist/atoms/components/data/index.d.ts.map +1 -1
  40. package/dist/atoms/composed/ConnectionStatus/ConnectionStatus.d.ts +16 -0
  41. package/dist/atoms/composed/ConnectionStatus/ConnectionStatus.d.ts.map +1 -0
  42. package/dist/atoms/composed/ConnectionStatus/index.d.ts +3 -0
  43. package/dist/atoms/composed/ConnectionStatus/index.d.ts.map +1 -0
  44. package/dist/atoms/hooks/index.d.ts +9 -0
  45. package/dist/atoms/hooks/index.d.ts.map +1 -1
  46. package/dist/atoms/hooks/useAdaptiveTable.d.ts +49 -0
  47. package/dist/atoms/hooks/useAdaptiveTable.d.ts.map +1 -0
  48. package/dist/atoms/hooks/useApi.d.ts +1 -1
  49. package/dist/atoms/hooks/useApi.d.ts.map +1 -1
  50. package/dist/atoms/hooks/useColumnVisibility.d.ts +75 -0
  51. package/dist/atoms/hooks/useColumnVisibility.d.ts.map +1 -0
  52. package/dist/atoms/hooks/useEntityData.d.ts +36 -0
  53. package/dist/atoms/hooks/useEntityData.d.ts.map +1 -0
  54. package/dist/atoms/hooks/useEntityDetail.d.ts +43 -0
  55. package/dist/atoms/hooks/useEntityDetail.d.ts.map +1 -0
  56. package/dist/atoms/hooks/useExpandedRows.d.ts +66 -0
  57. package/dist/atoms/hooks/useExpandedRows.d.ts.map +1 -0
  58. package/dist/atoms/hooks/useFieldMetadata.d.ts +18 -0
  59. package/dist/atoms/hooks/useFieldMetadata.d.ts.map +1 -0
  60. package/dist/atoms/hooks/useOnlineStatus.d.ts +16 -0
  61. package/dist/atoms/hooks/useOnlineStatus.d.ts.map +1 -0
  62. package/dist/atoms/hooks/useResponsiveTable.d.ts +123 -0
  63. package/dist/atoms/hooks/useResponsiveTable.d.ts.map +1 -0
  64. package/dist/atoms/hooks/useTableFilters.d.ts +92 -0
  65. package/dist/atoms/hooks/useTableFilters.d.ts.map +1 -0
  66. package/dist/atoms/index.d.ts +1 -0
  67. package/dist/atoms/index.d.ts.map +1 -1
  68. package/dist/atoms/primitives/sheet.d.ts +23 -0
  69. package/dist/atoms/primitives/sheet.d.ts.map +1 -0
  70. package/dist/atoms/primitives/table.d.ts.map +1 -1
  71. package/dist/atoms/services/api/client.d.ts +12 -2
  72. package/dist/atoms/services/api/client.d.ts.map +1 -1
  73. package/dist/atoms/services/auth-service.d.ts +15 -0
  74. package/dist/atoms/services/auth-service.d.ts.map +1 -1
  75. package/dist/atoms/services/index.d.ts +2 -2
  76. package/dist/atoms/services/index.d.ts.map +1 -1
  77. package/dist/atoms/shared/config/table-config.d.ts +79 -0
  78. package/dist/atoms/shared/config/table-config.d.ts.map +1 -0
  79. package/dist/atoms/shared/index.d.ts +1 -0
  80. package/dist/atoms/shared/index.d.ts.map +1 -1
  81. package/dist/atoms/types/auth.d.ts +95 -2
  82. package/dist/atoms/types/auth.d.ts.map +1 -1
  83. package/dist/atoms/types/index.d.ts +1 -0
  84. package/dist/atoms/types/index.d.ts.map +1 -1
  85. package/dist/atoms/types/navigation.d.ts +1 -1
  86. package/dist/atoms/types/navigation.d.ts.map +1 -1
  87. package/dist/atoms/types/ui-config.d.ts +46 -11
  88. package/dist/atoms/types/ui-config.d.ts.map +1 -1
  89. package/dist/atoms/types/ui-metadata.d.ts +103 -0
  90. package/dist/atoms/types/ui-metadata.d.ts.map +1 -0
  91. package/dist/atoms/utils/entity-card-mapping.d.ts +105 -0
  92. package/dist/atoms/utils/entity-card-mapping.d.ts.map +1 -0
  93. package/dist/atoms/utils/field-detection.d.ts +2 -2
  94. package/dist/atoms/utils/field-detection.d.ts.map +1 -1
  95. package/dist/atoms/utils/icon-map.d.ts +48 -0
  96. package/dist/atoms/utils/icon-map.d.ts.map +1 -1
  97. package/dist/atoms/utils/index.d.ts +2 -0
  98. package/dist/atoms/utils/index.d.ts.map +1 -1
  99. package/dist/atoms/utils/ui-mapping.d.ts +9 -3
  100. package/dist/atoms/utils/ui-mapping.d.ts.map +1 -1
  101. package/dist/features/auth/components/ProtectedRoute.d.ts +3 -1
  102. package/dist/features/auth/components/ProtectedRoute.d.ts.map +1 -1
  103. package/dist/features/auth/hooks/useAuth.d.ts.map +1 -1
  104. package/dist/features/auth/providers/NoAuthProvider.d.ts +17 -0
  105. package/dist/features/auth/providers/NoAuthProvider.d.ts.map +1 -0
  106. package/dist/features/auth/providers/index.d.ts +1 -0
  107. package/dist/features/auth/providers/index.d.ts.map +1 -1
  108. package/dist/frontend-patterns.css +1 -4554
  109. package/dist/index.d.ts +12 -4
  110. package/dist/index.d.ts.map +1 -1
  111. package/dist/index.es.js +8793 -18275
  112. package/dist/index.es.js.map +1 -1
  113. package/dist/index.js +8790 -18271
  114. package/dist/index.js.map +1 -1
  115. package/dist/molecules/layout/AppHeader/AppHeader.d.ts.map +1 -1
  116. package/dist/molecules/layout/BulkSelectionBar.d.ts +14 -2
  117. package/dist/molecules/layout/BulkSelectionBar.d.ts.map +1 -1
  118. package/dist/molecules/layout/FieldGrid/FieldGrid.d.ts +61 -0
  119. package/dist/molecules/layout/FieldGrid/FieldGrid.d.ts.map +1 -0
  120. package/dist/molecules/layout/FieldGrid/index.d.ts +2 -0
  121. package/dist/molecules/layout/FieldGrid/index.d.ts.map +1 -0
  122. package/dist/molecules/layout/ListToolbar/ListToolbar.d.ts +37 -0
  123. package/dist/molecules/layout/ListToolbar/ListToolbar.d.ts.map +1 -0
  124. package/dist/molecules/layout/ListToolbar/index.d.ts +2 -0
  125. package/dist/molecules/layout/ListToolbar/index.d.ts.map +1 -0
  126. package/dist/molecules/layout/PageTitle/PageTitle.d.ts +17 -0
  127. package/dist/molecules/layout/PageTitle/PageTitle.d.ts.map +1 -0
  128. package/dist/molecules/layout/PageTitle/index.d.ts +2 -0
  129. package/dist/molecules/layout/PageTitle/index.d.ts.map +1 -0
  130. package/dist/molecules/layout/index.d.ts +3 -0
  131. package/dist/molecules/layout/index.d.ts.map +1 -1
  132. package/dist/molecules/layout/navigation-context.d.ts.map +1 -1
  133. package/dist/sync/EntityStoreProvider.d.ts +35 -0
  134. package/dist/sync/EntityStoreProvider.d.ts.map +1 -0
  135. package/dist/sync/createEntityHooks.d.ts +29 -0
  136. package/dist/sync/createEntityHooks.d.ts.map +1 -0
  137. package/dist/sync/createStore.d.ts +65 -0
  138. package/dist/sync/createStore.d.ts.map +1 -0
  139. package/dist/sync/index.d.ts +6 -0
  140. package/dist/sync/index.d.ts.map +1 -0
  141. package/dist/sync/types.d.ts +383 -0
  142. package/dist/sync/types.d.ts.map +1 -0
  143. package/dist/templates/ListPageTemplate.d.ts +21 -0
  144. package/dist/templates/ListPageTemplate.d.ts.map +1 -0
  145. package/dist/templates/admin/AdminCRUDTemplate.d.ts.map +1 -1
  146. package/dist/templates/factory.d.ts +11 -0
  147. package/dist/templates/factory.d.ts.map +1 -1
  148. package/dist/templates/index.d.ts +1 -0
  149. package/dist/templates/index.d.ts.map +1 -1
  150. package/package.json +11 -7
  151. package/cli/commands/generate-hooks.ts +0 -325
  152. package/cli/commands/init.ts +0 -33
  153. package/cli/commands/scaffold.ts +0 -224
  154. package/cli/index.ts +0 -122
  155. package/cli/src/codegen/openapi/__tests__/naming-utils.test.js +0 -367
  156. package/cli/src/codegen/openapi/client-generator.js +0 -727
  157. package/cli/src/codegen/openapi/confidence-scorer.js +0 -93
  158. package/cli/src/codegen/openapi/hook-config.js +0 -48
  159. package/cli/src/codegen/openapi/hook-generator.js +0 -763
  160. package/cli/src/codegen/openapi/naming-constants.js +0 -98
  161. package/cli/src/codegen/openapi/naming-utils.js +0 -149
  162. package/cli/src/codegen/openapi/parser.js +0 -274
  163. package/cli/src/codegen/openapi/type-generator.js +0 -329
  164. package/dist/codegen/openapi/bulk-types.d.ts +0 -142
  165. package/dist/codegen/openapi/bulk-types.d.ts.map +0 -1
@@ -1,763 +0,0 @@
1
- /**
2
- * React Hook Generator
3
- *
4
- * Generates React hooks with TanStack Query integration for API endpoints
5
- * with proper TypeScript typing and error/loading states.
6
- *
7
- * Part of FRO-3: React Hook Generator
8
- */
9
- import { HookConfigManager } from './hook-config.js';
10
- import { ConfidenceScorer } from './confidence-scorer.js';
11
- import { singularize, pluralize, isPlural } from './naming-utils.js';
12
- import { AUTH_ACTIONS, HEALTH_ENDPOINTS, USER_PROFILE_ENDPOINTS, SINGLETON_RESOURCES, COLLECTION_ACTIONS, SPECIAL_ACTIONS } from './naming-constants.js';
13
- export class ReactHookGenerator {
14
- options;
15
- configManager;
16
- confidenceScorer;
17
- scoredNames = [];
18
- constructor(options = {}) {
19
- this.options = {
20
- queryKeyPrefix: options.queryKeyPrefix || 'api',
21
- includeInfiniteQueries: options.includeInfiniteQueries !== false,
22
- includeOptimisticUpdates: options.includeOptimisticUpdates !== false,
23
- includeMutationHelpers: options.includeMutationHelpers ?? true,
24
- authenticationRequired: options.authenticationRequired ?? true,
25
- errorHandling: options.errorHandling || 'throw',
26
- configPath: options.configPath || './hooks.config.json',
27
- enableConfidenceScoring: options.enableConfidenceScoring ?? true
28
- };
29
- this.configManager = new HookConfigManager(this.options.configPath);
30
- this.confidenceScorer = new ConfidenceScorer();
31
- }
32
- async generate(parsedAPI) {
33
- const endpoints = parsedAPI.endpoints;
34
- // Load configuration
35
- await this.configManager.load();
36
- // Reset scored names for this generation
37
- this.scoredNames = [];
38
- const result = {
39
- queries: this.generateQueryHooks(endpoints),
40
- mutations: this.generateMutationHooks(endpoints),
41
- keys: this.generateQueryKeys(endpoints),
42
- types: this.generateHookTypes(endpoints),
43
- index: this.generateIndexFile(),
44
- report: undefined
45
- };
46
- // Generate confidence report if scoring is enabled
47
- if (this.options.enableConfidenceScoring && this.scoredNames.length > 0) {
48
- console.log(`Generated ${this.scoredNames.length} scored hook names`);
49
- result.report = this.confidenceScorer.generateReport(this.scoredNames);
50
- // Update statistics
51
- const stats = {
52
- totalGenerated: this.scoredNames.length,
53
- highConfidence: this.scoredNames.filter(s => s.confidence === 'high').length,
54
- mediumConfidence: this.scoredNames.filter(s => s.confidence === 'medium').length,
55
- lowConfidence: this.scoredNames.filter(s => s.confidence === 'low').length,
56
- humanReviewed: this.configManager.getReviewedNames().length
57
- };
58
- this.configManager.updateStatistics(stats);
59
- // Save updated configuration
60
- await this.configManager.save();
61
- }
62
- return result;
63
- }
64
- generateQueryHooks(endpoints) {
65
- const hooks = [];
66
- hooks.push(this.generateFileHeader('Query Hooks'));
67
- hooks.push('');
68
- hooks.push("import { useQuery, useInfiniteQuery, QueryOptions, InfiniteQueryOptions } from '@tanstack/react-query'");
69
- hooks.push("import { apiClient } from '../client'");
70
- hooks.push("import { queryKeys } from './keys'");
71
- hooks.push("import * as Types from './types'");
72
- hooks.push('');
73
- // Filter GET endpoints for queries
74
- const queryEndpoints = endpoints.filter(endpoint => endpoint.method === 'get');
75
- for (const endpoint of queryEndpoints) {
76
- const hookName = this.generateQueryHookName(endpoint);
77
- const hook = this.generateQueryHook(endpoint, hookName);
78
- hooks.push(hook);
79
- hooks.push('');
80
- // Generate infinite query version if applicable
81
- if (this.options.includeInfiniteQueries && this.isListEndpoint(endpoint)) {
82
- const infiniteHook = this.generateInfiniteQueryHook(endpoint, hookName);
83
- hooks.push(infiniteHook);
84
- hooks.push('');
85
- }
86
- }
87
- return hooks.join('\n');
88
- }
89
- generateQueryHook(endpoint, hookName) {
90
- const operationName = this.getOperationName(endpoint);
91
- const hasParams = this.hasRequiredParams(endpoint);
92
- const lines = [];
93
- // Generate JSDoc
94
- lines.push('/**');
95
- if (endpoint.summary) {
96
- lines.push(` * ${endpoint.summary}`);
97
- }
98
- if (endpoint.description) {
99
- lines.push(` * ${endpoint.description}`);
100
- }
101
- lines.push(` * @param ${hasParams ? 'params' : 'options'} ${hasParams ? 'Request parameters' : 'Query options'}`);
102
- lines.push(' */');
103
- // Generate hook signature
104
- const paramType = hasParams ? this.generateParamType(endpoint) : '';
105
- const params = hasParams ? `params: ${paramType}, options?: QueryOptions` : 'options?: QueryOptions = {}';
106
- lines.push(`export function ${hookName}(${params}) {`);
107
- // Generate hook body
108
- const queryKeyCall = hasParams ?
109
- `queryKeys.${this.camelCase(operationName)}(params)` :
110
- `queryKeys.${this.camelCase(operationName)}()`;
111
- const apiCall = hasParams ?
112
- `() => apiClient.${this.camelCase(operationName)}(params)` :
113
- `() => apiClient.${this.camelCase(operationName)}()`;
114
- lines.push(' return useQuery({');
115
- lines.push(` queryKey: ${queryKeyCall},`);
116
- lines.push(` queryFn: ${apiCall},`);
117
- if (this.options.authenticationRequired) {
118
- lines.push(' enabled: !!authToken && (options?.enabled ?? true),');
119
- }
120
- lines.push(' ...options');
121
- lines.push(' })');
122
- lines.push('}');
123
- return lines.join('\n');
124
- }
125
- generateInfiniteQueryHook(endpoint, baseHookName) {
126
- const hookName = baseHookName.replace('use', 'useInfinite');
127
- const operationName = this.getOperationName(endpoint);
128
- const lines = [];
129
- lines.push('/**');
130
- lines.push(` * Infinite query version of ${baseHookName}`);
131
- lines.push(' */');
132
- const paramType = this.generateParamType(endpoint);
133
- lines.push(`export function ${hookName}(params: ${paramType}, options?: InfiniteQueryOptions) {`);
134
- lines.push(' return useInfiniteQuery({');
135
- lines.push(` queryKey: queryKeys.${this.camelCase(operationName)}(params),`);
136
- lines.push(` queryFn: ({ pageParam = 1 }) => apiClient.${this.camelCase(operationName)}({ ...params, page: pageParam }),`);
137
- lines.push(' getNextPageParam: (lastPage, allPages) => {');
138
- lines.push(' // Implement pagination logic based on your API response structure');
139
- lines.push(' return lastPage?.hasNextPage ? allPages.length + 1 : undefined');
140
- lines.push(' },');
141
- lines.push(' ...options');
142
- lines.push(' })');
143
- lines.push('}');
144
- return lines.join('\n');
145
- }
146
- generateMutationHooks(endpoints) {
147
- const hooks = [];
148
- hooks.push(this.generateFileHeader('Mutation Hooks'));
149
- hooks.push('');
150
- hooks.push("import { useMutation, useQueryClient, MutationOptions } from '@tanstack/react-query'");
151
- hooks.push("import { apiClient } from '../client'");
152
- hooks.push("import { queryKeys } from './keys'");
153
- hooks.push("import * as Types from './types'");
154
- hooks.push('');
155
- // Filter non-GET endpoints for mutations
156
- const mutationEndpoints = endpoints.filter(endpoint => endpoint.method !== 'get');
157
- for (const endpoint of mutationEndpoints) {
158
- const hookName = this.generateMutationHookName(endpoint);
159
- const hook = this.generateMutationHook(endpoint, hookName);
160
- hooks.push(hook);
161
- hooks.push('');
162
- }
163
- return hooks.join('\n');
164
- }
165
- generateMutationHook(endpoint, hookName) {
166
- const operationName = this.getOperationName(endpoint);
167
- const lines = [];
168
- // Generate JSDoc
169
- lines.push('/**');
170
- if (endpoint.summary) {
171
- lines.push(` * ${endpoint.summary}`);
172
- }
173
- if (endpoint.description) {
174
- lines.push(` * ${endpoint.description}`);
175
- }
176
- lines.push(' */');
177
- // Generate hook signature
178
- const mutationType = this.generateMutationType(endpoint);
179
- lines.push(`export function ${hookName}(options?: MutationOptions<any, any, ${mutationType}>) {`);
180
- lines.push(' const queryClient = useQueryClient()');
181
- lines.push('');
182
- lines.push(' return useMutation({');
183
- // Generate mutation function
184
- const hasParams = this.hasRequiredParams(endpoint);
185
- if (hasParams) {
186
- lines.push(` mutationFn: ({ pathParams, ...data }) => apiClient.${this.camelCase(operationName)}(pathParams, data),`);
187
- }
188
- else {
189
- lines.push(` mutationFn: (data) => apiClient.${this.camelCase(operationName)}(data),`);
190
- }
191
- // Generate optimistic updates and cache invalidation
192
- if (this.options.includeOptimisticUpdates) {
193
- lines.push(this.generateOptimisticUpdate(endpoint));
194
- }
195
- lines.push(this.generateCacheInvalidation(endpoint));
196
- lines.push(' ...options');
197
- lines.push(' })');
198
- lines.push('}');
199
- return lines.join('\n');
200
- }
201
- generateOptimisticUpdate(endpoint) {
202
- const method = endpoint.method.toLowerCase();
203
- switch (method) {
204
- case 'post':
205
- return ` onMutate: async (newData) => {
206
- // Cancel outgoing refetches
207
- await queryClient.cancelQueries({ queryKey: queryKeys.all })
208
-
209
- // Snapshot previous value
210
- const previousData = queryClient.getQueryData(queryKeys.all)
211
-
212
- // Optimistically update cache
213
- queryClient.setQueryData(queryKeys.all, (old: any) => [...(old || []), newData])
214
-
215
- return { previousData }
216
- },
217
- onError: (err, newData, context) => {
218
- // Rollback on error
219
- queryClient.setQueryData(queryKeys.all, context?.previousData)
220
- },`;
221
- case 'put':
222
- case 'patch':
223
- return ` onMutate: async (updatedData) => {
224
- await queryClient.cancelQueries({ queryKey: queryKeys.all })
225
-
226
- const previousData = queryClient.getQueryData(queryKeys.all)
227
-
228
- // Update specific item in cache
229
- queryClient.setQueryData(queryKeys.all, (old: any) =>
230
- old?.map((item: any) => item.id === updatedData.id ? { ...item, ...updatedData } : item)
231
- )
232
-
233
- return { previousData }
234
- },
235
- onError: (err, updatedData, context) => {
236
- queryClient.setQueryData(queryKeys.all, context?.previousData)
237
- },`;
238
- case 'delete':
239
- return ` onMutate: async (id) => {
240
- await queryClient.cancelQueries({ queryKey: queryKeys.all })
241
-
242
- const previousData = queryClient.getQueryData(queryKeys.all)
243
-
244
- // Remove item from cache
245
- queryClient.setQueryData(queryKeys.all, (old: any) =>
246
- old?.filter((item: any) => item.id !== id)
247
- )
248
-
249
- return { previousData }
250
- },
251
- onError: (err, id, context) => {
252
- queryClient.setQueryData(queryKeys.all, context?.previousData)
253
- },`;
254
- default:
255
- return '';
256
- }
257
- }
258
- generateCacheInvalidation(endpoint) {
259
- const relatedTags = this.getRelatedQueryTags(endpoint);
260
- return ` onSettled: () => {
261
- // Invalidate related queries
262
- ${relatedTags.map(tag => `queryClient.invalidateQueries({ queryKey: queryKeys.${tag} })`).join('\n ')}
263
- },`;
264
- }
265
- generateQueryKeys(endpoints) {
266
- const keys = [];
267
- keys.push(this.generateFileHeader('Query Keys'));
268
- keys.push('');
269
- keys.push('/**');
270
- keys.push(' * Centralized query key factory');
271
- keys.push(' * Ensures consistent cache key generation across the application');
272
- keys.push(' */');
273
- keys.push('');
274
- keys.push('export const queryKeys = {');
275
- keys.push(` all: ['${this.options.queryKeyPrefix}'] as const,`);
276
- keys.push('');
277
- // Group endpoints by resource/tag
278
- const groupedEndpoints = this.groupEndpointsByResource(endpoints);
279
- for (const [resource, resourceEndpoints] of Object.entries(groupedEndpoints)) {
280
- // Sanitize resource name to be a valid JS identifier
281
- const sanitizedResource = this.sanitizeIdentifier(resource);
282
- console.log(`DEBUG: resource="${resource}" -> sanitizedResource="${sanitizedResource}"`);
283
- keys.push(` // ${resource} keys`);
284
- keys.push(` ${sanitizedResource}: () => [...queryKeys.all, '${sanitizedResource}'] as const,`);
285
- const usedKeys = new Set([sanitizedResource]); // Track used keys to avoid duplicates
286
- for (const endpoint of resourceEndpoints) {
287
- if (endpoint.method !== 'get')
288
- continue;
289
- const operationName = this.getOperationName(endpoint);
290
- let keyName = this.camelCase(operationName.replace(/^get/, ''));
291
- // Skip if keyName matches sanitizedResource (base key already handles this)
292
- if (keyName === sanitizedResource) {
293
- continue;
294
- }
295
- // If keyName already used, add suffix to disambiguate
296
- if (usedKeys.has(keyName)) {
297
- const isList = !endpoint.path.includes('{');
298
- keyName = isList ? `${keyName}List` : `${keyName}Detail`;
299
- }
300
- // Still duplicate after suffix? Skip to avoid compilation error
301
- if (usedKeys.has(keyName)) {
302
- continue;
303
- }
304
- usedKeys.add(keyName);
305
- if (this.hasRequiredParams(endpoint)) {
306
- keys.push(` ${keyName}: (params: Record<string, unknown>) => [...queryKeys.${sanitizedResource}(), '${keyName}', params] as const,`);
307
- }
308
- else {
309
- keys.push(` ${keyName}: () => [...queryKeys.${sanitizedResource}(), '${keyName}'] as const,`);
310
- }
311
- }
312
- keys.push('');
313
- }
314
- keys.push('}');
315
- return keys.join('\n');
316
- }
317
- generateHookTypes(endpoints) {
318
- const types = [];
319
- types.push(this.generateFileHeader('Hook Types'));
320
- types.push('');
321
- types.push("import { QueryOptions, MutationOptions, InfiniteQueryOptions } from '@tanstack/react-query'");
322
- types.push('');
323
- // Generate parameter and response types for hooks
324
- for (const endpoint of endpoints) {
325
- const operationName = this.getOperationName(endpoint);
326
- if (this.hasRequiredParams(endpoint)) {
327
- const paramType = this.generateParamType(endpoint);
328
- types.push(`export interface ${this.capitalize(operationName)}Params ${paramType}`);
329
- types.push('');
330
- }
331
- if (endpoint.method !== 'get') {
332
- const mutationType = this.generateMutationType(endpoint);
333
- types.push(`export interface ${this.capitalize(operationName)}Data ${mutationType}`);
334
- types.push('');
335
- }
336
- }
337
- return types.join('\n');
338
- }
339
- generateIndexFile() {
340
- const exports = [];
341
- exports.push(this.generateFileHeader('Generated React Hooks'));
342
- exports.push('');
343
- exports.push('// Query hooks');
344
- exports.push("export * from './queries'");
345
- exports.push('');
346
- exports.push('// Mutation hooks');
347
- exports.push("export * from './mutations'");
348
- exports.push('');
349
- exports.push('// Query keys');
350
- exports.push("export { queryKeys } from './keys'");
351
- exports.push('');
352
- exports.push('// Hook types');
353
- exports.push("export * from './types'");
354
- return exports.join('\n');
355
- }
356
- generateFileHeader(title) {
357
- return `/**
358
- * ${title}
359
- *
360
- * Auto-generated React hooks from OpenAPI specification
361
- * Do not edit manually - regenerate using the hook generator
362
- */`;
363
- }
364
- generateQueryHookName(endpoint) {
365
- const operationName = this.getOperationName(endpoint);
366
- return `use${this.capitalize(operationName)}`;
367
- }
368
- generateMutationHookName(endpoint) {
369
- const operationName = this.getOperationName(endpoint);
370
- return `use${this.capitalize(operationName)}`;
371
- }
372
- getOperationName(endpoint) {
373
- // Check if we have a manual override in config
374
- if (endpoint.operationId) {
375
- const override = this.configManager.getOverride(endpoint.operationId);
376
- if (override) {
377
- return override;
378
- }
379
- }
380
- // Generate the name using existing logic
381
- let operationName;
382
- if (endpoint.operationId) {
383
- operationName = this.cleanOperationId(endpoint.operationId, endpoint);
384
- }
385
- else {
386
- operationName = this.generateCleanName(endpoint);
387
- }
388
- // Apply deduplication if enabled
389
- const patterns = this.configManager.getPatterns();
390
- if (patterns.deduplicateSegments) {
391
- operationName = this.removeDuplicateSegments(operationName);
392
- }
393
- // Score the generated name if confidence scoring is enabled
394
- if (this.options.enableConfidenceScoring && endpoint.operationId) {
395
- const hookName = endpoint.method === 'get'
396
- ? `use${this.capitalize(operationName)}`
397
- : `use${this.capitalize(this.getMethodPrefix(endpoint.method))}${this.capitalize(operationName)}`;
398
- const scored = this.confidenceScorer.scoreHookName(hookName, endpoint.operationId, endpoint.path, endpoint.method);
399
- // Debug logging
400
- if (scored.confidence === 'low') {
401
- console.log(`Low confidence hook: ${hookName} (score: ${scored.score})`);
402
- }
403
- this.scoredNames.push(scored);
404
- // If we have a suggestion with higher confidence, use it
405
- if (scored.confidence === 'low' && scored.suggestions.length > 0) {
406
- const suggestion = scored.suggestions[0];
407
- // Extract just the operation name from the suggestion
408
- const prefix = `use${this.capitalize(this.getMethodPrefix(endpoint.method))}`;
409
- if (suggestion.startsWith(prefix)) {
410
- operationName = this.camelCase(suggestion.substring(prefix.length));
411
- }
412
- else if (suggestion.startsWith('use')) {
413
- operationName = this.camelCase(suggestion.substring(3));
414
- }
415
- }
416
- }
417
- return operationName;
418
- }
419
- cleanOperationId(operationId, endpoint) {
420
- // Parse the operation ID into components
421
- const parsed = this.parseOperationId(operationId, endpoint);
422
- // Check if it's a custom action endpoint
423
- if (parsed.isCustomAction) {
424
- return this.formatCustomActionName(parsed);
425
- }
426
- // Check if it's a special endpoint that should keep its verb
427
- if (this.isSpecialEndpoint(parsed)) {
428
- return this.formatSpecialEndpointName(parsed);
429
- }
430
- // Format standard CRUD operation
431
- return this.formatStandardOperationName(parsed, endpoint);
432
- }
433
- /**
434
- * Parse operation ID into structured components
435
- */
436
- parseOperationId(operationId, endpoint) {
437
- // Common patterns for operation IDs
438
- const patterns = [
439
- // Pattern: action_resource_api_v1_path_segments_method
440
- /^(?<action>\w+?)_(?<resource>\w+?)_api_v\d+_(?<path>.+?)_(?<method>get|post|put|patch|delete)$/i,
441
- // Pattern: action_api_v1_path_segments_method
442
- /^(?<action>\w+?)_api_v\d+_(?<path>.+?)_(?<method>get|post|put|patch|delete)$/i,
443
- // Pattern: action_resource_path_method
444
- /^(?<action>\w+?)_(?<resource>\w+?)_(?<path>.+?)_(?<method>get|post|put|patch|delete)$/i,
445
- // Pattern: simple action_action_method (e.g., ready_ready_get, health_health_get)
446
- /^(?<action>\w+?)_\1_(?<method>get|post|put|patch|delete)$/i,
447
- // Pattern: simple action_resource
448
- /^(?<action>\w+?)_(?<resource>\w+?)$/i,
449
- ];
450
- let components = {};
451
- for (const pattern of patterns) {
452
- const match = operationId.match(pattern);
453
- if (match && match.groups) {
454
- components = match.groups;
455
- break;
456
- }
457
- }
458
- // Extract path information
459
- const pathSegments = endpoint.path.split('/').filter(s => s && !s.startsWith('{'));
460
- const lastPathSegment = pathSegments[pathSegments.length - 1]?.replace(/-/g, '_');
461
- // Check if it's a custom action
462
- const urlSegments = endpoint.path.split('/');
463
- const lastSegment = urlSegments[urlSegments.length - 1];
464
- const prevSegment = urlSegments[urlSegments.length - 2];
465
- // Custom action: either after a path param (e.g., /accounts/{id}/archive)
466
- // or a known action verb on a collection (e.g., /activities/search)
467
- const isResourceAction = !lastSegment.startsWith('{') && prevSegment?.startsWith('{');
468
- const isCollectionAction = !lastSegment.startsWith('{') && !prevSegment?.startsWith('{') && COLLECTION_ACTIONS.includes(lastSegment);
469
- const isCustomAction = isResourceAction || isCollectionAction;
470
- // Extract or infer the action verb
471
- const actionVerb = this.extractActionVerb(components.action || operationId, endpoint.method);
472
- // Extract or infer the resource
473
- const resource = components.resource ||
474
- this.extractResourceFromPath(endpoint.path) ||
475
- lastPathSegment ||
476
- 'resource';
477
- return {
478
- action: actionVerb,
479
- resource: isCollectionAction ? prevSegment : resource,
480
- path: components.path || '',
481
- method: endpoint.method,
482
- isCustomAction,
483
- customActionName: isCustomAction ? lastSegment.replace(/-/g, '_') : undefined,
484
- originalId: operationId
485
- };
486
- }
487
- /**
488
- * Extract the primary action verb from the operation ID
489
- */
490
- extractActionVerb(actionPart, method) {
491
- // Common action verbs to recognize
492
- const actionVerbs = [
493
- ...AUTH_ACTIONS,
494
- ...HEALTH_ENDPOINTS,
495
- 'list', 'get', 'create', 'update', 'delete',
496
- ...COLLECTION_ACTIONS,
497
- 'approve', 'reject', 'archive', 'restore',
498
- 'sync', 'refresh', 'reset', 'verify', 'validate'
499
- ];
500
- // Check if the action part starts with any known verb
501
- const lowerAction = actionPart.toLowerCase();
502
- for (const verb of actionVerbs) {
503
- if (lowerAction.startsWith(verb)) {
504
- return verb;
505
- }
506
- }
507
- // Default to HTTP method mapping
508
- const methodMap = {
509
- get: 'get',
510
- post: 'create',
511
- put: 'update',
512
- patch: 'update',
513
- delete: 'delete'
514
- };
515
- return methodMap[method] || method;
516
- }
517
- /**
518
- * Extract resource name from the API path
519
- */
520
- extractResourceFromPath(path) {
521
- const segments = path.split('/').filter(s => s && !s.startsWith('{'));
522
- // Skip common prefixes
523
- const filtered = segments.filter(s => !['api', 'v1', 'v2'].includes(s));
524
- // Return the last meaningful segment
525
- return filtered[filtered.length - 1]?.replace(/-/g, '_') || null;
526
- }
527
- /**
528
- * Check if this is a special endpoint that should keep its action verb
529
- */
530
- isSpecialEndpoint(parsed) {
531
- return SPECIAL_ACTIONS.includes(parsed.action);
532
- }
533
- /**
534
- * Format name for custom action endpoints
535
- */
536
- formatCustomActionName(parsed) {
537
- const { action, resource, customActionName } = parsed;
538
- // For custom actions like "reassign-budget", format as "reassign_budget"
539
- if (customActionName) {
540
- // Check if the action is already part of the custom action name
541
- if (customActionName.startsWith(action)) {
542
- return customActionName;
543
- }
544
- // Otherwise combine resource and action
545
- return `${singularize(resource)}_${customActionName}`;
546
- }
547
- return `${action}_${singularize(resource)}`;
548
- }
549
- /**
550
- * Format name for special endpoints (login, health, etc.)
551
- */
552
- formatSpecialEndpointName(parsed) {
553
- const { action, resource } = parsed;
554
- // For simple singleton endpoints, just return the action
555
- const simpleActions = [...AUTH_ACTIONS.filter(a => ['login', 'logout'].includes(a)), ...HEALTH_ENDPOINTS.filter(h => ['health', 'ready', 'status'].includes(h)), ...USER_PROFILE_ENDPOINTS];
556
- if (simpleActions.includes(action)) {
557
- return action;
558
- }
559
- // For other special endpoints, combine with resource if meaningful
560
- if (resource && resource !== action) {
561
- return `${action}_${resource}`;
562
- }
563
- return action;
564
- }
565
- /**
566
- * Format name for standard CRUD operations
567
- */
568
- formatStandardOperationName(parsed, endpoint) {
569
- const { resource } = parsed;
570
- // Determine if it's a list or single resource operation
571
- const isList = endpoint.method === 'get' && !endpoint.path.includes('{') && !this.isSingletonEndpoint(resource, endpoint.path);
572
- switch (endpoint.method) {
573
- case 'get':
574
- return isList ? pluralize(resource) : singularize(resource);
575
- case 'post':
576
- return `create_${singularize(resource)}`;
577
- case 'put':
578
- case 'patch':
579
- return `update_${singularize(resource)}`;
580
- case 'delete':
581
- return `delete_${singularize(resource)}`;
582
- default:
583
- return singularize(resource);
584
- }
585
- }
586
- generateCleanName(endpoint) {
587
- const pathParts = endpoint.path
588
- .split('/')
589
- .filter(part => part && !part.startsWith('{'));
590
- if (pathParts.length === 0)
591
- return endpoint.method;
592
- // Get the main resource (usually the last non-parameter segment)
593
- const resource = pathParts[pathParts.length - 1];
594
- let cleanResource = resource.replace(/-/g, '_');
595
- // Check if it's a custom action (last segment after a parameter)
596
- const segments = endpoint.path.split('/');
597
- const lastSegment = segments[segments.length - 1];
598
- const hasCustomAction = !lastSegment.startsWith('{') && segments[segments.length - 2]?.startsWith('{');
599
- if (hasCustomAction) {
600
- // Custom action: use resource + action
601
- const mainResource = pathParts[pathParts.length - 2] || pathParts[pathParts.length - 1];
602
- const action = lastSegment.replace(/-/g, '_');
603
- return `${mainResource}_${action}`;
604
- }
605
- // For nested resources (e.g., /categories/{id}/subcategories),
606
- // just use the last resource name
607
- if (pathParts.length > 2 && segments.some(s => s.startsWith('{'))) {
608
- // This is a nested resource, just use the last part
609
- return cleanResource;
610
- }
611
- // Standard CRUD operations
612
- switch (endpoint.method) {
613
- case 'get':
614
- // Check if it's a list or single resource
615
- const isList = !endpoint.path.includes('{');
616
- // Only pluralize if it's a list and not already plural
617
- if (isList && !cleanResource.endsWith('s') && !cleanResource.endsWith('ies')) {
618
- return pluralize(cleanResource);
619
- }
620
- return cleanResource;
621
- case 'post':
622
- return `create_${cleanResource}`;
623
- case 'put':
624
- case 'patch':
625
- return `update_${cleanResource}`;
626
- case 'delete':
627
- return `delete_${cleanResource}`;
628
- default:
629
- return `${endpoint.method}_${cleanResource}`;
630
- }
631
- }
632
- isSingletonEndpoint(resource, path) {
633
- // Check if resource matches singleton pattern
634
- const lowerResource = resource.toLowerCase().replace(/_/g, '');
635
- if (SINGLETON_RESOURCES.some(s => lowerResource === s || lowerResource.endsWith(s))) {
636
- return true;
637
- }
638
-
639
- // Check path patterns for common singletons
640
- const pathSegments = path.split('/').filter(s => s && !s.startsWith('{'));
641
- const lastSegment = pathSegments[pathSegments.length - 1]?.toLowerCase();
642
-
643
- if (SINGLETON_RESOURCES.includes(lastSegment)) {
644
- return true;
645
- }
646
-
647
- return false;
648
- }
649
- hasRequiredParams(endpoint) {
650
- return endpoint.parameters.some(p => p.required) ||
651
- endpoint.parameters.some(p => p.in === 'path');
652
- }
653
- generateParamType(endpoint) {
654
- const pathParams = endpoint.parameters.filter(p => p.in === 'path');
655
- const queryParams = endpoint.parameters.filter(p => p.in === 'query');
656
- const properties = [];
657
- pathParams.forEach(param => {
658
- properties.push(`${param.name}: ${this.getParameterType(param)}`);
659
- });
660
- queryParams.forEach(param => {
661
- const optional = param.required ? '' : '?';
662
- properties.push(`${param.name}${optional}: ${this.getParameterType(param)}`);
663
- });
664
- return `{ ${properties.join('; ')} }`;
665
- }
666
- generateMutationType(endpoint) {
667
- const hasPathParams = endpoint.parameters.some(p => p.in === 'path');
668
- if (hasPathParams) {
669
- const pathType = this.generateParamType(endpoint);
670
- return `{ pathParams: ${pathType}; [key: string]: any }`;
671
- }
672
- return '{ [key: string]: any }';
673
- }
674
- getParameterType(param) {
675
- switch (param.schema.type) {
676
- case 'string': return 'string';
677
- case 'number':
678
- case 'integer': return 'number';
679
- case 'boolean': return 'boolean';
680
- default: return 'any';
681
- }
682
- }
683
- isListEndpoint(endpoint) {
684
- // Extract resource for singleton check
685
- const pathSegments = endpoint.path.split('/').filter(s => s && !s.startsWith('{'));
686
- const resource = pathSegments[pathSegments.length - 1]?.replace(/-/g, '_') || '';
687
-
688
- // Singleton endpoints are never list endpoints
689
- if (this.isSingletonEndpoint(resource, endpoint.path)) {
690
- return false;
691
- }
692
-
693
- return endpoint.path.includes('list') ||
694
- !endpoint.path.includes('{') ||
695
- (endpoint.summary?.toLowerCase().includes('list') ?? false) ||
696
- (endpoint.summary?.toLowerCase().includes('get all') ?? false);
697
- }
698
- groupEndpointsByResource(endpoints) {
699
- const groups = {};
700
- for (const endpoint of endpoints) {
701
- const tag = endpoint.tags?.[0] || 'default';
702
- // Sanitize tag to be a valid JS identifier (camelCase, no spaces)
703
- const resource = this.sanitizeIdentifier(tag);
704
- if (!groups[resource]) {
705
- groups[resource] = [];
706
- }
707
- groups[resource].push(endpoint);
708
- }
709
- return groups;
710
- }
711
- sanitizeIdentifier(str) {
712
- // Convert to camelCase and remove invalid characters
713
- return str
714
- .replace(/[^a-zA-Z0-9\s]/g, '') // Remove special chars except spaces
715
- .split(/\s+/) // Split on whitespace
716
- .map((word, i) => i === 0 ? word.toLowerCase() : word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
717
- .join('');
718
- }
719
- getRelatedQueryTags(endpoint) {
720
- const tag = endpoint.tags?.[0] || 'default';
721
- const resource = this.sanitizeIdentifier(tag);
722
- return [resource, 'all'];
723
- }
724
- camelCase(str) {
725
- return str.replace(/[-_](.)/g, (_, char) => char.toUpperCase())
726
- .replace(/^./, char => char.toLowerCase());
727
- }
728
- capitalize(str) {
729
- // Convert snake_case to PascalCase
730
- return str.split('_')
731
- .map(part => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase())
732
- .join('');
733
- }
734
- getMethodPrefix(method) {
735
- const prefixes = {
736
- post: 'create',
737
- put: 'update',
738
- patch: 'update',
739
- delete: 'delete'
740
- };
741
- return prefixes[method.toLowerCase()] || method.toLowerCase();
742
- }
743
- removeDuplicateSegments(operationName) {
744
- const parts = operationName.split('_');
745
- const seen = new Set();
746
- const result = [];
747
- for (const part of parts) {
748
- const lower = part.toLowerCase();
749
- // Keep important words even if duplicated (like verbs)
750
- const importantWords = ['create', 'update', 'delete', 'get', 'list', 'reassign', 'assign'];
751
- if (!seen.has(lower) || importantWords.includes(lower)) {
752
- seen.add(lower);
753
- result.push(part);
754
- }
755
- }
756
- return result.join('_');
757
- }
758
- }
759
- // Factory function
760
- export async function generateHooks(parsedAPI, options) {
761
- const generator = new ReactHookGenerator(options);
762
- return generator.generate(parsedAPI);
763
- }