Skip to content

Commit c5000de

Browse files
authored
fix: updated parseRequestUnion for union requests
2 parents 43dee8f + d2836f4 commit c5000de

File tree

2 files changed

+371
-11
lines changed

2 files changed

+371
-11
lines changed

packages/openapi-generator/src/route.ts

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -183,17 +183,20 @@ function parseRequestUnion(
183183
parameters.push(...headerParams.values());
184184
}
185185

186-
const firstSubSchema = schema.schemas[0];
187-
if (firstSubSchema !== undefined && firstSubSchema.type === 'object') {
188-
const pathSchema = firstSubSchema.properties['params'];
189-
if (pathSchema !== undefined && pathSchema.type === 'object') {
190-
for (const [name, prop] of Object.entries(pathSchema.properties)) {
191-
parameters.push({
192-
type: 'path',
193-
name,
194-
schema: prop,
195-
required: pathSchema.required.includes(name),
196-
});
186+
// Find the first schema in the union that has path parameters
187+
for (const subSchema of schema.schemas) {
188+
if (subSchema.type === 'object') {
189+
const pathSchema = subSchema.properties['params'];
190+
if (pathSchema !== undefined && pathSchema.type === 'object') {
191+
for (const [name, prop] of Object.entries(pathSchema.properties)) {
192+
parameters.push({
193+
type: 'path',
194+
name,
195+
schema: prop,
196+
required: pathSchema.required.includes(name),
197+
});
198+
}
199+
break; // Found path params, stop looking
197200
}
198201
}
199202
}

packages/openapi-generator/test/openapi/union.test.ts

Lines changed: 357 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,363 @@ testCase('route with unknown unions', ROUTE_WITH_UNKNOWN_UNIONS, {
353353
},
354354
});
355355

356+
const ROUTE_WITH_PATH_PARAMS_IN_UNION_NOT_FIRST = `
357+
import * as t from 'io-ts';
358+
import * as h from '@api-ts/io-ts-http';
359+
360+
export const route = h.httpRoute({
361+
path: '/internal/api/policy/v1/{applicationName}/touchpoints/{touchpoint}/rules/evaluation',
362+
method: 'POST',
363+
request: t.union([
364+
h.httpRequest({
365+
body: { emptyRequest: t.boolean }
366+
}),
367+
h.httpRequest({
368+
params: {
369+
applicationName: t.string,
370+
touchpoint: t.string,
371+
},
372+
body: { requestWithParams: t.string }
373+
}),
374+
]),
375+
response: {
376+
200: t.string,
377+
},
378+
});
379+
`;
380+
381+
testCase(
382+
'route with path params in union second schema (regression test)',
383+
ROUTE_WITH_PATH_PARAMS_IN_UNION_NOT_FIRST,
384+
{
385+
info: {
386+
title: 'Test',
387+
version: '1.0.0',
388+
},
389+
openapi: '3.0.3',
390+
paths: {
391+
'/internal/api/policy/v1/{applicationName}/touchpoints/{touchpoint}/rules/evaluation':
392+
{
393+
post: {
394+
parameters: [
395+
{
396+
in: 'path',
397+
name: 'applicationName',
398+
required: true,
399+
schema: { type: 'string' },
400+
},
401+
{
402+
in: 'path',
403+
name: 'touchpoint',
404+
required: true,
405+
schema: { type: 'string' },
406+
},
407+
],
408+
requestBody: {
409+
content: {
410+
'application/json': {
411+
schema: {
412+
oneOf: [
413+
{
414+
properties: {
415+
emptyRequest: { type: 'boolean' },
416+
},
417+
required: ['emptyRequest'],
418+
type: 'object',
419+
},
420+
{
421+
properties: {
422+
requestWithParams: { type: 'string' },
423+
},
424+
required: ['requestWithParams'],
425+
type: 'object',
426+
},
427+
],
428+
},
429+
},
430+
},
431+
},
432+
responses: {
433+
'200': {
434+
description: 'OK',
435+
content: {
436+
'application/json': {
437+
schema: {
438+
type: 'string',
439+
},
440+
},
441+
},
442+
},
443+
},
444+
},
445+
},
446+
},
447+
components: {
448+
schemas: {},
449+
},
450+
},
451+
);
452+
453+
const ROUTE_WITH_PATH_PARAMS_ONLY_IN_THIRD_SCHEMA = `
454+
import * as t from 'io-ts';
455+
import * as h from '@api-ts/io-ts-http';
456+
457+
export const route = h.httpRoute({
458+
path: '/api/{userId}/posts/{postId}',
459+
method: 'GET',
460+
request: t.union([
461+
// First: empty request
462+
h.httpRequest({}),
463+
// Second: only query params
464+
h.httpRequest({
465+
query: { filter: t.string }
466+
}),
467+
// Third: has the path params
468+
h.httpRequest({
469+
params: {
470+
userId: t.string,
471+
postId: t.string,
472+
},
473+
query: { details: t.boolean }
474+
}),
475+
]),
476+
response: {
477+
200: t.string,
478+
},
479+
});
480+
`;
481+
482+
testCase(
483+
'route with path params only in third schema',
484+
ROUTE_WITH_PATH_PARAMS_ONLY_IN_THIRD_SCHEMA,
485+
{
486+
info: {
487+
title: 'Test',
488+
version: '1.0.0',
489+
},
490+
openapi: '3.0.3',
491+
paths: {
492+
'/api/{userId}/posts/{postId}': {
493+
get: {
494+
parameters: [
495+
{
496+
in: 'query',
497+
name: 'union',
498+
required: true,
499+
explode: true,
500+
style: 'form',
501+
schema: {
502+
oneOf: [
503+
{
504+
properties: { filter: { type: 'string' } },
505+
required: ['filter'],
506+
type: 'object',
507+
},
508+
{
509+
properties: { details: { type: 'boolean' } },
510+
required: ['details'],
511+
type: 'object',
512+
},
513+
],
514+
},
515+
},
516+
{ in: 'path', name: 'userId', required: true, schema: { type: 'string' } },
517+
{ in: 'path', name: 'postId', required: true, schema: { type: 'string' } },
518+
],
519+
responses: {
520+
'200': {
521+
description: 'OK',
522+
content: {
523+
'application/json': {
524+
schema: {
525+
type: 'string',
526+
},
527+
},
528+
},
529+
},
530+
},
531+
},
532+
},
533+
},
534+
components: {
535+
schemas: {},
536+
},
537+
},
538+
);
539+
540+
const ROUTE_WITH_FULLY_DEFINED_PARAMS = `
541+
import * as t from 'io-ts';
542+
import * as h from '@api-ts/io-ts-http';
543+
544+
const AddressBookConnectionSides = t.union([t.literal('send'), t.literal('receive')]);
545+
546+
/**
547+
* Create policy evaluation definition
548+
* @operationId v1.post.policy.evaluation.definition
549+
* @tag Policy Builder
550+
* @private
551+
*/
552+
export const route = h.httpRoute({
553+
path: '/internal/api/policy/v1/{applicationName}/touchpoints/{touchpoint}/rules/evaluations',
554+
method: 'POST',
555+
request: t.union([
556+
h.httpRequest({
557+
params: {
558+
applicationName: t.string,
559+
touchpoint: t.string,
560+
},
561+
body: t.type({
562+
approvalRequestId: t.string,
563+
counterPartyId: t.string,
564+
description: h.optional(t.string),
565+
enterpriseId: t.string,
566+
grossAmount: h.optional(t.number),
567+
idempotencyKey: t.string,
568+
isFirstTimeCounterParty: t.boolean,
569+
isMutualConnection: t.boolean,
570+
netAmount: h.optional(t.number),
571+
settlementId: t.string,
572+
userId: t.string,
573+
walletId: t.string,
574+
})
575+
}),
576+
h.httpRequest({
577+
params: {
578+
applicationName: t.string,
579+
touchpoint: t.string,
580+
},
581+
body: t.type({
582+
connectionId: t.string,
583+
description: h.optional(t.string),
584+
enterpriseId: t.string,
585+
idempotencyKey: t.string,
586+
side: AddressBookConnectionSides,
587+
walletId: t.string,
588+
})
589+
}),
590+
]),
591+
response: {
592+
200: t.string,
593+
},
594+
});
595+
`;
596+
597+
testCase(
598+
'union request with consistently defined path parameters',
599+
ROUTE_WITH_FULLY_DEFINED_PARAMS,
600+
{
601+
info: {
602+
title: 'Test',
603+
version: '1.0.0',
604+
},
605+
openapi: '3.0.3',
606+
paths: {
607+
'/internal/api/policy/v1/{applicationName}/touchpoints/{touchpoint}/rules/evaluations':
608+
{
609+
post: {
610+
summary: 'Create policy evaluation definition',
611+
operationId: 'v1.post.policy.evaluation.definition',
612+
tags: ['Policy Builder'],
613+
'x-internal': true,
614+
parameters: [
615+
{
616+
in: 'path',
617+
name: 'applicationName',
618+
required: true,
619+
schema: { type: 'string' },
620+
},
621+
{
622+
in: 'path',
623+
name: 'touchpoint',
624+
required: true,
625+
schema: { type: 'string' },
626+
},
627+
],
628+
requestBody: {
629+
content: {
630+
'application/json': {
631+
schema: {
632+
oneOf: [
633+
{
634+
type: 'object',
635+
properties: {
636+
approvalRequestId: { type: 'string' },
637+
counterPartyId: { type: 'string' },
638+
description: { type: 'string' },
639+
enterpriseId: { type: 'string' },
640+
grossAmount: { type: 'number' },
641+
idempotencyKey: { type: 'string' },
642+
isFirstTimeCounterParty: { type: 'boolean' },
643+
isMutualConnection: { type: 'boolean' },
644+
netAmount: { type: 'number' },
645+
settlementId: { type: 'string' },
646+
userId: { type: 'string' },
647+
walletId: { type: 'string' },
648+
},
649+
required: [
650+
'approvalRequestId',
651+
'counterPartyId',
652+
'enterpriseId',
653+
'idempotencyKey',
654+
'isFirstTimeCounterParty',
655+
'isMutualConnection',
656+
'settlementId',
657+
'userId',
658+
'walletId',
659+
],
660+
},
661+
{
662+
type: 'object',
663+
properties: {
664+
connectionId: { type: 'string' },
665+
description: { type: 'string' },
666+
enterpriseId: { type: 'string' },
667+
idempotencyKey: { type: 'string' },
668+
side: {
669+
$ref: '#/components/schemas/AddressBookConnectionSides',
670+
},
671+
walletId: { type: 'string' },
672+
},
673+
required: [
674+
'connectionId',
675+
'enterpriseId',
676+
'idempotencyKey',
677+
'side',
678+
'walletId',
679+
],
680+
},
681+
],
682+
},
683+
},
684+
},
685+
},
686+
responses: {
687+
'200': {
688+
description: 'OK',
689+
content: {
690+
'application/json': {
691+
schema: {
692+
type: 'string',
693+
},
694+
},
695+
},
696+
},
697+
},
698+
},
699+
},
700+
},
701+
components: {
702+
schemas: {
703+
AddressBookConnectionSides: {
704+
enum: ['send', 'receive'],
705+
title: 'AddressBookConnectionSides',
706+
type: 'string',
707+
},
708+
},
709+
},
710+
},
711+
);
712+
356713
const ROUTE_WITH_DUPLICATE_HEADERS = `
357714
import * as t from 'io-ts';
358715
import * as h from '@api-ts/io-ts-http';

0 commit comments

Comments
 (0)