@pattern-stack/frontend-patterns 0.2.0-alpha.0 → 0.2.0-alpha.3
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/cli/commands/generate-hooks.ts +13 -4
- package/cli/src/codegen/openapi/__tests__/naming-utils.test.js +367 -0
- package/cli/src/codegen/openapi/client-generator.js +87 -19
- package/cli/src/codegen/openapi/confidence-scorer.js +93 -0
- package/cli/src/codegen/openapi/hook-config.js +48 -0
- package/cli/src/codegen/openapi/hook-generator.js +100 -62
- package/cli/src/codegen/openapi/naming-constants.js +98 -0
- package/cli/src/codegen/openapi/naming-utils.js +149 -0
- package/dist/atoms/components/core/Badge/Badge.d.ts +1 -1
- package/dist/atoms/components/data/DataTable/DataTable.d.ts.map +1 -1
- package/dist/atoms/components/data/DataTable/DataTable.types.d.ts +15 -3
- package/dist/atoms/components/data/DataTable/DataTable.types.d.ts.map +1 -1
- package/dist/atoms/hooks/index.d.ts +2 -0
- package/dist/atoms/hooks/index.d.ts.map +1 -1
- package/dist/atoms/hooks/useFieldMetadata.d.ts +18 -0
- package/dist/atoms/hooks/useFieldMetadata.d.ts.map +1 -0
- package/dist/atoms/hooks/useResponsiveTable.d.ts +103 -0
- package/dist/atoms/hooks/useResponsiveTable.d.ts.map +1 -0
- package/dist/atoms/primitives/sheet.d.ts +23 -0
- package/dist/atoms/primitives/sheet.d.ts.map +1 -0
- package/dist/atoms/services/auth-service.d.ts.map +1 -1
- package/dist/atoms/types/auth.d.ts +51 -0
- 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/ui-config.d.ts +25 -8
- package/dist/atoms/types/ui-config.d.ts.map +1 -1
- package/dist/atoms/types/ui-metadata.d.ts +112 -0
- package/dist/atoms/types/ui-metadata.d.ts.map +1 -0
- 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/hooks/useAuth.d.ts.map +1 -1
- package/dist/frontend-patterns.css +82 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.es.js +1030 -248
- package/dist/index.es.js.map +1 -1
- package/dist/index.js +1031 -248
- package/dist/index.js.map +1 -1
- 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 +2 -0
- package/dist/molecules/layout/index.d.ts.map +1 -1
- 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.map +1 -1
- package/dist/templates/index.d.ts +1 -0
- package/dist/templates/index.d.ts.map +1 -1
- package/package.json +4 -3
|
@@ -8,11 +8,15 @@
|
|
|
8
8
|
|
|
9
9
|
import { promises as fs } from 'fs'
|
|
10
10
|
import { join, dirname } from 'path'
|
|
11
|
+
import { fileURLToPath } from 'url'
|
|
11
12
|
import { loadOpenAPISpec, parseOpenAPI } from '../../src/codegen/openapi/parser.js'
|
|
12
13
|
import { generateTypes } from '../../src/codegen/openapi/type-generator.js'
|
|
13
14
|
import { generateAPIClient } from '../../src/codegen/openapi/client-generator.js'
|
|
14
15
|
import { generateHooks } from '../../src/codegen/openapi/hook-generator.js'
|
|
15
16
|
|
|
17
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
18
|
+
const __dirname = dirname(__filename)
|
|
19
|
+
|
|
16
20
|
interface GenerateHooksOptions {
|
|
17
21
|
output: string
|
|
18
22
|
prefix?: string
|
|
@@ -121,6 +125,11 @@ export async function generateHooksCommand(source: string, options: GenerateHook
|
|
|
121
125
|
await writeFile(join(options.output, 'hooks', 'keys.ts'), hooks.keys)
|
|
122
126
|
await writeFile(join(options.output, 'hooks', 'types.ts'), hooks.types)
|
|
123
127
|
await writeFile(join(options.output, 'hooks', 'index.ts'), hooks.index)
|
|
128
|
+
|
|
129
|
+
// Copy bulk-types.ts to output directory (referenced by hooks/types.ts)
|
|
130
|
+
const bulkTypesPath = join(__dirname, '../../src/codegen/openapi/bulk-types.ts')
|
|
131
|
+
const bulkTypesContent = await fs.readFile(bulkTypesPath, 'utf8')
|
|
132
|
+
await writeFile(join(options.output, 'bulk-types.ts'), bulkTypesContent)
|
|
124
133
|
|
|
125
134
|
// Write confidence report if available
|
|
126
135
|
if (hooks.report) {
|
|
@@ -154,10 +163,10 @@ export { queryKeys } from './hooks'`
|
|
|
154
163
|
|
|
155
164
|
await writeFile(join(options.output, 'index.ts'), mainIndex)
|
|
156
165
|
|
|
157
|
-
// Generate usage example
|
|
166
|
+
// Generate usage example (as .tsx since it contains JSX)
|
|
158
167
|
const usageExample = generateUsageExample(spec.info.title, options)
|
|
159
|
-
await writeFile(join(options.output, 'example.
|
|
160
|
-
|
|
168
|
+
await writeFile(join(options.output, 'example.tsx'), usageExample)
|
|
169
|
+
|
|
161
170
|
console.log()
|
|
162
171
|
console.log('✅ Generation complete!')
|
|
163
172
|
console.log(`📦 Generated files in: ${options.output}`)
|
|
@@ -165,7 +174,7 @@ export { queryKeys } from './hooks'`
|
|
|
165
174
|
console.log('🚀 Quick start:')
|
|
166
175
|
console.log(`import { createAPIClient, useGetUsers } from './${options.output}'`)
|
|
167
176
|
console.log()
|
|
168
|
-
console.log('💡 See example.
|
|
177
|
+
console.log('💡 See example.tsx for detailed usage instructions')
|
|
169
178
|
|
|
170
179
|
} catch (error) {
|
|
171
180
|
console.error('❌ Generation failed:')
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import { singularize, pluralize, isPlural } from '../naming-utils.js';
|
|
2
|
+
|
|
3
|
+
describe('singularize()', () => {
|
|
4
|
+
describe('regular plurals', () => {
|
|
5
|
+
it('should convert cats to cat', () => {
|
|
6
|
+
expect(singularize('cats')).toBe('cat');
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it('should convert dogs to dog', () => {
|
|
10
|
+
expect(singularize('dogs')).toBe('dog');
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('should convert users to user', () => {
|
|
14
|
+
expect(singularize('users')).toBe('user');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('should convert accounts to account', () => {
|
|
18
|
+
expect(singularize('accounts')).toBe('account');
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe('-ies plurals', () => {
|
|
23
|
+
it('should convert activities to activity', () => {
|
|
24
|
+
expect(singularize('activities')).toBe('activity');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should convert categories to category', () => {
|
|
28
|
+
expect(singularize('categories')).toBe('category');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should convert companies to company', () => {
|
|
32
|
+
expect(singularize('companies')).toBe('company');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should convert opportunities to opportunity', () => {
|
|
36
|
+
expect(singularize('opportunities')).toBe('opportunity');
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe('-es plurals', () => {
|
|
41
|
+
it('should convert boxes to box', () => {
|
|
42
|
+
expect(singularize('boxes')).toBe('box');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should convert watches to watch', () => {
|
|
46
|
+
expect(singularize('watches')).toBe('watch');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should convert dishes to dish', () => {
|
|
50
|
+
expect(singularize('dishes')).toBe('dish');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should convert benches to bench', () => {
|
|
54
|
+
expect(singularize('benches')).toBe('bench');
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('-ses plurals (tricky cases)', () => {
|
|
59
|
+
it('should convert addresses to address', () => {
|
|
60
|
+
expect(singularize('addresses')).toBe('address');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should convert businesses to business', () => {
|
|
64
|
+
expect(singularize('businesses')).toBe('business');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should convert processes to process', () => {
|
|
68
|
+
expect(singularize('processes')).toBe('process');
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe('irregular plurals', () => {
|
|
73
|
+
it('should convert people to person', () => {
|
|
74
|
+
expect(singularize('people')).toBe('person');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should convert children to child', () => {
|
|
78
|
+
expect(singularize('children')).toBe('child');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should convert men to man', () => {
|
|
82
|
+
expect(singularize('men')).toBe('man');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should convert women to woman', () => {
|
|
86
|
+
expect(singularize('women')).toBe('woman');
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe('already singular', () => {
|
|
91
|
+
it('should keep data as data', () => {
|
|
92
|
+
expect(singularize('data')).toBe('data');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should keep metadata as metadata', () => {
|
|
96
|
+
expect(singularize('metadata')).toBe('metadata');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('should keep user as user', () => {
|
|
100
|
+
expect(singularize('user')).toBe('user');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should keep account as account', () => {
|
|
104
|
+
expect(singularize('account')).toBe('account');
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe('edge cases', () => {
|
|
109
|
+
it('should keep status as status', () => {
|
|
110
|
+
expect(singularize('status')).toBe('status');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('should keep class as class', () => {
|
|
114
|
+
expect(singularize('class')).toBe('class');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('should keep bus as bus', () => {
|
|
118
|
+
expect(singularize('bus')).toBe('bus');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('should convert pass to pass (not pas)', () => {
|
|
122
|
+
expect(singularize('pass')).toBe('pass');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('should handle empty string', () => {
|
|
126
|
+
expect(singularize('')).toBe('');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('should handle single character', () => {
|
|
130
|
+
expect(singularize('a')).toBe('a');
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe('pluralize()', () => {
|
|
136
|
+
describe('regular plurals', () => {
|
|
137
|
+
it('should convert cat to cats', () => {
|
|
138
|
+
expect(pluralize('cat')).toBe('cats');
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('should convert dog to dogs', () => {
|
|
142
|
+
expect(pluralize('dog')).toBe('dogs');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('should convert user to users', () => {
|
|
146
|
+
expect(pluralize('user')).toBe('users');
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('should convert account to accounts', () => {
|
|
150
|
+
expect(pluralize('account')).toBe('accounts');
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
describe('-y endings (consonant + y)', () => {
|
|
155
|
+
it('should convert activity to activities', () => {
|
|
156
|
+
expect(pluralize('activity')).toBe('activities');
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('should convert category to categories', () => {
|
|
160
|
+
expect(pluralize('category')).toBe('categories');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('should convert company to companies', () => {
|
|
164
|
+
expect(pluralize('company')).toBe('companies');
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('should convert opportunity to opportunities', () => {
|
|
168
|
+
expect(pluralize('opportunity')).toBe('opportunities');
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
describe('vowel + y endings', () => {
|
|
173
|
+
it('should convert day to days', () => {
|
|
174
|
+
expect(pluralize('day')).toBe('days');
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('should convert key to keys', () => {
|
|
178
|
+
expect(pluralize('key')).toBe('keys');
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('should convert boy to boys', () => {
|
|
182
|
+
expect(pluralize('boy')).toBe('boys');
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('should convert array to arrays', () => {
|
|
186
|
+
expect(pluralize('array')).toBe('arrays');
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
describe('-x, -ch, -sh, -s, -ss endings', () => {
|
|
191
|
+
it('should convert box to boxes', () => {
|
|
192
|
+
expect(pluralize('box')).toBe('boxes');
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('should convert watch to watches', () => {
|
|
196
|
+
expect(pluralize('watch')).toBe('watches');
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('should convert dish to dishes', () => {
|
|
200
|
+
expect(pluralize('dish')).toBe('dishes');
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('should convert bench to benches', () => {
|
|
204
|
+
expect(pluralize('bench')).toBe('benches');
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('should convert address to addresses', () => {
|
|
208
|
+
expect(pluralize('address')).toBe('addresses');
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('should convert business to businesses', () => {
|
|
212
|
+
expect(pluralize('business')).toBe('businesses');
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('should convert class to classes', () => {
|
|
216
|
+
expect(pluralize('class')).toBe('classes');
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
describe('irregular plurals', () => {
|
|
221
|
+
it('should convert person to people', () => {
|
|
222
|
+
expect(pluralize('person')).toBe('people');
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('should convert child to children', () => {
|
|
226
|
+
expect(pluralize('child')).toBe('children');
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('should convert man to men', () => {
|
|
230
|
+
expect(pluralize('man')).toBe('men');
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('should convert woman to women', () => {
|
|
234
|
+
expect(pluralize('woman')).toBe('women');
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
describe('already plural (no double-pluralize)', () => {
|
|
239
|
+
it('should keep accounts as accounts', () => {
|
|
240
|
+
expect(pluralize('accounts')).toBe('accounts');
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('should keep users as users', () => {
|
|
244
|
+
expect(pluralize('users')).toBe('users');
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('should keep activities as activities', () => {
|
|
248
|
+
expect(pluralize('activities')).toBe('activities');
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('should keep boxes as boxes', () => {
|
|
252
|
+
expect(pluralize('boxes')).toBe('boxes');
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
describe('edge cases', () => {
|
|
257
|
+
it('should handle empty string', () => {
|
|
258
|
+
expect(pluralize('')).toBe('');
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('should handle single character', () => {
|
|
262
|
+
expect(pluralize('a')).toBe('as');
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('should keep status as status (special case)', () => {
|
|
266
|
+
expect(pluralize('status')).toBe('status');
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
describe('isPlural()', () => {
|
|
272
|
+
describe('should return true for plural words', () => {
|
|
273
|
+
it('should identify cats as plural', () => {
|
|
274
|
+
expect(isPlural('cats')).toBe(true);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('should identify activities as plural', () => {
|
|
278
|
+
expect(isPlural('activities')).toBe(true);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('should identify boxes as plural', () => {
|
|
282
|
+
expect(isPlural('boxes')).toBe(true);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it('should identify watches as plural', () => {
|
|
286
|
+
expect(isPlural('watches')).toBe(true);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it('should identify addresses as plural', () => {
|
|
290
|
+
expect(isPlural('addresses')).toBe(true);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('should identify people as plural', () => {
|
|
294
|
+
expect(isPlural('people')).toBe(true);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('should identify children as plural', () => {
|
|
298
|
+
expect(isPlural('children')).toBe(true);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it('should identify users as plural', () => {
|
|
302
|
+
expect(isPlural('users')).toBe(true);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it('should identify accounts as plural', () => {
|
|
306
|
+
expect(isPlural('accounts')).toBe(true);
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
describe('should return false for singular words', () => {
|
|
311
|
+
it('should identify cat as singular', () => {
|
|
312
|
+
expect(isPlural('cat')).toBe(false);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it('should identify activity as singular', () => {
|
|
316
|
+
expect(isPlural('activity')).toBe(false);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it('should identify box as singular', () => {
|
|
320
|
+
expect(isPlural('box')).toBe(false);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it('should identify user as singular', () => {
|
|
324
|
+
expect(isPlural('user')).toBe(false);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it('should identify account as singular', () => {
|
|
328
|
+
expect(isPlural('account')).toBe(false);
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
describe('should return false for edge cases', () => {
|
|
333
|
+
it('should identify status as singular', () => {
|
|
334
|
+
expect(isPlural('status')).toBe(false);
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it('should identify class as singular', () => {
|
|
338
|
+
expect(isPlural('class')).toBe(false);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it('should identify bus as singular', () => {
|
|
342
|
+
expect(isPlural('bus')).toBe(false);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it('should identify process as singular', () => {
|
|
346
|
+
expect(isPlural('process')).toBe(false);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it('should identify data as singular', () => {
|
|
350
|
+
expect(isPlural('data')).toBe(false);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it('should identify metadata as singular', () => {
|
|
354
|
+
expect(isPlural('metadata')).toBe(false);
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
describe('should handle empty/minimal input', () => {
|
|
359
|
+
it('should return false for empty string', () => {
|
|
360
|
+
expect(isPlural('')).toBe(false);
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it('should return false for single character', () => {
|
|
364
|
+
expect(isPlural('a')).toBe(false);
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
});
|
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
*
|
|
7
7
|
* Part of FRO-12: API Client Generator
|
|
8
8
|
*/
|
|
9
|
+
import { singularize, pluralize, isPlural } from './naming-utils.js';
|
|
10
|
+
import { AUTH_ACTIONS, HEALTH_ENDPOINTS, USER_PROFILE_ENDPOINTS, SINGLETON_RESOURCES, COLLECTION_ACTIONS } from './naming-constants.js';
|
|
11
|
+
|
|
9
12
|
export class APIClientGenerator {
|
|
10
13
|
options;
|
|
11
14
|
constructor(options = {}) {
|
|
@@ -39,7 +42,7 @@ export class APIClientGenerator {
|
|
|
39
42
|
generateAxiosClient() {
|
|
40
43
|
return `/**
|
|
41
44
|
* Axios API Client
|
|
42
|
-
*
|
|
45
|
+
*
|
|
43
46
|
* Auto-generated API client with interceptors and error handling
|
|
44
47
|
*/
|
|
45
48
|
|
|
@@ -146,7 +149,7 @@ export class APIClient {
|
|
|
146
149
|
generateFetchClient() {
|
|
147
150
|
return `/**
|
|
148
151
|
* Fetch API Client
|
|
149
|
-
*
|
|
152
|
+
*
|
|
150
153
|
* Auto-generated API client using native fetch with error handling
|
|
151
154
|
*/
|
|
152
155
|
|
|
@@ -166,7 +169,7 @@ export class APIClient {
|
|
|
166
169
|
): Promise<T> {
|
|
167
170
|
try {
|
|
168
171
|
const fullUrl = this.buildUrl(url, options.params)
|
|
169
|
-
|
|
172
|
+
|
|
170
173
|
const fetchOptions: RequestInit = {
|
|
171
174
|
method,
|
|
172
175
|
headers: {
|
|
@@ -205,7 +208,7 @@ export class APIClient {
|
|
|
205
208
|
|
|
206
209
|
private buildUrl(path: string, params?: Record<string, any>): string {
|
|
207
210
|
const url = new URL(path, this.config.baseUrl)
|
|
208
|
-
|
|
211
|
+
|
|
209
212
|
if (params) {
|
|
210
213
|
Object.entries(params).forEach(([key, value]) => {
|
|
211
214
|
if (value !== undefined && value !== null) {
|
|
@@ -325,7 +328,7 @@ export class APIClient {
|
|
|
325
328
|
private shouldRetry(error: AxiosError): boolean {
|
|
326
329
|
const retryCount = (error.config as any)?._retryCount || 0
|
|
327
330
|
const maxRetries = this.config.retries || ${this.options.retries}
|
|
328
|
-
|
|
331
|
+
|
|
329
332
|
return (
|
|
330
333
|
retryCount < maxRetries &&
|
|
331
334
|
(!error.response || error.response.status >= 500) &&
|
|
@@ -335,11 +338,11 @@ export class APIClient {
|
|
|
335
338
|
|
|
336
339
|
private async retryRequest(config: any): Promise<any> {
|
|
337
340
|
config._retryCount = (config._retryCount || 0) + 1
|
|
338
|
-
|
|
341
|
+
|
|
339
342
|
// Exponential backoff
|
|
340
343
|
const delay = Math.pow(2, config._retryCount) * 1000
|
|
341
344
|
await new Promise(resolve => setTimeout(resolve, delay))
|
|
342
|
-
|
|
345
|
+
|
|
343
346
|
return this.client.request(config)
|
|
344
347
|
}`;
|
|
345
348
|
}
|
|
@@ -500,7 +503,7 @@ export class APIClient {
|
|
|
500
503
|
const authTypes = this.generateAuthTypes();
|
|
501
504
|
return `/**
|
|
502
505
|
* API Client Types
|
|
503
|
-
*
|
|
506
|
+
*
|
|
504
507
|
* Configuration and utility types for the generated API client
|
|
505
508
|
*/
|
|
506
509
|
|
|
@@ -556,7 +559,7 @@ export interface APIError {
|
|
|
556
559
|
generateConfiguration() {
|
|
557
560
|
return `/**
|
|
558
561
|
* API Client Configuration
|
|
559
|
-
*
|
|
562
|
+
*
|
|
560
563
|
* Default configuration and factory functions
|
|
561
564
|
*/
|
|
562
565
|
|
|
@@ -580,7 +583,7 @@ export const defaultConfig: Partial<APIClientConfig> = {
|
|
|
580
583
|
generateIndexFile() {
|
|
581
584
|
return `/**
|
|
582
585
|
* Generated API Client
|
|
583
|
-
*
|
|
586
|
+
*
|
|
584
587
|
* Auto-generated from OpenAPI specification
|
|
585
588
|
*/
|
|
586
589
|
|
|
@@ -593,20 +596,85 @@ export { createAPIClient, defaultConfig } from './config'`;
|
|
|
593
596
|
generateFileHeader(title) {
|
|
594
597
|
return `/**
|
|
595
598
|
* ${title}
|
|
596
|
-
*
|
|
599
|
+
*
|
|
597
600
|
* Auto-generated from OpenAPI specification
|
|
598
601
|
* Do not edit manually
|
|
599
602
|
*/`;
|
|
600
603
|
}
|
|
601
604
|
getMethodName(endpoint) {
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
605
|
+
// Use semantic naming instead of raw operationId
|
|
606
|
+
return this.generateSemanticMethodName(endpoint);
|
|
607
|
+
}
|
|
608
|
+
generateSemanticMethodName(endpoint) {
|
|
609
|
+
// Extract resource from path
|
|
610
|
+
const pathSegments = endpoint.path.split('/').filter(s => s && !s.startsWith('{') && !['api', 'v1', 'v2'].includes(s));
|
|
611
|
+
const urlSegments = endpoint.path.split('/');
|
|
612
|
+
const hasPathParam = endpoint.path.includes('{');
|
|
613
|
+
const lastSegment = pathSegments[pathSegments.length - 1]?.replace(/-/g, '_') || 'resource';
|
|
614
|
+
const prevSegment = pathSegments[pathSegments.length - 2]?.replace(/-/g, '_');
|
|
615
|
+
|
|
616
|
+
// Auth endpoints - use action name directly
|
|
617
|
+
if (AUTH_ACTIONS.includes(lastSegment.toLowerCase())) {
|
|
618
|
+
return this.camelCase(lastSegment);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// Health/status singletons
|
|
622
|
+
if (HEALTH_ENDPOINTS.includes(lastSegment.toLowerCase())) {
|
|
623
|
+
return this.camelCase(lastSegment);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// User profile singletons
|
|
627
|
+
if (USER_PROFILE_ENDPOINTS.includes(lastSegment.toLowerCase())) {
|
|
628
|
+
return this.camelCase(`get_current_user`);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// Metadata endpoints - include parent resource to avoid collision
|
|
632
|
+
if (lastSegment.toLowerCase() === 'metadata' && prevSegment) {
|
|
633
|
+
return hasPathParam
|
|
634
|
+
? this.camelCase(`get_${singularize(prevSegment)}_metadata`)
|
|
635
|
+
: this.camelCase(`get_${prevSegment}_metadata`);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// Collection actions (e.g., /activities/search)
|
|
639
|
+
if (COLLECTION_ACTIONS.includes(lastSegment.toLowerCase()) && prevSegment) {
|
|
640
|
+
return this.camelCase(`${lastSegment}_${prevSegment}`);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// Determine resource name - for nested paths, include context
|
|
644
|
+
let resource = lastSegment;
|
|
645
|
+
if (pathSegments.length > 2 && hasPathParam) {
|
|
646
|
+
// e.g., /accounts/{id}/stages/allowed → get_account_allowed_stages
|
|
647
|
+
const parentResource = pathSegments.find((s, i) => {
|
|
648
|
+
const nextSeg = urlSegments[urlSegments.findIndex(u => u === s) + 1];
|
|
649
|
+
return nextSeg?.startsWith('{');
|
|
650
|
+
});
|
|
651
|
+
if (parentResource && parentResource !== lastSegment) {
|
|
652
|
+
resource = `${singularize(parentResource)}_${lastSegment}`;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// Generate name based on HTTP method
|
|
657
|
+
switch (endpoint.method) {
|
|
658
|
+
case 'get':
|
|
659
|
+
return hasPathParam
|
|
660
|
+
? this.camelCase(`get_${singularize(resource)}`)
|
|
661
|
+
: this.camelCase(`list_${resource}`);
|
|
662
|
+
case 'post':
|
|
663
|
+
// Check for custom actions like /accounts/{id}/stage
|
|
664
|
+
const lastUrl = urlSegments[urlSegments.length - 1];
|
|
665
|
+
const prevUrl = urlSegments[urlSegments.length - 2];
|
|
666
|
+
if (!lastUrl.startsWith('{') && prevUrl?.startsWith('{')) {
|
|
667
|
+
return this.camelCase(`${lastUrl}_${singularize(prevSegment || resource)}`);
|
|
668
|
+
}
|
|
669
|
+
return this.camelCase(`create_${singularize(resource)}`);
|
|
670
|
+
case 'put':
|
|
671
|
+
case 'patch':
|
|
672
|
+
return this.camelCase(`update_${singularize(resource)}`);
|
|
673
|
+
case 'delete':
|
|
674
|
+
return this.camelCase(`delete_${singularize(resource)}`);
|
|
675
|
+
default:
|
|
676
|
+
return this.camelCase(`${endpoint.method}_${resource}`);
|
|
677
|
+
}
|
|
610
678
|
}
|
|
611
679
|
generatePathWithParams(endpoint) {
|
|
612
680
|
return endpoint.path.replace(/{([^}]+)}/g, '${$1}');
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Confidence Scorer (Stub)
|
|
3
|
+
*
|
|
4
|
+
* Scores generated hook names for quality/confidence.
|
|
5
|
+
* This is a minimal stub that returns high confidence for all names.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export class ConfidenceScorer {
|
|
9
|
+
constructor() {
|
|
10
|
+
this.scoringRules = [];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Score a generated hook name
|
|
15
|
+
* @returns {{ hookName: string, operationId: string, confidence: string, score: number, suggestions: string[] }}
|
|
16
|
+
*/
|
|
17
|
+
scoreHookName(hookName, operationId, path, method) {
|
|
18
|
+
// Simple heuristic: shorter names are better
|
|
19
|
+
const lengthScore = Math.max(0, 100 - hookName.length);
|
|
20
|
+
|
|
21
|
+
// Check for common anti-patterns
|
|
22
|
+
const hasApiV1 = hookName.toLowerCase().includes('apiv1');
|
|
23
|
+
const hasUnderscore = hookName.includes('_');
|
|
24
|
+
const isVeryLong = hookName.length > 40;
|
|
25
|
+
|
|
26
|
+
let score = lengthScore;
|
|
27
|
+
if (hasApiV1) score -= 30;
|
|
28
|
+
if (hasUnderscore) score -= 10;
|
|
29
|
+
if (isVeryLong) score -= 20;
|
|
30
|
+
|
|
31
|
+
// Normalize to 0-100
|
|
32
|
+
score = Math.max(0, Math.min(100, score));
|
|
33
|
+
|
|
34
|
+
// Determine confidence level
|
|
35
|
+
let confidence;
|
|
36
|
+
if (score >= 70) confidence = 'high';
|
|
37
|
+
else if (score >= 40) confidence = 'medium';
|
|
38
|
+
else confidence = 'low';
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
hookName,
|
|
42
|
+
operationId,
|
|
43
|
+
path,
|
|
44
|
+
method,
|
|
45
|
+
confidence,
|
|
46
|
+
score,
|
|
47
|
+
suggestions: confidence === 'low' ? [this.suggestBetterName(path, method)] : []
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
suggestBetterName(path, method) {
|
|
52
|
+
// Extract resource from path
|
|
53
|
+
const segments = path.split('/').filter(s => s && !s.startsWith('{') && !['api', 'v1'].includes(s));
|
|
54
|
+
const resource = segments[segments.length - 1]?.replace(/-/g, '') || 'resource';
|
|
55
|
+
|
|
56
|
+
// Capitalize first letter
|
|
57
|
+
const capitalizedResource = resource.charAt(0).toUpperCase() + resource.slice(1);
|
|
58
|
+
|
|
59
|
+
switch (method) {
|
|
60
|
+
case 'get':
|
|
61
|
+
return path.includes('{') ? `useGet${capitalizedResource}` : `use${capitalizedResource}`;
|
|
62
|
+
case 'post':
|
|
63
|
+
return `useCreate${capitalizedResource}`;
|
|
64
|
+
case 'put':
|
|
65
|
+
case 'patch':
|
|
66
|
+
return `useUpdate${capitalizedResource}`;
|
|
67
|
+
case 'delete':
|
|
68
|
+
return `useDelete${capitalizedResource}`;
|
|
69
|
+
default:
|
|
70
|
+
return `use${capitalizedResource}`;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
generateReport(scoredNames) {
|
|
75
|
+
const high = scoredNames.filter(s => s.confidence === 'high').length;
|
|
76
|
+
const medium = scoredNames.filter(s => s.confidence === 'medium').length;
|
|
77
|
+
const low = scoredNames.filter(s => s.confidence === 'low').length;
|
|
78
|
+
|
|
79
|
+
return `
|
|
80
|
+
Hook Name Quality Report
|
|
81
|
+
========================
|
|
82
|
+
Total hooks: ${scoredNames.length}
|
|
83
|
+
High confidence: ${high} (${Math.round(high/scoredNames.length*100)}%)
|
|
84
|
+
Medium confidence: ${medium} (${Math.round(medium/scoredNames.length*100)}%)
|
|
85
|
+
Low confidence: ${low} (${Math.round(low/scoredNames.length*100)}%)
|
|
86
|
+
|
|
87
|
+
${low > 0 ? 'Low confidence hooks that may need review:\n' +
|
|
88
|
+
scoredNames.filter(s => s.confidence === 'low')
|
|
89
|
+
.map(s => ` - ${s.hookName} → suggested: ${s.suggestions[0]}`)
|
|
90
|
+
.join('\n') : 'All hooks have acceptable names!'}
|
|
91
|
+
`;
|
|
92
|
+
}
|
|
93
|
+
}
|