samchon / nestia

NestJS Helper Libraries + TypeScript OpenAPI generator
https://nestia.io/
MIT License
1.85k stars 95 forks source link

Add support for React Native in @TypedFormData.Body() #1018

Closed omaziarz closed 1 month ago

omaziarz commented 2 months ago

Feature Request

In react native the only way to send Files in a multipart request is by doing the following :

const formdata = new FormData();
formdata.append('file', {
  uri: 'path-to-file-on-the-phone',
  name: 'file-name',
  type: 'image/png',
});

this doesn't work :

formdata.append('file', new Blob(...))
formdata.append('file', new File(...))

As expected when using the first approach I will get the following error when using the custom object:

[Error: {"path":"$input.file","reason":"Error on typia.http.assertFormData(): invalid type on $input.file, expect to be Blob","expected":"Blob","value":"[object Object]","message":"Request multipart data is not following the promised type."}]

I think that to support all environments, validation inside @TypedFormData.Body() should support the custom object as a valid Blob because, at least, in fastify the custom object works the same way as a Blob without any additional handling needed

//this is not of type Blob but it works when sent to fastify
{
  uri: 'path-to-file-on-the-phone',
  name: 'file-name',
  type: 'image/png',
}
samchon commented 2 months ago

I have a question. Is it really okay just by writinng the string path? It actually converted to the File instance?

Can you show me an example creating the FormData instance in the React Native with exact type?

I want to know about below type exactly. Is it really okay with primitive object instance? Or needs another class type?

{
  uri: 'path-to-file-on-the-phone',
  name: 'file-name',
  type: 'image/png',
}
omaziarz commented 2 months ago

yes sure

React-Native

import { SafeAreaView, Button } from 'react-native';

export default function HomeScreen() {
  const handlePress = async () => {
    const body = new FormData();
    body.append('file', {
      name: 'image.png',
      type: 'image/png',
      uri: 'https://picsum.photos/500/500', // this can be any uri so local phone uri or internet
    });

    fetch('http://192.168.0.106:3050/upload', {
      method: 'POST',
      headers: {
        'Content-Type': 'multipart/form-data',
      },
      body,
    });
  };

  return (
    <SafeAreaView
      style={{
        flex: 1,
        backgroundColor: '#fff',
        alignItems: 'center',
        gap: 100,
      }}
    >
      <Button title="changePP" onPress={handlePress} />
    </SafeAreaView>
  );
}

Fastify Server

import fastify from 'fastify';
import multipart from '@fastify/multipart';
const server = fastify();

server.register(multipart);

server.post('/upload', async (req, res) => {
  const file = await req.file();

  console.log((await file?.toBuffer())?.byteLength);
});

server.listen({
  port: 3050,
  host: '0.0.0.0',
});

The value of console.log((await file?.toBuffer())?.byteLength); will be > 0, fastify handles it without any added logic

samchon commented 2 months ago

In the @nestia/sdk, both client and server side must have the same type.

Therefore, I don't know how to solve this problem especially for the React Native.

For a while, how about solving this problem by hard casting to the File class in the RN client side?

I will consider the adapt the File | IFileUri type in the @TypedFormData.Body() decorator, but need your suggestion.

omaziarz commented 2 months ago

what do you mean by hard casting ? using the as keyword ?

If the File | IFileUri solution is doable for you then IFileUri should be the following :

interface IFileUri {
  uri: string;
  name: string;
  type: string; 
}

these are the only required properties.

samchon commented 1 month ago

I'll solve this problem by making below type.

By the way, I'm considering below type names.

@omaziarz, will you determine the type name?

export type FormDataInput<T extends object> =
  T extends Array<any>
    ? never
    : T extends Function
      ? never
      : {
          [P in keyof T]: T[P] extends Array<infer U>
            ? FormDataValue<U>[]
            : FormDataValue<T[P]>;
        };

type FormDataValue<T> = T extends File ? T | FileProps : T;

interface FileProps {
  uri: string;
  name: string;
  type: string;
}
omaziarz commented 1 month ago

I'll solve this problem by making below type.

By the way, I'm considering below type names.

@omaziarz, will you determine the type name?

  • FormDataInput
  • FormDataRequest
  • FormDataProps
export type FormDataInput<T extends object> =
  T extends Array<any>
    ? never
    : T extends Function
      ? never
      : {
          [P in keyof T]: T[P] extends Array<infer U>
            ? FormDataValue<U>[]
            : FormDataValue<T[P]>;
        };

type FormDataValue<T> = T extends File ? T | FileProps : T;

interface FileProps {
  uri: string;
  name: string;
  type: string;
}

Well i'm not really good with names myself haha but FormDataInput sounds good to me. And hit me up when you need me to test it before publishing a new version. Thanks for your reactivity :)

samchon commented 1 month ago

Upgrade to latest. Would works properly.

omaziarz commented 1 month ago

@samchon just to confirm with you that it works thank you very much !!