qdadm 0.28.0 → 0.29.0
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/package.json +4 -2
- package/src/gen/FieldMapper.js +116 -0
- package/src/gen/StorageProfileFactory.js +109 -0
- package/src/gen/connectors/BaseConnector.js +142 -0
- package/src/gen/connectors/ManualConnector.js +385 -0
- package/src/gen/connectors/ManualConnector.test.js +499 -0
- package/src/gen/connectors/OpenAPIConnector.js +568 -0
- package/src/gen/connectors/OpenAPIConnector.test.js +737 -0
- package/src/gen/connectors/__fixtures__/sample-openapi.json +311 -0
- package/src/gen/connectors/index.js +11 -0
- package/src/gen/createManagers.js +224 -0
- package/src/gen/decorators.js +129 -0
- package/src/gen/generateManagers.js +266 -0
- package/src/gen/generateManagers.test.js +358 -0
- package/src/gen/index.js +45 -0
- package/src/gen/schema.js +221 -0
- package/src/gen/vite-plugin.js +105 -0
- package/src/generated/managers/testManager.js +45 -0
|
@@ -0,0 +1,737 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { OpenAPIConnector } from './OpenAPIConnector.js'
|
|
3
|
+
import sampleSpec from './__fixtures__/sample-openapi.json'
|
|
4
|
+
|
|
5
|
+
describe('OpenAPIConnector', () => {
|
|
6
|
+
describe('parse()', () => {
|
|
7
|
+
describe('entity extraction', () => {
|
|
8
|
+
it('extracts entities from standard REST paths', () => {
|
|
9
|
+
const connector = new OpenAPIConnector()
|
|
10
|
+
const result = connector.parse(sampleSpec)
|
|
11
|
+
|
|
12
|
+
const entityNames = result.map(e => e.name)
|
|
13
|
+
expect(entityNames).toContain('users')
|
|
14
|
+
expect(entityNames).toContain('posts')
|
|
15
|
+
expect(entityNames).toContain('categories')
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('extracts correct endpoints for entities', () => {
|
|
19
|
+
const connector = new OpenAPIConnector()
|
|
20
|
+
const result = connector.parse(sampleSpec)
|
|
21
|
+
|
|
22
|
+
const users = result.find(e => e.name === 'users')
|
|
23
|
+
const posts = result.find(e => e.name === 'posts')
|
|
24
|
+
|
|
25
|
+
expect(users.endpoint).toBe('/api/users')
|
|
26
|
+
expect(posts.endpoint).toBe('/api/posts')
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('does not extract non-matching paths', () => {
|
|
30
|
+
const connector = new OpenAPIConnector()
|
|
31
|
+
const result = connector.parse(sampleSpec)
|
|
32
|
+
|
|
33
|
+
// /internal/metrics should not match default patterns
|
|
34
|
+
const entityNames = result.map(e => e.name)
|
|
35
|
+
expect(entityNames).not.toContain('internal')
|
|
36
|
+
expect(entityNames).not.toContain('metrics')
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('extracts fields from response schemas', () => {
|
|
40
|
+
const connector = new OpenAPIConnector()
|
|
41
|
+
const result = connector.parse(sampleSpec)
|
|
42
|
+
|
|
43
|
+
const users = result.find(e => e.name === 'users')
|
|
44
|
+
expect(users.fields.id).toBeDefined()
|
|
45
|
+
expect(users.fields.email).toBeDefined()
|
|
46
|
+
expect(users.fields.name).toBeDefined()
|
|
47
|
+
expect(users.fields.created_at).toBeDefined()
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('merges fields from multiple operations', () => {
|
|
51
|
+
const connector = new OpenAPIConnector()
|
|
52
|
+
const result = connector.parse(sampleSpec)
|
|
53
|
+
|
|
54
|
+
const users = result.find(e => e.name === 'users')
|
|
55
|
+
// password comes from CreateUserRequest (POST)
|
|
56
|
+
expect(users.fields.password).toBeDefined()
|
|
57
|
+
})
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
describe('field type mapping', () => {
|
|
61
|
+
it('maps string to text', () => {
|
|
62
|
+
const connector = new OpenAPIConnector()
|
|
63
|
+
const result = connector.parse(sampleSpec)
|
|
64
|
+
|
|
65
|
+
const users = result.find(e => e.name === 'users')
|
|
66
|
+
expect(users.fields.name.type).toBe('text')
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('maps integer to number', () => {
|
|
70
|
+
const connector = new OpenAPIConnector()
|
|
71
|
+
const result = connector.parse(sampleSpec)
|
|
72
|
+
|
|
73
|
+
const users = result.find(e => e.name === 'users')
|
|
74
|
+
expect(users.fields.id.type).toBe('number')
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('maps string with email format to email', () => {
|
|
78
|
+
const connector = new OpenAPIConnector()
|
|
79
|
+
const result = connector.parse(sampleSpec)
|
|
80
|
+
|
|
81
|
+
const users = result.find(e => e.name === 'users')
|
|
82
|
+
expect(users.fields.email.type).toBe('email')
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('maps string with date-time format to datetime', () => {
|
|
86
|
+
const connector = new OpenAPIConnector()
|
|
87
|
+
const result = connector.parse(sampleSpec)
|
|
88
|
+
|
|
89
|
+
const users = result.find(e => e.name === 'users')
|
|
90
|
+
expect(users.fields.created_at.type).toBe('datetime')
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('maps string with date format to date', () => {
|
|
94
|
+
const connector = new OpenAPIConnector()
|
|
95
|
+
const result = connector.parse(sampleSpec)
|
|
96
|
+
|
|
97
|
+
const posts = result.find(e => e.name === 'posts')
|
|
98
|
+
expect(posts.fields.published_at.type).toBe('date')
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('maps string with uri format to url', () => {
|
|
102
|
+
const connector = new OpenAPIConnector()
|
|
103
|
+
const result = connector.parse(sampleSpec)
|
|
104
|
+
|
|
105
|
+
const users = result.find(e => e.name === 'users')
|
|
106
|
+
expect(users.fields.website.type).toBe('url')
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('maps string with uuid format to uuid', () => {
|
|
110
|
+
const connector = new OpenAPIConnector()
|
|
111
|
+
const result = connector.parse(sampleSpec)
|
|
112
|
+
|
|
113
|
+
const users = result.find(e => e.name === 'users')
|
|
114
|
+
expect(users.fields.uuid.type).toBe('uuid')
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('maps boolean to boolean', () => {
|
|
118
|
+
const connector = new OpenAPIConnector()
|
|
119
|
+
const result = connector.parse(sampleSpec)
|
|
120
|
+
|
|
121
|
+
const users = result.find(e => e.name === 'users')
|
|
122
|
+
expect(users.fields.active.type).toBe('boolean')
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('maps array to array', () => {
|
|
126
|
+
const connector = new OpenAPIConnector()
|
|
127
|
+
const result = connector.parse(sampleSpec)
|
|
128
|
+
|
|
129
|
+
const users = result.find(e => e.name === 'users')
|
|
130
|
+
expect(users.fields.tags.type).toBe('array')
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it('maps object to object', () => {
|
|
134
|
+
const connector = new OpenAPIConnector()
|
|
135
|
+
const result = connector.parse(sampleSpec)
|
|
136
|
+
|
|
137
|
+
const users = result.find(e => e.name === 'users')
|
|
138
|
+
expect(users.fields.metadata.type).toBe('object')
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it('maps enum fields to select', () => {
|
|
142
|
+
const connector = new OpenAPIConnector()
|
|
143
|
+
const result = connector.parse(sampleSpec)
|
|
144
|
+
|
|
145
|
+
const users = result.find(e => e.name === 'users')
|
|
146
|
+
expect(users.fields.role.type).toBe('select')
|
|
147
|
+
expect(users.fields.role.enum).toEqual(['admin', 'user', 'guest'])
|
|
148
|
+
})
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
describe('field properties', () => {
|
|
152
|
+
it('extracts required fields', () => {
|
|
153
|
+
const connector = new OpenAPIConnector()
|
|
154
|
+
const result = connector.parse(sampleSpec)
|
|
155
|
+
|
|
156
|
+
const users = result.find(e => e.name === 'users')
|
|
157
|
+
expect(users.fields.email.required).toBe(true)
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
it('extracts readOnly flag', () => {
|
|
161
|
+
const connector = new OpenAPIConnector()
|
|
162
|
+
const result = connector.parse(sampleSpec)
|
|
163
|
+
|
|
164
|
+
const users = result.find(e => e.name === 'users')
|
|
165
|
+
expect(users.fields.id.readOnly).toBe(true)
|
|
166
|
+
expect(users.fields.created_at.readOnly).toBe(true)
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
it('extracts description as label', () => {
|
|
170
|
+
const connector = new OpenAPIConnector()
|
|
171
|
+
const result = connector.parse(sampleSpec)
|
|
172
|
+
|
|
173
|
+
const users = result.find(e => e.name === 'users')
|
|
174
|
+
expect(users.fields.id.label).toBe('User ID')
|
|
175
|
+
expect(users.fields.email.label).toBe('Email address')
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
it('extracts default values', () => {
|
|
179
|
+
const connector = new OpenAPIConnector()
|
|
180
|
+
const result = connector.parse(sampleSpec)
|
|
181
|
+
|
|
182
|
+
const users = result.find(e => e.name === 'users')
|
|
183
|
+
expect(users.fields.active.default).toBe(true)
|
|
184
|
+
expect(users.fields.role.default).toBe('user')
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
it('extracts format property', () => {
|
|
188
|
+
const connector = new OpenAPIConnector()
|
|
189
|
+
const result = connector.parse(sampleSpec)
|
|
190
|
+
|
|
191
|
+
const users = result.find(e => e.name === 'users')
|
|
192
|
+
expect(users.fields.email.format).toBe('email')
|
|
193
|
+
expect(users.fields.created_at.format).toBe('date-time')
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
it('extracts enum values', () => {
|
|
197
|
+
const connector = new OpenAPIConnector()
|
|
198
|
+
const result = connector.parse(sampleSpec)
|
|
199
|
+
|
|
200
|
+
const posts = result.find(e => e.name === 'posts')
|
|
201
|
+
expect(posts.fields.status.enum).toEqual(['draft', 'published', 'archived'])
|
|
202
|
+
})
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
describe('nested object handling', () => {
|
|
206
|
+
it('extracts nested object fields with dot notation', () => {
|
|
207
|
+
const connector = new OpenAPIConnector()
|
|
208
|
+
const result = connector.parse(sampleSpec)
|
|
209
|
+
|
|
210
|
+
const users = result.find(e => e.name === 'users')
|
|
211
|
+
expect(users.fields['profile.bio']).toBeDefined()
|
|
212
|
+
expect(users.fields['profile.avatar']).toBeDefined()
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
it('maps nested field types correctly', () => {
|
|
216
|
+
const connector = new OpenAPIConnector()
|
|
217
|
+
const result = connector.parse(sampleSpec)
|
|
218
|
+
|
|
219
|
+
const users = result.find(e => e.name === 'users')
|
|
220
|
+
expect(users.fields['profile.bio'].type).toBe('text')
|
|
221
|
+
expect(users.fields['profile.avatar'].type).toBe('url')
|
|
222
|
+
})
|
|
223
|
+
})
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
describe('dataWrapper option', () => {
|
|
227
|
+
it('unwraps data from default "data" property', () => {
|
|
228
|
+
const connector = new OpenAPIConnector()
|
|
229
|
+
const result = connector.parse(sampleSpec)
|
|
230
|
+
|
|
231
|
+
const users = result.find(e => e.name === 'users')
|
|
232
|
+
// Should have User fields, not "data" field
|
|
233
|
+
expect(users.fields.id).toBeDefined()
|
|
234
|
+
expect(users.fields.data).toBeUndefined()
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
it('respects custom dataWrapper', () => {
|
|
238
|
+
const spec = {
|
|
239
|
+
openapi: '3.0.0',
|
|
240
|
+
paths: {
|
|
241
|
+
'/api/items': {
|
|
242
|
+
get: {
|
|
243
|
+
responses: {
|
|
244
|
+
'200': {
|
|
245
|
+
content: {
|
|
246
|
+
'application/json': {
|
|
247
|
+
schema: {
|
|
248
|
+
type: 'object',
|
|
249
|
+
properties: {
|
|
250
|
+
result: {
|
|
251
|
+
type: 'array',
|
|
252
|
+
items: {
|
|
253
|
+
type: 'object',
|
|
254
|
+
properties: {
|
|
255
|
+
id: { type: 'integer' },
|
|
256
|
+
name: { type: 'string' }
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const connector = new OpenAPIConnector({ dataWrapper: 'result' })
|
|
272
|
+
const result = connector.parse(spec)
|
|
273
|
+
|
|
274
|
+
const items = result.find(e => e.name === 'items')
|
|
275
|
+
expect(items.fields.id).toBeDefined()
|
|
276
|
+
expect(items.fields.name).toBeDefined()
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
it('handles missing dataWrapper gracefully', () => {
|
|
280
|
+
const spec = {
|
|
281
|
+
openapi: '3.0.0',
|
|
282
|
+
paths: {
|
|
283
|
+
'/api/items': {
|
|
284
|
+
get: {
|
|
285
|
+
responses: {
|
|
286
|
+
'200': {
|
|
287
|
+
content: {
|
|
288
|
+
'application/json': {
|
|
289
|
+
schema: {
|
|
290
|
+
type: 'object',
|
|
291
|
+
properties: {
|
|
292
|
+
id: { type: 'integer' },
|
|
293
|
+
name: { type: 'string' }
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const connector = new OpenAPIConnector({ dataWrapper: 'data' })
|
|
306
|
+
const result = connector.parse(spec)
|
|
307
|
+
|
|
308
|
+
// Should fall back to full schema
|
|
309
|
+
const items = result.find(e => e.name === 'items')
|
|
310
|
+
expect(items.fields.id).toBeDefined()
|
|
311
|
+
expect(items.fields.name).toBeDefined()
|
|
312
|
+
})
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
describe('pathPatterns option', () => {
|
|
316
|
+
it('uses custom path patterns', () => {
|
|
317
|
+
const spec = {
|
|
318
|
+
openapi: '3.0.0',
|
|
319
|
+
paths: {
|
|
320
|
+
'/v2/admin/users': {
|
|
321
|
+
get: {
|
|
322
|
+
responses: {
|
|
323
|
+
'200': {
|
|
324
|
+
content: {
|
|
325
|
+
'application/json': {
|
|
326
|
+
schema: {
|
|
327
|
+
type: 'object',
|
|
328
|
+
properties: {
|
|
329
|
+
data: {
|
|
330
|
+
type: 'array',
|
|
331
|
+
items: {
|
|
332
|
+
type: 'object',
|
|
333
|
+
properties: {
|
|
334
|
+
id: { type: 'integer' }
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
},
|
|
346
|
+
'/v2/admin/users/{id}': {
|
|
347
|
+
get: {
|
|
348
|
+
responses: {
|
|
349
|
+
'200': {
|
|
350
|
+
content: {
|
|
351
|
+
'application/json': {
|
|
352
|
+
schema: {
|
|
353
|
+
type: 'object',
|
|
354
|
+
properties: {
|
|
355
|
+
data: {
|
|
356
|
+
type: 'object',
|
|
357
|
+
properties: {
|
|
358
|
+
id: { type: 'integer' }
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const connector = new OpenAPIConnector({
|
|
373
|
+
pathPatterns: [
|
|
374
|
+
/^\/v2\/admin\/([a-z-]+)\/?$/,
|
|
375
|
+
/^\/v2\/admin\/([a-z-]+)\/\{[^}]+\}$/
|
|
376
|
+
]
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
const result = connector.parse(spec)
|
|
380
|
+
|
|
381
|
+
expect(result).toHaveLength(1)
|
|
382
|
+
expect(result[0].name).toBe('users')
|
|
383
|
+
expect(result[0].endpoint).toBe('/v2/admin/users')
|
|
384
|
+
})
|
|
385
|
+
|
|
386
|
+
it('ignores paths that do not match patterns', () => {
|
|
387
|
+
const spec = {
|
|
388
|
+
openapi: '3.0.0',
|
|
389
|
+
paths: {
|
|
390
|
+
'/health': {
|
|
391
|
+
get: {
|
|
392
|
+
responses: {
|
|
393
|
+
'200': {
|
|
394
|
+
content: {
|
|
395
|
+
'application/json': {
|
|
396
|
+
schema: { type: 'object' }
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const connector = new OpenAPIConnector()
|
|
407
|
+
const result = connector.parse(spec)
|
|
408
|
+
|
|
409
|
+
expect(result).toHaveLength(0)
|
|
410
|
+
})
|
|
411
|
+
})
|
|
412
|
+
|
|
413
|
+
describe('operationFilter option', () => {
|
|
414
|
+
it('filters operations based on custom function', () => {
|
|
415
|
+
const connector = new OpenAPIConnector({
|
|
416
|
+
operationFilter: (path, method, op) => !op.tags?.includes('internal')
|
|
417
|
+
})
|
|
418
|
+
|
|
419
|
+
const result = connector.parse(sampleSpec)
|
|
420
|
+
|
|
421
|
+
const entityNames = result.map(e => e.name)
|
|
422
|
+
expect(entityNames).not.toContain('metrics')
|
|
423
|
+
})
|
|
424
|
+
|
|
425
|
+
it('allows all operations when no filter provided', () => {
|
|
426
|
+
// Add a spec with all operations having tags
|
|
427
|
+
const spec = {
|
|
428
|
+
openapi: '3.0.0',
|
|
429
|
+
paths: {
|
|
430
|
+
'/api/items': {
|
|
431
|
+
get: {
|
|
432
|
+
tags: ['public'],
|
|
433
|
+
responses: {
|
|
434
|
+
'200': {
|
|
435
|
+
content: {
|
|
436
|
+
'application/json': {
|
|
437
|
+
schema: {
|
|
438
|
+
type: 'object',
|
|
439
|
+
properties: {
|
|
440
|
+
data: {
|
|
441
|
+
type: 'array',
|
|
442
|
+
items: {
|
|
443
|
+
type: 'object',
|
|
444
|
+
properties: { id: { type: 'integer' } }
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const connector = new OpenAPIConnector()
|
|
459
|
+
const result = connector.parse(spec)
|
|
460
|
+
|
|
461
|
+
expect(result).toHaveLength(1)
|
|
462
|
+
expect(result[0].name).toBe('items')
|
|
463
|
+
})
|
|
464
|
+
})
|
|
465
|
+
|
|
466
|
+
describe('customMappings option', () => {
|
|
467
|
+
it('uses custom format mappings', () => {
|
|
468
|
+
const spec = {
|
|
469
|
+
openapi: '3.0.0',
|
|
470
|
+
paths: {
|
|
471
|
+
'/api/items': {
|
|
472
|
+
get: {
|
|
473
|
+
responses: {
|
|
474
|
+
'200': {
|
|
475
|
+
content: {
|
|
476
|
+
'application/json': {
|
|
477
|
+
schema: {
|
|
478
|
+
type: 'object',
|
|
479
|
+
properties: {
|
|
480
|
+
data: {
|
|
481
|
+
type: 'array',
|
|
482
|
+
items: {
|
|
483
|
+
type: 'object',
|
|
484
|
+
properties: {
|
|
485
|
+
phone: {
|
|
486
|
+
type: 'string',
|
|
487
|
+
format: 'phone'
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const connector = new OpenAPIConnector({
|
|
504
|
+
customMappings: {
|
|
505
|
+
formats: { phone: 'text' }
|
|
506
|
+
}
|
|
507
|
+
})
|
|
508
|
+
|
|
509
|
+
const result = connector.parse(spec)
|
|
510
|
+
const items = result.find(e => e.name === 'items')
|
|
511
|
+
|
|
512
|
+
expect(items.fields.phone.type).toBe('text')
|
|
513
|
+
})
|
|
514
|
+
})
|
|
515
|
+
|
|
516
|
+
describe('validation', () => {
|
|
517
|
+
it('throws for null source', () => {
|
|
518
|
+
const connector = new OpenAPIConnector()
|
|
519
|
+
expect(() => connector.parse(null)).toThrow('source must be an object')
|
|
520
|
+
})
|
|
521
|
+
|
|
522
|
+
it('throws for undefined source', () => {
|
|
523
|
+
const connector = new OpenAPIConnector()
|
|
524
|
+
expect(() => connector.parse(undefined)).toThrow('source must be an object')
|
|
525
|
+
})
|
|
526
|
+
|
|
527
|
+
it('throws for source without paths', () => {
|
|
528
|
+
const connector = new OpenAPIConnector()
|
|
529
|
+
expect(() => connector.parse({ openapi: '3.0.0' })).toThrow('source must have paths object')
|
|
530
|
+
})
|
|
531
|
+
|
|
532
|
+
it('throws for paths that is not an object', () => {
|
|
533
|
+
const connector = new OpenAPIConnector()
|
|
534
|
+
expect(() => connector.parse({ paths: 'invalid' })).toThrow('source must have paths object')
|
|
535
|
+
})
|
|
536
|
+
})
|
|
537
|
+
|
|
538
|
+
describe('parseWithWarnings()', () => {
|
|
539
|
+
it('returns schemas and warnings', () => {
|
|
540
|
+
const connector = new OpenAPIConnector()
|
|
541
|
+
const result = connector.parseWithWarnings(sampleSpec)
|
|
542
|
+
|
|
543
|
+
expect(result.schemas).toBeDefined()
|
|
544
|
+
expect(Array.isArray(result.schemas)).toBe(true)
|
|
545
|
+
expect(result.warnings).toBeDefined()
|
|
546
|
+
expect(Array.isArray(result.warnings)).toBe(true)
|
|
547
|
+
})
|
|
548
|
+
|
|
549
|
+
it('collects warnings for unresolved refs', () => {
|
|
550
|
+
const spec = {
|
|
551
|
+
openapi: '3.0.0',
|
|
552
|
+
paths: {
|
|
553
|
+
'/api/items': {
|
|
554
|
+
get: {
|
|
555
|
+
responses: {
|
|
556
|
+
'200': {
|
|
557
|
+
content: {
|
|
558
|
+
'application/json': {
|
|
559
|
+
schema: {
|
|
560
|
+
type: 'object',
|
|
561
|
+
properties: {
|
|
562
|
+
data: {
|
|
563
|
+
type: 'array',
|
|
564
|
+
items: {
|
|
565
|
+
type: 'object',
|
|
566
|
+
properties: {
|
|
567
|
+
related: { $ref: '#/components/schemas/Missing' }
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
const connector = new OpenAPIConnector()
|
|
583
|
+
const result = connector.parseWithWarnings(spec)
|
|
584
|
+
|
|
585
|
+
expect(result.warnings.some(w => w.code === 'UNRESOLVED_REF')).toBe(true)
|
|
586
|
+
})
|
|
587
|
+
})
|
|
588
|
+
|
|
589
|
+
describe('extensions option', () => {
|
|
590
|
+
it('adds extensions to all parsed entities', () => {
|
|
591
|
+
const connector = new OpenAPIConnector({
|
|
592
|
+
extensions: { source: 'openapi', custom: true }
|
|
593
|
+
})
|
|
594
|
+
|
|
595
|
+
const result = connector.parse(sampleSpec)
|
|
596
|
+
|
|
597
|
+
for (const entity of result) {
|
|
598
|
+
expect(entity.extensions).toEqual({ source: 'openapi', custom: true })
|
|
599
|
+
}
|
|
600
|
+
})
|
|
601
|
+
|
|
602
|
+
it('does not add extensions when empty', () => {
|
|
603
|
+
const connector = new OpenAPIConnector()
|
|
604
|
+
const result = connector.parse(sampleSpec)
|
|
605
|
+
|
|
606
|
+
for (const entity of result) {
|
|
607
|
+
expect(entity.extensions).toBeUndefined()
|
|
608
|
+
}
|
|
609
|
+
})
|
|
610
|
+
})
|
|
611
|
+
|
|
612
|
+
describe('constructor options', () => {
|
|
613
|
+
it('uses constructor name by default', () => {
|
|
614
|
+
const connector = new OpenAPIConnector()
|
|
615
|
+
expect(connector.name).toBe('OpenAPIConnector')
|
|
616
|
+
})
|
|
617
|
+
|
|
618
|
+
it('allows custom name', () => {
|
|
619
|
+
const connector = new OpenAPIConnector({ name: 'MyOpenAPI' })
|
|
620
|
+
expect(connector.name).toBe('MyOpenAPI')
|
|
621
|
+
})
|
|
622
|
+
|
|
623
|
+
it('defaults dataWrapper to "data"', () => {
|
|
624
|
+
const connector = new OpenAPIConnector()
|
|
625
|
+
expect(connector.dataWrapper).toBe('data')
|
|
626
|
+
})
|
|
627
|
+
|
|
628
|
+
it('treats null dataWrapper as default', () => {
|
|
629
|
+
// null coalesces to 'data', use empty string to disable unwrapping
|
|
630
|
+
const connector = new OpenAPIConnector({ dataWrapper: null })
|
|
631
|
+
expect(connector.dataWrapper).toBe('data')
|
|
632
|
+
})
|
|
633
|
+
|
|
634
|
+
it('allows empty string dataWrapper to disable unwrapping', () => {
|
|
635
|
+
const connector = new OpenAPIConnector({ dataWrapper: '' })
|
|
636
|
+
expect(connector.dataWrapper).toBe('')
|
|
637
|
+
})
|
|
638
|
+
|
|
639
|
+
it('defaults to empty customMappings', () => {
|
|
640
|
+
const connector = new OpenAPIConnector()
|
|
641
|
+
expect(connector.customMappings).toEqual({})
|
|
642
|
+
})
|
|
643
|
+
|
|
644
|
+
it('defaults to no operationFilter', () => {
|
|
645
|
+
const connector = new OpenAPIConnector()
|
|
646
|
+
expect(connector.operationFilter).toBeNull()
|
|
647
|
+
})
|
|
648
|
+
|
|
649
|
+
it('uses default path patterns', () => {
|
|
650
|
+
const connector = new OpenAPIConnector()
|
|
651
|
+
expect(connector.pathPatterns).toHaveLength(2)
|
|
652
|
+
})
|
|
653
|
+
})
|
|
654
|
+
|
|
655
|
+
describe('$ref resolution', () => {
|
|
656
|
+
it('resolves component schema references', () => {
|
|
657
|
+
const connector = new OpenAPIConnector()
|
|
658
|
+
const result = connector.parse(sampleSpec)
|
|
659
|
+
|
|
660
|
+
const users = result.find(e => e.name === 'users')
|
|
661
|
+
// Fields from User schema via $ref
|
|
662
|
+
expect(users.fields.email).toBeDefined()
|
|
663
|
+
expect(users.fields.email.type).toBe('email')
|
|
664
|
+
})
|
|
665
|
+
|
|
666
|
+
it('resolves nested $ref in response content', () => {
|
|
667
|
+
const spec = {
|
|
668
|
+
openapi: '3.0.0',
|
|
669
|
+
paths: {
|
|
670
|
+
'/api/items': {
|
|
671
|
+
get: {
|
|
672
|
+
responses: {
|
|
673
|
+
'200': {
|
|
674
|
+
content: {
|
|
675
|
+
'application/json': {
|
|
676
|
+
schema: {
|
|
677
|
+
$ref: '#/components/schemas/ItemsResponse'
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
},
|
|
686
|
+
components: {
|
|
687
|
+
schemas: {
|
|
688
|
+
ItemsResponse: {
|
|
689
|
+
type: 'object',
|
|
690
|
+
properties: {
|
|
691
|
+
data: {
|
|
692
|
+
type: 'array',
|
|
693
|
+
items: { $ref: '#/components/schemas/Item' }
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
},
|
|
697
|
+
Item: {
|
|
698
|
+
type: 'object',
|
|
699
|
+
properties: {
|
|
700
|
+
id: { type: 'integer' },
|
|
701
|
+
name: { type: 'string' }
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
const connector = new OpenAPIConnector()
|
|
709
|
+
const result = connector.parse(spec)
|
|
710
|
+
|
|
711
|
+
const items = result.find(e => e.name === 'items')
|
|
712
|
+
expect(items.fields.id).toBeDefined()
|
|
713
|
+
expect(items.fields.name).toBeDefined()
|
|
714
|
+
})
|
|
715
|
+
})
|
|
716
|
+
|
|
717
|
+
describe('list operation detection', () => {
|
|
718
|
+
it('detects list operation from collection path', () => {
|
|
719
|
+
const connector = new OpenAPIConnector()
|
|
720
|
+
const result = connector.parse(sampleSpec)
|
|
721
|
+
|
|
722
|
+
// Should extract array item schema, not the array itself
|
|
723
|
+
const users = result.find(e => e.name === 'users')
|
|
724
|
+
expect(users.fields.id).toBeDefined()
|
|
725
|
+
expect(users.fields.id.type).toBe('number')
|
|
726
|
+
})
|
|
727
|
+
|
|
728
|
+
it('detects list operation from pagination indicators', () => {
|
|
729
|
+
const connector = new OpenAPIConnector()
|
|
730
|
+
const result = connector.parse(sampleSpec)
|
|
731
|
+
|
|
732
|
+
// Posts have pagination in response
|
|
733
|
+
const posts = result.find(e => e.name === 'posts')
|
|
734
|
+
expect(posts.fields.id).toBeDefined()
|
|
735
|
+
})
|
|
736
|
+
})
|
|
737
|
+
})
|