Skip to content

Commit 58ce907

Browse files
authored
feat(core): add better support for form-data arrays (#2026)
* feat(core): add union enum generation * feat(core): add better support for form-data arrays * Update output.md * fix: merge formData with formDataArrayHandling
1 parent 7fc20e1 commit 58ce907

File tree

13 files changed

+353
-34
lines changed

13 files changed

+353
-34
lines changed

docs/src/pages/reference/configuration/output.md

Lines changed: 138 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1902,7 +1902,8 @@ Use this property to provide a config to your http client or completely remove t
19021902

19031903
Type: `Boolean` or `String` or `Object`.
19041904

1905-
Valid values: path of the formData function or object with a path and name.
1905+
Valid values: path of the formData function or object with a path and name. You can also define how
1906+
the names of form entries are handled regarding arrays.
19061907

19071908
Use this property to disable the auto generation of form data if you use multipart
19081909

@@ -1935,6 +1936,142 @@ export const customFormDataFn = <Body>(body: Body): FormData => {
19351936
};
19361937
```
19371938

1939+
##### mutator
1940+
1941+
Type: `String` | `Object`
1942+
1943+
Same as defining the mutator directly on `formData`, but this way you can specify `arrayHandling` as well.
1944+
1945+
```js
1946+
module.exports = {
1947+
petstore: {
1948+
output: {
1949+
override: {
1950+
formData: {
1951+
mutator: {
1952+
path: './api/mutator/custom-form-data-fn.ts',
1953+
name: 'customFormDataFn',
1954+
},
1955+
},
1956+
},
1957+
},
1958+
},
1959+
};
1960+
```
1961+
1962+
##### arrayHandling
1963+
1964+
Type: `serialize` | `serialize-with-brackets` | `explode`
1965+
1966+
Default Value: `serialize`
1967+
1968+
Decides how FormData generation handles arrays.
1969+
1970+
```js
1971+
module.exports = {
1972+
petstore: {
1973+
output: {
1974+
override: {
1975+
formData: {
1976+
mutator: {
1977+
path: './api/mutator/custom-form-data-fn.ts',
1978+
name: 'customFormDataFn',
1979+
},
1980+
arrayHandling: 'serialize-with-brackets',
1981+
},
1982+
},
1983+
},
1984+
},
1985+
};
1986+
```
1987+
1988+
For all of the following examples, this specificaiton is used:
1989+
1990+
```yaml
1991+
components:
1992+
schemas:
1993+
Pet:
1994+
type: object
1995+
properties:
1996+
name:
1997+
type: string
1998+
age:
1999+
type: number
2000+
relatives:
2001+
type: array
2002+
items:
2003+
type: object
2004+
properties:
2005+
name:
2006+
type: string
2007+
colors:
2008+
type: array
2009+
items:
2010+
type: string
2011+
enum:
2012+
- white
2013+
- black
2014+
- green
2015+
```
2016+
2017+
Type `serialize` setting results in the following generated code:
2018+
2019+
```ts
2020+
const formData = new FormData();
2021+
if (pet.name !== undefined) {
2022+
formData.append(`name`, pet.name);
2023+
}
2024+
if (pet.age !== undefined) {
2025+
formData.append(`age`, pet.age.toString());
2026+
}
2027+
if (pet.relatives !== undefined) {
2028+
pet.relatives.forEach((value) =>
2029+
formData.append(`relatives`, JSON.stringify(value)),
2030+
);
2031+
}
2032+
```
2033+
2034+
Type `serialize-with-brackets` setting results in the following generated code:
2035+
2036+
```ts
2037+
const formData = new FormData();
2038+
if (pet.name !== undefined) {
2039+
formData.append(`name`, pet.name);
2040+
}
2041+
if (pet.age !== undefined) {
2042+
formData.append(`age`, pet.age.toString());
2043+
}
2044+
if (pet.relatives !== undefined) {
2045+
pet.relatives.forEach((value) =>
2046+
formData.append(`relatives[]`, JSON.stringify(value)),
2047+
);
2048+
}
2049+
```
2050+
2051+
Type `explode` setting results in the following generated code:
2052+
2053+
```ts
2054+
const formData = new FormData();
2055+
if (pet.name !== undefined) {
2056+
formData.append(`name`, pet.name);
2057+
}
2058+
if (pet.age !== undefined) {
2059+
formData.append(`age`, pet.age.toString());
2060+
}
2061+
if (pet.relatives !== undefined) {
2062+
pet.relatives.forEach((value, index) => {
2063+
if (value.name !== undefined) {
2064+
formData.append(`relatives[${index}].name`, value.name);
2065+
}
2066+
if (value.colors !== undefined) {
2067+
value.colors.forEach((value, index1) =>
2068+
formData.append(`relatives[${index}].colors[${index1}]`, value),
2069+
);
2070+
}
2071+
});
2072+
}
2073+
```
2074+
19382075
#### formUrlEncoded
19392076

19402077
Type: `Boolean` or `String` or `Object`.

packages/angular/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ const generateImplementation = (
131131
{ route, context }: GeneratorOptions,
132132
) => {
133133
const isRequestOptions = override?.requestOptions !== false;
134-
const isFormData = override?.formData !== false;
134+
const isFormData = override?.formData.disabled === false;
135135
const isFormUrlEncoded = override?.formUrlEncoded !== false;
136136
const isExactOptionalPropertyTypes =
137137
!!context.output.tsconfig?.compilerOptions?.exactOptionalPropertyTypes;

packages/axios/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ const generateAxiosImplementation = (
7777
{ route, context }: GeneratorOptions,
7878
) => {
7979
const isRequestOptions = override?.requestOptions !== false;
80-
const isFormData = override?.formData !== false;
80+
const isFormData = override?.formData.disabled === false;
8181
const isFormUrlEncoded = override?.formUrlEncoded !== false;
8282
const isExactOptionalPropertyTypes =
8383
!!context.output.tsconfig?.compilerOptions?.exactOptionalPropertyTypes;

packages/core/src/generators/verbs-options.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
asyncReduce,
3030
camel,
3131
dynamicImport,
32+
isBoolean,
3233
isObject,
3334
isString,
3435
isVerb,
@@ -145,12 +146,11 @@ const generateVerbOptions = async ({
145146
});
146147

147148
const formData =
148-
(isString(override?.formData) || isObject(override?.formData)) &&
149-
body.formData
149+
!override.formData.disabled && body.formData
150150
? await generateMutator({
151151
output: output.target,
152152
name: operationName,
153-
mutator: override.formData,
153+
mutator: override.formData.mutator,
154154
workspace: context.workspace,
155155
tsconfig: context.output.tsconfig,
156156
})

packages/core/src/getters/res-req-types.ts

Lines changed: 59 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,12 @@ import {
1111
} from 'openapi3-ts/oas30';
1212
import { resolveObject } from '../resolvers/object';
1313
import { resolveExampleRefs, resolveRef } from '../resolvers/ref';
14-
import { ContextSpecs, GeneratorImport, ResReqTypesValue } from '../types';
14+
import {
15+
ContextSpecs,
16+
FormDataArrayHandling,
17+
GeneratorImport,
18+
ResReqTypesValue,
19+
} from '../types';
1520
import { camel } from '../utils';
1621
import { isReference } from '../utils/assertion';
1722
import { pascal } from '../utils/case';
@@ -381,12 +386,16 @@ const resolveSchemaPropertiesToFormData = ({
381386
propName,
382387
context,
383388
isRequestBodyOptional,
389+
keyPrefix = '',
390+
depth = 0,
384391
}: {
385392
schema: SchemaObject;
386393
variableName: string;
387394
propName: string;
388395
context: ContextSpecs;
389396
isRequestBodyOptional: boolean;
397+
keyPrefix?: string;
398+
depth?: number;
390399
}) => {
391400
const formDataValues = Object.entries(schema.properties ?? {}).reduce(
392401
(acc, [key, value]) => {
@@ -407,16 +416,50 @@ const resolveSchemaPropertiesToFormData = ({
407416
const nonOptionalValueKey = `${propName}${formattedKey}`;
408417

409418
if (property.type === 'object') {
410-
formDataValue = `${variableName}.append('${key}', JSON.stringify(${nonOptionalValueKey}));\n`;
419+
if (
420+
context.output.override.formData.arrayHandling ===
421+
FormDataArrayHandling.EXPLODE
422+
) {
423+
formDataValue = resolveSchemaPropertiesToFormData({
424+
schema: property,
425+
variableName,
426+
propName: nonOptionalValueKey,
427+
context,
428+
isRequestBodyOptional,
429+
keyPrefix: `${keyPrefix}${key}.`,
430+
depth: depth + 1,
431+
});
432+
} else {
433+
formDataValue = `${variableName}.append(\`${keyPrefix}${key}\`, JSON.stringify(${nonOptionalValueKey}));\n`;
434+
}
411435
} else if (property.type === 'array') {
412436
let valueStr = 'value';
437+
let hasNonPrimitiveChild = false;
413438
if (property.items) {
414439
const { schema: itemSchema } = resolveRef<SchemaObject>(
415440
property.items,
416441
context,
417442
);
418443
if (itemSchema.type === 'object' || itemSchema.type === 'array') {
419-
valueStr = 'JSON.stringify(value)';
444+
if (
445+
context.output.override.formData.arrayHandling ===
446+
FormDataArrayHandling.EXPLODE
447+
) {
448+
hasNonPrimitiveChild = true;
449+
const resolvedValue = resolveSchemaPropertiesToFormData({
450+
schema: itemSchema,
451+
variableName,
452+
propName: 'value',
453+
context,
454+
isRequestBodyOptional,
455+
keyPrefix: `${keyPrefix}${key}[\${index${depth > 0 ? depth : ''}}].`,
456+
depth: depth + 1,
457+
});
458+
formDataValue = `${valueKey}.forEach((value, index${depth > 0 ? depth : ''}) => {
459+
${resolvedValue}});\n`;
460+
} else {
461+
valueStr = 'JSON.stringify(value)';
462+
}
420463
} else if (
421464
itemSchema.type === 'number' ||
422465
itemSchema.type?.includes('number') ||
@@ -428,7 +471,16 @@ const resolveSchemaPropertiesToFormData = ({
428471
valueStr = 'value.toString()';
429472
}
430473
}
431-
formDataValue = `${valueKey}.forEach(value => ${variableName}.append('${key}', ${valueStr}));\n`;
474+
if (
475+
context.output.override.formData.arrayHandling ===
476+
FormDataArrayHandling.EXPLODE
477+
) {
478+
if (!hasNonPrimitiveChild) {
479+
formDataValue = `${valueKey}.forEach((value, index${depth > 0 ? depth : ''}) => ${variableName}.append(\`${keyPrefix}${key}[\${index${depth > 0 ? depth : ''}}]\`, ${valueStr}));\n`;
480+
}
481+
} else {
482+
formDataValue = `${valueKey}.forEach(value => ${variableName}.append(\`${keyPrefix}${key}${context.output.override.formData.arrayHandling === FormDataArrayHandling.SERIALIZE_WITH_BRACKETS ? '[]' : ''}\`, ${valueStr}));\n`;
483+
}
432484
} else if (
433485
property.type === 'number' ||
434486
property.type?.includes('number') ||
@@ -437,9 +489,9 @@ const resolveSchemaPropertiesToFormData = ({
437489
property.type === 'boolean' ||
438490
property.type?.includes('boolean')
439491
) {
440-
formDataValue = `${variableName}.append('${key}', ${nonOptionalValueKey}.toString())\n`;
492+
formDataValue = `${variableName}.append(\`${keyPrefix}${key}\`, ${nonOptionalValueKey}.toString())\n`;
441493
} else {
442-
formDataValue = `${variableName}.append('${key}', ${nonOptionalValueKey})\n`;
494+
formDataValue = `${variableName}.append(\`${keyPrefix}${key}\`, ${nonOptionalValueKey})\n`;
443495
}
444496

445497
let existSubSchemaNullable = false;
@@ -453,7 +505,7 @@ const resolveSchemaPropertiesToFormData = ({
453505
return ['number', 'integer', 'boolean'].includes(subSchema.type);
454506
})
455507
) {
456-
formDataValue = `${variableName}.append('${key}', ${nonOptionalValueKey}.toString())\n`;
508+
formDataValue = `${variableName}.append(\`${key}\`, ${nonOptionalValueKey}.toString())\n`;
457509
}
458510

459511
if (

packages/core/src/types.ts

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ export type NormalizedOverrideOutput = {
8181
mock?: OverrideMockOptions;
8282
contentType?: OverrideOutputContentType;
8383
header: false | ((info: InfoObject) => string[] | string);
84-
formData: boolean | NormalizedMutator;
84+
formData: NormalizedFormDataType<NormalizedMutator>;
8585
formUrlEncoded: boolean | NormalizedMutator;
8686
paramsSerializer?: NormalizedMutator;
8787
paramsSerializerOptions?: NormalizedParamsSerializerOptions;
@@ -148,7 +148,7 @@ export type NormalizedOperationOptions = {
148148
verb: Verbs,
149149
) => string;
150150
fetch?: FetchOptions;
151-
formData?: boolean | NormalizedMutator;
151+
formData?: NormalizedFormDataType<NormalizedMutator>;
152152
formUrlEncoded?: boolean | NormalizedMutator;
153153
paramsSerializer?: NormalizedMutator;
154154
requestOptions?: object | boolean;
@@ -379,6 +379,36 @@ export type ParamsSerializerOptions = {
379379
qs?: Record<string, any>;
380380
};
381381

382+
export const FormDataArrayHandling = {
383+
SERIALIZE: 'serialize',
384+
EXPLODE: 'explode',
385+
SERIALIZE_WITH_BRACKETS: 'serialize-with-brackets',
386+
} as const;
387+
388+
export type FormDataArrayHandling =
389+
(typeof FormDataArrayHandling)[keyof typeof FormDataArrayHandling];
390+
391+
export type NormalizedFormDataType<TMutator> =
392+
| {
393+
disabled: true;
394+
mutator?: never;
395+
arrayHandling: FormDataArrayHandling;
396+
}
397+
| {
398+
disabled: false;
399+
mutator?: TMutator;
400+
arrayHandling: FormDataArrayHandling;
401+
};
402+
export type FormDataType<TMutator> =
403+
| {
404+
mutator: TMutator;
405+
arrayHandling?: FormDataArrayHandling;
406+
}
407+
| {
408+
mutator?: TMutator;
409+
arrayHandling: FormDataArrayHandling;
410+
};
411+
382412
export type OverrideOutput = {
383413
title?: (title: string) => string;
384414
transformer?: OutputTransformer;
@@ -388,7 +418,7 @@ export type OverrideOutput = {
388418
mock?: OverrideMockOptions;
389419
contentType?: OverrideOutputContentType;
390420
header?: boolean | ((info: InfoObject) => string[] | string);
391-
formData?: boolean | Mutator;
421+
formData?: boolean | Mutator | FormDataType<Mutator>;
392422
formUrlEncoded?: boolean | Mutator;
393423
paramsSerializer?: Mutator;
394424
paramsSerializerOptions?: ParamsSerializerOptions;
@@ -605,7 +635,7 @@ export type OperationOptions = {
605635
verb: Verbs,
606636
) => string;
607637
fetch?: FetchOptions;
608-
formData?: boolean | Mutator;
638+
formData?: boolean | Mutator | FormDataType<Mutator>;
609639
formUrlEncoded?: boolean | Mutator;
610640
paramsSerializer?: Mutator;
611641
requestOptions?: object | boolean;

packages/fetch/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export const generateRequestFunction = (
3737
{ route, context, pathRoute }: GeneratorOptions,
3838
) => {
3939
const isRequestOptions = override?.requestOptions !== false;
40-
const isFormData = override?.formData !== false;
40+
const isFormData = override?.formData.disabled === false;
4141
const isFormUrlEncoded = override?.formUrlEncoded !== false;
4242

4343
const getUrlFnName = camel(`get-${operationName}-url`);

0 commit comments

Comments
 (0)