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