0

Ok, so I have an endpoint on my NestJs server that accepts BOTH files AND other data properties. It looks like this:

@Post()
    @UseInterceptors(
    FileFieldsInterceptor([
        { name: 'settingsFile', maxCount: 1 },
                { name: 'imageFile', maxCount: 1}
    ])
    )
    async create(
    @Body() createCaptionDto: CreatePostDto,
    @UploadedFiles()
    files: {
        settingsFile: Express.Multer.File[];
                imageFile: Express.Multer.File[];
    }
    ) {...

CreatePostDto looks like this:

export class CreatePostDto {
        ...

    @IsArray()
    @ArrayMinSize(1)
    // @ValidateNested({ each: true })
    // @Type(() => PostSource)
    sources: Array<PostSource>;

    @IsArray()
    @IsString({ each: true })
    tags: Array<string>;
}

The problem arises when I try to call this endpoint from the front-end. I am using JS fetch API, and to send the multiple files, I am using JS FormData object as the fetch API body, as per the NestJs documentation and this question.

this works well for almost all values I wish to put in the body. As you can see from the CreatePostDto above, the tags array of strings works perfectly. I construct it like this:

for (const tag of createCaptionDto.tags) {
    formData.append('tags[]', tag);
}

Here every tag is a string. But when I need an array of objects like in the Sources array, I run into many different problems.

If I try...

formData.append('sources[]', JSON.stringify(sources));

, the server parses it as:

['[{attr1: 223, attr2: 3737}, {attr1: 235325, attr2: 5366}]']

So it is just the stringified array of objects inside another array. This is not right.

So then I try...

for (const source of createCaptionDto.sources) {
    formData.append('sources[]', JSON.stringify(source));
}

, the server parses it as:

['{attr1: 223, attr2: 3737}', '{attr1: 235325, attr2: 5366}']

Closer, but still not right.

So then I try...

formData.append('sources', JSON.stringify(sources));

... but NestJs doesn't think this is an array.

So it seems no matter what I do, there is a fundamental mismatch in what serialization needs to be done to send the request through FormData and what deserialization is being done on the server. Does anyone have any idea what I can do about this? Or have done something similar before? Thank you

HSE
  • 11
  • 1
  • 2

1 Answers1

0

I was able to solve it, despite this solution coming only one hour after the question was posted, I posted it because I had abandoned any hope of doing it in a practical way. I instead made my own serializer that attaches an object to the formData object (serializing the object the same way fetch() does). Here is the code:

export default function attachBodyToFormData(
    obj: Record<string, any>,
    formData: FormData,
    parentKey?: string
) {
    for (const key in obj) {
        if (obj.hasOwnProperty(key)) {
            const value = obj[key];

            // Create a new key name by combining the parent key with the current key, separated by square brackets
            const newKey = parentKey ? `${parentKey}[${key}]` : key;

            if (typeof value === 'object' && !Array.isArray(value)) {
                // Recursively call this function if the value is an object
                attachBodyToFormData(value, formData, newKey);
            } else if (Array.isArray(value)) {
                // If the value is an array, iterate through each item and call this function recursively
                value.forEach((item: any, index: number) => {
                    const arrayKey = `${newKey}[${index}]`;
                    if (typeof item === 'object') {
                        attachBodyToFormData(item, formData, arrayKey);
                    } else {
                        formData.append(arrayKey, item);
                    }
                });
            } else {
                // Add the key-value pair to the FormData object
                formData.append(newKey, value);
            }
        }
    }
}

HSE
  • 11
  • 1
  • 2