@ebarahona/loopback-openapi-v3 1.0.0 → 1.1.1
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/README.md +106 -72
- package/dist/errors.d.ts +19 -0
- package/dist/errors.js +37 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -1
- package/dist/keys.d.ts +1 -1
- package/dist/keys.js.map +1 -1
- package/dist/openapi-version.component.d.ts +2 -1
- package/dist/openapi-version.component.js.map +1 -1
- package/dist/openapi-version.enhancer.js.map +1 -1
- package/dist/transform.d.ts +1 -1
- package/dist/transform.js +58 -17
- package/dist/transform.js.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +22 -6
- package/src/__tests__/install-hooks-script.spec.ts +267 -0
- package/src/__tests__/openapi-version.enhancer.spec.ts +1 -1
- package/src/__tests__/transform.spec.ts +122 -51
- package/src/errors.ts +83 -0
- package/src/index.ts +26 -6
- package/src/keys.ts +17 -1
- package/src/openapi-version.component.ts +4 -5
- package/src/openapi-version.enhancer.ts +8 -9
- package/src/transform.ts +209 -41
- package/src/types.ts +16 -0
|
@@ -21,8 +21,12 @@ describe('parseVersion', () => {
|
|
|
21
21
|
});
|
|
22
22
|
|
|
23
23
|
it('rejects non-v3 versions', () => {
|
|
24
|
-
expect(() => parseVersion('2.0.0')).toThrow(
|
|
25
|
-
|
|
24
|
+
expect(() => parseVersion('2.0.0')).toThrow(
|
|
25
|
+
'Unsupported OpenAPI major version',
|
|
26
|
+
);
|
|
27
|
+
expect(() => parseVersion('4.0.0')).toThrow(
|
|
28
|
+
'Unsupported OpenAPI major version',
|
|
29
|
+
);
|
|
26
30
|
});
|
|
27
31
|
});
|
|
28
32
|
|
|
@@ -74,8 +78,9 @@ describe('nullable edge cases', () => {
|
|
|
74
78
|
},
|
|
75
79
|
};
|
|
76
80
|
|
|
77
|
-
const {spec: result
|
|
78
|
-
const mixed = (result.components as Record<string, unknown>)
|
|
81
|
+
const {spec: result} = transformOpenApiSpec(spec, {version: '3.1.0'});
|
|
82
|
+
const mixed = (result.components as Record<string, unknown>)
|
|
83
|
+
?.schemas as Record<string, unknown>;
|
|
79
84
|
const schema = mixed?.Mixed as Record<string, unknown>;
|
|
80
85
|
expect(schema).not.toHaveProperty('nullable');
|
|
81
86
|
expect(schema.oneOf).toEqual([
|
|
@@ -100,8 +105,13 @@ describe('nullable edge cases', () => {
|
|
|
100
105
|
},
|
|
101
106
|
};
|
|
102
107
|
|
|
103
|
-
const {spec: result
|
|
104
|
-
const schema = (
|
|
108
|
+
const {spec: result} = transformOpenApiSpec(spec, {version: '3.1.0'});
|
|
109
|
+
const schema = (
|
|
110
|
+
(result.components as Record<string, unknown>)?.schemas as Record<
|
|
111
|
+
string,
|
|
112
|
+
unknown
|
|
113
|
+
>
|
|
114
|
+
)?.Mixed as Record<string, unknown>;
|
|
105
115
|
expect(schema.anyOf).toEqual([{type: 'string'}, {type: 'null'}]);
|
|
106
116
|
});
|
|
107
117
|
|
|
@@ -120,8 +130,13 @@ describe('nullable edge cases', () => {
|
|
|
120
130
|
},
|
|
121
131
|
};
|
|
122
132
|
|
|
123
|
-
const {spec: result
|
|
124
|
-
const schema = (
|
|
133
|
+
const {spec: result} = transformOpenApiSpec(spec, {version: '3.1.0'});
|
|
134
|
+
const schema = (
|
|
135
|
+
(result.components as Record<string, unknown>)?.schemas as Record<
|
|
136
|
+
string,
|
|
137
|
+
unknown
|
|
138
|
+
>
|
|
139
|
+
)?.Mixed as Record<string, unknown>;
|
|
125
140
|
expect(schema).not.toHaveProperty('allOf');
|
|
126
141
|
expect(schema.anyOf).toEqual([
|
|
127
142
|
{allOf: [{type: 'object'}, {properties: {id: {type: 'integer'}}}]},
|
|
@@ -141,8 +156,13 @@ describe('nullable edge cases', () => {
|
|
|
141
156
|
},
|
|
142
157
|
};
|
|
143
158
|
|
|
144
|
-
const {spec: result
|
|
145
|
-
const schema = (
|
|
159
|
+
const {spec: result} = transformOpenApiSpec(spec, {version: '3.1.0'});
|
|
160
|
+
const schema = (
|
|
161
|
+
(result.components as Record<string, unknown>)?.schemas as Record<
|
|
162
|
+
string,
|
|
163
|
+
unknown
|
|
164
|
+
>
|
|
165
|
+
)?.Empty as Record<string, unknown>;
|
|
146
166
|
expect(schema.type).toBe('null');
|
|
147
167
|
expect(schema).not.toHaveProperty('nullable');
|
|
148
168
|
});
|
|
@@ -161,8 +181,13 @@ describe('nullable edge cases', () => {
|
|
|
161
181
|
},
|
|
162
182
|
};
|
|
163
183
|
|
|
164
|
-
const {spec: result
|
|
165
|
-
const schema = (
|
|
184
|
+
const {spec: result} = transformOpenApiSpec(spec, {version: '3.0.0'});
|
|
185
|
+
const schema = (
|
|
186
|
+
(result.components as Record<string, unknown>)?.schemas as Record<
|
|
187
|
+
string,
|
|
188
|
+
unknown
|
|
189
|
+
>
|
|
190
|
+
)?.Mixed as Record<string, unknown>;
|
|
166
191
|
expect(schema.nullable).toBe(true);
|
|
167
192
|
expect(schema.oneOf).toEqual([{type: 'string'}, {type: 'integer'}]);
|
|
168
193
|
});
|
|
@@ -189,8 +214,9 @@ describe('component-level schema traversal', () => {
|
|
|
189
214
|
},
|
|
190
215
|
};
|
|
191
216
|
|
|
192
|
-
const {spec: result
|
|
193
|
-
const params = (result.components as Record<string, unknown>)
|
|
217
|
+
const {spec: result} = transformOpenApiSpec(spec, {version: '3.1.0'});
|
|
218
|
+
const params = (result.components as Record<string, unknown>)
|
|
219
|
+
?.parameters as Record<string, unknown>;
|
|
194
220
|
const param = params?.FilterParam as Record<string, unknown>;
|
|
195
221
|
const schema = param?.schema as Record<string, unknown>;
|
|
196
222
|
expect(schema?.type).toEqual(['string', 'null']);
|
|
@@ -219,8 +245,9 @@ describe('component-level schema traversal', () => {
|
|
|
219
245
|
},
|
|
220
246
|
};
|
|
221
247
|
|
|
222
|
-
const {spec: result
|
|
223
|
-
const bodies = (result.components as Record<string, unknown>)
|
|
248
|
+
const {spec: result} = transformOpenApiSpec(spec, {version: '3.1.0'});
|
|
249
|
+
const bodies = (result.components as Record<string, unknown>)
|
|
250
|
+
?.requestBodies as Record<string, unknown>;
|
|
224
251
|
const body = bodies?.CreateItem as Record<string, unknown>;
|
|
225
252
|
const content = body?.content as Record<string, unknown>;
|
|
226
253
|
const json = content?.['application/json'] as Record<string, unknown>;
|
|
@@ -249,8 +276,9 @@ describe('component-level schema traversal', () => {
|
|
|
249
276
|
},
|
|
250
277
|
};
|
|
251
278
|
|
|
252
|
-
const {spec: result
|
|
253
|
-
const responses = (result.components as Record<string, unknown>)
|
|
279
|
+
const {spec: result} = transformOpenApiSpec(spec, {version: '3.1.0'});
|
|
280
|
+
const responses = (result.components as Record<string, unknown>)
|
|
281
|
+
?.responses as Record<string, unknown>;
|
|
254
282
|
const resp = responses?.ItemResponse as Record<string, unknown>;
|
|
255
283
|
const content = resp?.content as Record<string, unknown>;
|
|
256
284
|
const json = content?.['application/json'] as Record<string, unknown>;
|
|
@@ -272,8 +300,9 @@ describe('component-level schema traversal', () => {
|
|
|
272
300
|
},
|
|
273
301
|
};
|
|
274
302
|
|
|
275
|
-
const {spec: result
|
|
276
|
-
const headers = (result.components as Record<string, unknown>)
|
|
303
|
+
const {spec: result} = transformOpenApiSpec(spec, {version: '3.1.0'});
|
|
304
|
+
const headers = (result.components as Record<string, unknown>)
|
|
305
|
+
?.headers as Record<string, unknown>;
|
|
277
306
|
const header = headers?.XRateLimit as Record<string, unknown>;
|
|
278
307
|
const schema = header?.schema as Record<string, unknown>;
|
|
279
308
|
expect(schema?.type).toEqual(['integer', 'null']);
|
|
@@ -306,7 +335,7 @@ describe('webhooks', () => {
|
|
|
306
335
|
},
|
|
307
336
|
};
|
|
308
337
|
|
|
309
|
-
const {spec: result
|
|
338
|
+
const {spec: result} = transformOpenApiSpec(spec, {version: '3.1.0'});
|
|
310
339
|
const webhooks = result.webhooks as Record<string, unknown>;
|
|
311
340
|
const hook = webhooks?.newItem as Record<string, unknown>;
|
|
312
341
|
const post = hook?.post as Record<string, unknown>;
|
|
@@ -331,7 +360,7 @@ describe('webhooks', () => {
|
|
|
331
360
|
},
|
|
332
361
|
};
|
|
333
362
|
|
|
334
|
-
const {spec: result
|
|
363
|
+
const {spec: result} = transformOpenApiSpec(spec, {version: '3.1.0'});
|
|
335
364
|
const webhooks = result.webhooks as Record<string, unknown>;
|
|
336
365
|
const hook = webhooks?.newItem as Record<string, unknown>;
|
|
337
366
|
expect(hook).not.toHaveProperty('query');
|
|
@@ -351,7 +380,9 @@ describe('webhooks', () => {
|
|
|
351
380
|
},
|
|
352
381
|
};
|
|
353
382
|
|
|
354
|
-
const {spec: result, warnings} = transformOpenApiSpec(spec, {
|
|
383
|
+
const {spec: result, warnings} = transformOpenApiSpec(spec, {
|
|
384
|
+
version: '3.0.0',
|
|
385
|
+
});
|
|
355
386
|
expect(result).not.toHaveProperty('webhooks');
|
|
356
387
|
expect(warnings.some(w => w.field === 'webhooks')).toBe(true);
|
|
357
388
|
});
|
|
@@ -392,13 +423,16 @@ describe('callbacks', () => {
|
|
|
392
423
|
},
|
|
393
424
|
};
|
|
394
425
|
|
|
395
|
-
const {spec: result
|
|
426
|
+
const {spec: result} = transformOpenApiSpec(spec, {version: '3.1.0'});
|
|
396
427
|
const paths = result.paths as Record<string, unknown>;
|
|
397
428
|
const sub = paths?.['/subscribe'] as Record<string, unknown>;
|
|
398
429
|
const post = sub?.post as Record<string, unknown>;
|
|
399
430
|
const callbacks = post?.callbacks as Record<string, unknown>;
|
|
400
431
|
const onEvent = callbacks?.onEvent as Record<string, unknown>;
|
|
401
|
-
const cbPath = onEvent?.['{$request.body#/callbackUrl}'] as Record<
|
|
432
|
+
const cbPath = onEvent?.['{$request.body#/callbackUrl}'] as Record<
|
|
433
|
+
string,
|
|
434
|
+
unknown
|
|
435
|
+
>;
|
|
402
436
|
const cbPost = cbPath?.post as Record<string, unknown>;
|
|
403
437
|
const reqBody = cbPost?.requestBody as Record<string, unknown>;
|
|
404
438
|
const content = reqBody?.content as Record<string, unknown>;
|
|
@@ -429,7 +463,7 @@ describe('path-level parameters', () => {
|
|
|
429
463
|
},
|
|
430
464
|
};
|
|
431
465
|
|
|
432
|
-
const {spec: result
|
|
466
|
+
const {spec: result} = transformOpenApiSpec(spec, {version: '3.1.0'});
|
|
433
467
|
const paths = result.paths as Record<string, unknown>;
|
|
434
468
|
const pathItem = paths?.['/items/{id}'] as Record<string, unknown>;
|
|
435
469
|
const params = pathItem?.parameters as Record<string, unknown>[];
|
|
@@ -464,11 +498,14 @@ describe('circular references', () => {
|
|
|
464
498
|
expect(() => {
|
|
465
499
|
// This would infinite loop without the WeakSet guard
|
|
466
500
|
transformOpenApiSpec(
|
|
467
|
-
JSON.parse(
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
501
|
+
JSON.parse(
|
|
502
|
+
JSON.stringify(spec, (key, value) => {
|
|
503
|
+
// Break circular ref for JSON.stringify
|
|
504
|
+
if (key === 'self' && value === node)
|
|
505
|
+
return {$ref: '#/components/schemas/Node'};
|
|
506
|
+
return value;
|
|
507
|
+
}),
|
|
508
|
+
),
|
|
472
509
|
{version: '3.1.0'},
|
|
473
510
|
);
|
|
474
511
|
}).not.toThrow();
|
|
@@ -488,7 +525,7 @@ describe('3.2 additional fields', () => {
|
|
|
488
525
|
jsonSchemaDialect: 'https://json-schema.org/draft/2020-12/schema',
|
|
489
526
|
};
|
|
490
527
|
|
|
491
|
-
const {spec: result
|
|
528
|
+
const {spec: result} = transformOpenApiSpec(spec, {version: '3.0.0'});
|
|
492
529
|
expect(result).not.toHaveProperty('jsonSchemaDialect');
|
|
493
530
|
});
|
|
494
531
|
|
|
@@ -506,7 +543,7 @@ describe('3.2 additional fields', () => {
|
|
|
506
543
|
},
|
|
507
544
|
};
|
|
508
545
|
|
|
509
|
-
const {spec: result
|
|
546
|
+
const {spec: result} = transformOpenApiSpec(spec, {version: '3.1.0'});
|
|
510
547
|
const components = result.components as Record<string, unknown>;
|
|
511
548
|
expect(components).not.toHaveProperty('mediaTypes');
|
|
512
549
|
});
|
|
@@ -584,7 +621,9 @@ describe('warnings', () => {
|
|
|
584
621
|
};
|
|
585
622
|
|
|
586
623
|
const {spec: result} = transformOpenApiSpec(spec, {version: '3.1.0'});
|
|
587
|
-
expect(result.jsonSchemaDialect).toBe(
|
|
624
|
+
expect(result.jsonSchemaDialect).toBe(
|
|
625
|
+
'https://json-schema.org/draft/2020-12/schema',
|
|
626
|
+
);
|
|
588
627
|
});
|
|
589
628
|
});
|
|
590
629
|
|
|
@@ -594,14 +633,22 @@ describe('warnings', () => {
|
|
|
594
633
|
|
|
595
634
|
describe('version validation', () => {
|
|
596
635
|
it('rejects unsupported minor version 3.99.0', () => {
|
|
597
|
-
const spec = {
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
636
|
+
const spec = {
|
|
637
|
+
openapi: '3.0.0',
|
|
638
|
+
info: {title: 'T', version: '1'},
|
|
639
|
+
paths: {},
|
|
640
|
+
};
|
|
641
|
+
expect(() =>
|
|
642
|
+
transformOpenApiSpec(spec, {version: '3.99.0' as '3.0.0'}),
|
|
643
|
+
).toThrow('Unsupported OpenAPI minor version');
|
|
601
644
|
});
|
|
602
645
|
|
|
603
646
|
it('rejects invalid source version', () => {
|
|
604
|
-
const spec = {
|
|
647
|
+
const spec = {
|
|
648
|
+
openapi: 'banana',
|
|
649
|
+
info: {title: 'T', version: '1'},
|
|
650
|
+
paths: {},
|
|
651
|
+
};
|
|
605
652
|
expect(() => transformOpenApiSpec(spec, {version: '3.1.0'})).toThrow(
|
|
606
653
|
'Invalid OpenAPI version',
|
|
607
654
|
);
|
|
@@ -638,10 +685,17 @@ describe('3.2 media fields', () => {
|
|
|
638
685
|
},
|
|
639
686
|
};
|
|
640
687
|
|
|
641
|
-
const {spec: result, warnings} = transformOpenApiSpec(spec, {
|
|
642
|
-
|
|
688
|
+
const {spec: result, warnings} = transformOpenApiSpec(spec, {
|
|
689
|
+
version: '3.1.0',
|
|
690
|
+
});
|
|
691
|
+
const getOp = (result.paths as Record<string, unknown>)?.[
|
|
692
|
+
'/stream'
|
|
693
|
+
] as Record<string, unknown>;
|
|
643
694
|
const get = getOp?.get as Record<string, unknown>;
|
|
644
|
-
const resp = (get?.responses as Record<string, unknown>)?.['200'] as Record<
|
|
695
|
+
const resp = (get?.responses as Record<string, unknown>)?.['200'] as Record<
|
|
696
|
+
string,
|
|
697
|
+
unknown
|
|
698
|
+
>;
|
|
645
699
|
const content = resp?.content as Record<string, unknown>;
|
|
646
700
|
const sse = content?.['text/event-stream'] as Record<string, unknown>;
|
|
647
701
|
expect(sse).not.toHaveProperty('itemSchema');
|
|
@@ -665,10 +719,12 @@ describe('Server.name', () => {
|
|
|
665
719
|
servers: [{url: 'https://api.example.com', name: 'production'}],
|
|
666
720
|
};
|
|
667
721
|
|
|
668
|
-
const {spec: result, warnings} = transformOpenApiSpec(spec, {
|
|
722
|
+
const {spec: result, warnings} = transformOpenApiSpec(spec, {
|
|
723
|
+
version: '3.1.0',
|
|
724
|
+
});
|
|
669
725
|
const servers = result.servers as Record<string, unknown>[];
|
|
670
726
|
expect(servers[0]).not.toHaveProperty('name');
|
|
671
|
-
expect(servers[0]
|
|
727
|
+
expect(servers[0]!.url).toBe('https://api.example.com');
|
|
672
728
|
expect(warnings.some(w => w.field === 'servers')).toBe(true);
|
|
673
729
|
});
|
|
674
730
|
});
|
|
@@ -681,11 +737,15 @@ describe('License.identifier', () => {
|
|
|
681
737
|
it('strips license identifier when targeting 3.0', () => {
|
|
682
738
|
const spec = {
|
|
683
739
|
openapi: '3.1.0',
|
|
684
|
-
info: {
|
|
740
|
+
info: {
|
|
741
|
+
title: 'T',
|
|
742
|
+
version: '1',
|
|
743
|
+
license: {name: 'MIT', identifier: 'MIT'},
|
|
744
|
+
},
|
|
685
745
|
paths: {},
|
|
686
746
|
};
|
|
687
747
|
|
|
688
|
-
const {spec: result
|
|
748
|
+
const {spec: result} = transformOpenApiSpec(spec, {version: '3.0.0'});
|
|
689
749
|
const info = result.info as Record<string, unknown>;
|
|
690
750
|
const license = info?.license as Record<string, unknown>;
|
|
691
751
|
expect(license).not.toHaveProperty('identifier');
|
|
@@ -695,7 +755,11 @@ describe('License.identifier', () => {
|
|
|
695
755
|
it('preserves license identifier when targeting 3.1', () => {
|
|
696
756
|
const spec = {
|
|
697
757
|
openapi: '3.2.0',
|
|
698
|
-
info: {
|
|
758
|
+
info: {
|
|
759
|
+
title: 'T',
|
|
760
|
+
version: '1',
|
|
761
|
+
license: {name: 'MIT', identifier: 'MIT'},
|
|
762
|
+
},
|
|
699
763
|
paths: {},
|
|
700
764
|
};
|
|
701
765
|
|
|
@@ -725,7 +789,9 @@ describe('components.pathItems', () => {
|
|
|
725
789
|
},
|
|
726
790
|
};
|
|
727
791
|
|
|
728
|
-
const {spec: result, warnings} = transformOpenApiSpec(spec, {
|
|
792
|
+
const {spec: result, warnings} = transformOpenApiSpec(spec, {
|
|
793
|
+
version: '3.0.0',
|
|
794
|
+
});
|
|
729
795
|
const components = result.components as Record<string, unknown>;
|
|
730
796
|
expect(components).not.toHaveProperty('pathItems');
|
|
731
797
|
expect(warnings.some(w => w.field === 'components.pathItems')).toBe(true);
|
|
@@ -775,8 +841,11 @@ describe('XML Object', () => {
|
|
|
775
841
|
},
|
|
776
842
|
};
|
|
777
843
|
|
|
778
|
-
const {spec: result, warnings} = transformOpenApiSpec(spec, {
|
|
779
|
-
|
|
844
|
+
const {spec: result, warnings} = transformOpenApiSpec(spec, {
|
|
845
|
+
version: '3.1.0',
|
|
846
|
+
});
|
|
847
|
+
const item = (result.components as Record<string, unknown>)
|
|
848
|
+
?.schemas as Record<string, unknown>;
|
|
780
849
|
const schema = item?.Item as Record<string, unknown>;
|
|
781
850
|
const xml = schema?.xml as Record<string, unknown>;
|
|
782
851
|
expect(xml).not.toHaveProperty('text');
|
|
@@ -810,7 +879,9 @@ describe('transform ordering', () => {
|
|
|
810
879
|
};
|
|
811
880
|
|
|
812
881
|
const {warnings} = transformOpenApiSpec(spec, {version: '3.0.0'});
|
|
813
|
-
const queryWarning = warnings.find(w =>
|
|
882
|
+
const queryWarning = warnings.find(w =>
|
|
883
|
+
w.field.includes('webhooks.onEvent.query'),
|
|
884
|
+
);
|
|
814
885
|
const webhookWarning = warnings.find(w => w.field === 'webhooks');
|
|
815
886
|
expect(queryWarning).toBeDefined();
|
|
816
887
|
expect(webhookWarning).toBeDefined();
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed error classes for the OpenAPI version transformer.
|
|
3
|
+
*
|
|
4
|
+
* Each class extends `Error` and overrides `name` so consumers can match
|
|
5
|
+
* failures either by `instanceof` or by `error.name`. Throw the most
|
|
6
|
+
* specific subclass for the failure category; never raw `Error`.
|
|
7
|
+
*
|
|
8
|
+
* The `Object.setPrototypeOf(this, new.target.prototype)` call in the
|
|
9
|
+
* base class is required so `instanceof` checks work correctly in
|
|
10
|
+
* transpiled output (TypeScript/ES5 lose the prototype chain when
|
|
11
|
+
* extending built-in classes like `Error`).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Options accepted by `Error` constructors at runtime in Node.js 18+.
|
|
16
|
+
* Declared locally because the project targets ES2021 and the global
|
|
17
|
+
* `ErrorOptions` type ships in ES2022 libs.
|
|
18
|
+
*
|
|
19
|
+
* @public
|
|
20
|
+
*/
|
|
21
|
+
export interface OpenApiErrorOptions {
|
|
22
|
+
cause?: unknown;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Base error for all OpenAPI version-transformer failures.
|
|
27
|
+
* Consumers can catch this to handle any failure from the plugin.
|
|
28
|
+
*
|
|
29
|
+
* @public
|
|
30
|
+
*/
|
|
31
|
+
export class OpenApiVersionError extends Error {
|
|
32
|
+
override readonly name: string = 'OpenApiVersionError';
|
|
33
|
+
readonly cause?: unknown;
|
|
34
|
+
constructor(message: string, options?: OpenApiErrorOptions) {
|
|
35
|
+
super(message);
|
|
36
|
+
// Preserve `cause` at runtime. Node 18+'s Error constructor accepts
|
|
37
|
+
// an options.cause natively; the project targets ES2021 libs where
|
|
38
|
+
// that overload is not declared, so we set the property manually.
|
|
39
|
+
if (options && 'cause' in options) {
|
|
40
|
+
this.cause = options.cause;
|
|
41
|
+
}
|
|
42
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Thrown when the component is misconfigured (invalid target version,
|
|
48
|
+
* unsupported transform mode, missing required binding, etc.).
|
|
49
|
+
*
|
|
50
|
+
* @public
|
|
51
|
+
*/
|
|
52
|
+
export class OpenApiVersionConfigError extends OpenApiVersionError {
|
|
53
|
+
override readonly name = 'OpenApiVersionConfigError';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Thrown when an OpenAPI document fails to transform between versions
|
|
58
|
+
* (malformed input, unparsable structure, ambiguous nullable usage).
|
|
59
|
+
*
|
|
60
|
+
* @public
|
|
61
|
+
*/
|
|
62
|
+
export class OpenApiTransformError extends OpenApiVersionError {
|
|
63
|
+
override readonly name = 'OpenApiTransformError';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Thrown when a 3.1+ feature cannot be downgraded to 3.0 without data
|
|
68
|
+
* loss (e.g. webhooks, JSON Schema 2020-12 vocabularies, type arrays).
|
|
69
|
+
* The thrown error carries a structured list of features that prevented
|
|
70
|
+
* the downgrade so callers can react.
|
|
71
|
+
*
|
|
72
|
+
* @public
|
|
73
|
+
*/
|
|
74
|
+
export class OpenApiDowngradeError extends OpenApiVersionError {
|
|
75
|
+
override readonly name = 'OpenApiDowngradeError';
|
|
76
|
+
constructor(
|
|
77
|
+
message: string,
|
|
78
|
+
public readonly unsupportedFeatures: readonly string[],
|
|
79
|
+
options?: OpenApiErrorOptions,
|
|
80
|
+
) {
|
|
81
|
+
super(message, options);
|
|
82
|
+
}
|
|
83
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,10 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public entry point for `@ebarahona/loopback-openapi-v3`.
|
|
3
|
+
*
|
|
4
|
+
* Re-exports the component, enhancer, binding keys, configuration
|
|
5
|
+
* types, the pure transform functions, and the typed error hierarchy.
|
|
6
|
+
*
|
|
7
|
+
* @packageDocumentation
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** @public */
|
|
1
11
|
export {OpenApiVersionComponent} from './openapi-version.component';
|
|
12
|
+
/** @public */
|
|
2
13
|
export {OpenApiVersionEnhancer} from './openapi-version.enhancer';
|
|
14
|
+
/** @public */
|
|
3
15
|
export {OpenApiVersionBindings} from './keys';
|
|
4
|
-
|
|
16
|
+
/** @public */
|
|
17
|
+
export type {OpenApiVersion, OpenApiVersionConfig} from './types';
|
|
18
|
+
/** @public */
|
|
19
|
+
export {transformOpenApiSpec, parseVersion} from './transform';
|
|
20
|
+
/** @public */
|
|
21
|
+
export type {TransformResult, TransformWarning} from './transform';
|
|
22
|
+
/** @public */
|
|
5
23
|
export {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
} from './
|
|
24
|
+
OpenApiVersionError,
|
|
25
|
+
OpenApiVersionConfigError,
|
|
26
|
+
OpenApiTransformError,
|
|
27
|
+
OpenApiDowngradeError,
|
|
28
|
+
} from './errors';
|
|
29
|
+
/** @public */
|
|
30
|
+
export type {OpenApiErrorOptions} from './errors';
|
package/src/keys.ts
CHANGED
|
@@ -1,9 +1,25 @@
|
|
|
1
1
|
import {BindingKey} from '@loopback/core';
|
|
2
|
-
import {OpenApiVersionConfig} from './types';
|
|
2
|
+
import type {OpenApiVersionConfig} from './types';
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Binding keys for the OpenAPI version transformer component.
|
|
6
|
+
*
|
|
7
|
+
* @public
|
|
8
|
+
*/
|
|
4
9
|
export namespace OpenApiVersionBindings {
|
|
10
|
+
/**
|
|
11
|
+
* Binding key for the component configuration.
|
|
12
|
+
*
|
|
13
|
+
* @public
|
|
14
|
+
*/
|
|
5
15
|
export const CONFIG = BindingKey.create<OpenApiVersionConfig>(
|
|
6
16
|
'openapi-version.config',
|
|
7
17
|
);
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Binding key for the component itself.
|
|
21
|
+
*
|
|
22
|
+
* @public
|
|
23
|
+
*/
|
|
8
24
|
export const COMPONENT = 'openapi-version.component';
|
|
9
25
|
}
|
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
Component,
|
|
4
|
-
createBindingFromClass,
|
|
5
|
-
} from '@loopback/core';
|
|
1
|
+
import {Binding, createBindingFromClass} from '@loopback/core';
|
|
2
|
+
import type {Component} from '@loopback/core';
|
|
6
3
|
import {asSpecEnhancer} from '@loopback/openapi-v3';
|
|
7
4
|
import {OpenApiVersionBindings} from './keys';
|
|
8
5
|
import {OpenApiVersionEnhancer} from './openapi-version.enhancer';
|
|
@@ -18,6 +15,8 @@ import {DEFAULT_CONFIG} from './types';
|
|
|
18
15
|
* app.component(OpenApiVersionComponent);
|
|
19
16
|
* app.bind(OpenApiVersionBindings.CONFIG).to({version: '3.1.0'});
|
|
20
17
|
* ```
|
|
18
|
+
*
|
|
19
|
+
* @public
|
|
21
20
|
*/
|
|
22
21
|
export class OpenApiVersionComponent implements Component {
|
|
23
22
|
bindings: Binding[] = [
|
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
import {config, injectable} from '@loopback/core';
|
|
2
|
-
import {
|
|
3
|
-
OASEnhancer,
|
|
4
|
-
OpenApiSpec,
|
|
5
|
-
} from '@loopback/openapi-v3';
|
|
2
|
+
import {OASEnhancer, OpenApiSpec} from '@loopback/openapi-v3';
|
|
6
3
|
import debugFactory from 'debug';
|
|
7
4
|
import {OpenApiVersionBindings} from './keys';
|
|
8
5
|
import {transformOpenApiSpec} from './transform';
|
|
@@ -27,6 +24,8 @@ const debug = debugFactory('loopback:openapi-version');
|
|
|
27
24
|
* - 3.0: https://spec.openapis.org/oas/v3.0.3.html
|
|
28
25
|
* - 3.1: https://spec.openapis.org/oas/v3.1.0.html
|
|
29
26
|
* - 3.2: https://spec.openapis.org/oas/v3.2.0.html
|
|
27
|
+
*
|
|
28
|
+
* @public
|
|
30
29
|
*/
|
|
31
30
|
@injectable()
|
|
32
31
|
export class OpenApiVersionEnhancer implements OASEnhancer {
|
|
@@ -44,13 +43,13 @@ export class OpenApiVersionEnhancer implements OASEnhancer {
|
|
|
44
43
|
opts.version,
|
|
45
44
|
opts.transformNullable !== false,
|
|
46
45
|
);
|
|
47
|
-
const result = transformOpenApiSpec(
|
|
48
|
-
spec as Record<string, unknown>,
|
|
49
|
-
opts,
|
|
50
|
-
);
|
|
46
|
+
const result = transformOpenApiSpec(spec as Record<string, unknown>, opts);
|
|
51
47
|
|
|
52
48
|
if (result.warnings.length > 0) {
|
|
53
|
-
debug(
|
|
49
|
+
debug(
|
|
50
|
+
'emitted %d OpenAPI compatibility warnings',
|
|
51
|
+
result.warnings.length,
|
|
52
|
+
);
|
|
54
53
|
for (const w of result.warnings) {
|
|
55
54
|
debug('warning [%s]: %s', w.field, w.message);
|
|
56
55
|
}
|