Skip to content

Commit

Permalink
Fix #1018: add FormDataInput<T> type for React Native environment.
Browse files Browse the repository at this point in the history
In the React Native, it does not support the `File` class.

Instead, it can compose the `FormData` class instance by appending below primitive object type. The primitive type can replace the `File` class instance in the React Native.

In such reason, added `FormDataInput<T>` type which casts `File` typed property value as `File | IFileProps` union type, and changed SDK generator to utilize the type for the `multipart/form-data` request body type.
  • Loading branch information
samchon committed Sep 17, 2024
1 parent f7f5819 commit c0d2858
Show file tree
Hide file tree
Showing 15 changed files with 138 additions and 34 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"private": true,
"name": "@nestia/station",
"version": "3.13.0",
"version": "3.14.0-dev.20240918",
"description": "Nestia station",
"scripts": {
"build": "node build/index.js",
Expand Down
8 changes: 4 additions & 4 deletions packages/core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@nestia/core",
"version": "3.13.0",
"version": "3.14.0-dev.20240918",
"description": "Super-fast validation decorators of NestJS",
"main": "lib/index.js",
"typings": "lib/index.d.ts",
Expand Down Expand Up @@ -36,10 +36,10 @@
},
"homepage": "https://nestia.io",
"dependencies": {
"@nestia/fetcher": "^3.13.0",
"@nestia/fetcher": "^3.14.0-dev.20240918",
"@nestjs/common": ">=7.0.1",
"@nestjs/core": ">=7.0.1",
"@samchon/openapi": "^0.4.6",
"@samchon/openapi": "^1.0.2",
"detect-ts-node": "^1.0.5",
"get-function-location": "^2.0.0",
"glob": "^7.2.0",
Expand All @@ -53,7 +53,7 @@
"ws": "^7.5.3"
},
"peerDependencies": {
"@nestia/fetcher": ">=3.13.0",
"@nestia/fetcher": ">=3.14.0-dev.20240918",
"@nestjs/common": ">=7.0.1",
"@nestjs/core": ">=7.0.1",
"reflect-metadata": ">=0.1.12",
Expand Down
4 changes: 2 additions & 2 deletions packages/fetcher/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@nestia/fetcher",
"version": "3.13.0",
"version": "3.14.0-dev.20240918",
"description": "Fetcher library of Nestia SDK",
"main": "lib/index.js",
"typings": "lib/index.d.ts",
Expand All @@ -26,7 +26,7 @@
},
"homepage": "https://nestia.io",
"dependencies": {
"@samchon/openapi": "^1.0.0"
"@samchon/openapi": "^1.0.2"
},
"peerDependencies": {
"typescript": ">= 4.8.0"
Expand Down
88 changes: 88 additions & 0 deletions packages/fetcher/src/FormDataInput.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/**
* FormData input type.
*
* `FormDataInput<T>` is a type for the input of the `FormData` request, casting
* `File` property value type as an union of `File` and {@link FormDataInput.IFileProps},
* especially for the React Native environment.
*
* You know what? In the React Native environment, `File` class is not supported.
* Therefore, when composing a `FormData` request, you have to put the URI address
* of the local filesystem with file name and contet type that is represented by the
* {@link FormDataInput.IFileProps} type.
*
* This `FormDataInput<T>` type is designed for that purpose. If the property value
* type is a `File` class, it converts it to an union type of `File` and
* {@link FormDataInput.IFileProps} type. Also, if the property value type is an array
* of `File` class, it converts it to an array of union type of `File` and
* {@link FormDataInput.IFileProps} type too.
*
* Before | After
* ----------|------------------------
* `boolean` | `boolean`
* `bigint` | `bigint`
* `number` | `number`
* `string` | `string`
* `File` | `File \| IFileProps`
* `Object` | `never`
*
* @template T Target object type.
* @author Jeongho Nam - https://github.com/samchon
*/
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>
? FormDataInput.Value<U>[]
: FormDataInput.Value<T[P]>;
};
export namespace FormDataInput {
/**
* Value type of the `FormDataInput`.
*
* `Value<T>` is a type for the property value defined in the `FormDataInput`.
*
* If the original value type is a `File` class, `Value<T>` converts it to an union
* type of `File` and {@link IFileProps} type which is a structured data for the
* URI file location in the React Native environment.
*/
export type Value<T> = T extends File ? T | IFileProps : T;

/**
* Properties of a file.
*
* In the React Native, this `IFileProps` structured data can replace the `File`
* class instance in the `FormData` request.
*
* Just put the {@link uri URI address} of the local file system with the file's
* {@link name} and {@link type}. It would be casted to the `File` class instance
* automatically in the `FormData` request.
*
* Note that, this `IFileProps` type works only in the React Native environment.
* If you are developing a Web or NodeJS application, you have to utilize the
* `File` class instance directly.
*/
export interface IFileProps {
/**
* URI address of the file.
*
* In the React Native, the URI address in the local file system can replace
* the `File` class instance. If
*
* @format uri
*/
uri: string;

/**
* Name of the file.
*/
name: string;

/**
* Content type of the file.
*/
type: string;
}
}
1 change: 1 addition & 0 deletions packages/fetcher/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from "./FormDataInput";
export * from "./HttpError";
export * from "./IConnection";
export * from "./IEncryptionPassword";
Expand Down
7 changes: 3 additions & 4 deletions packages/fetcher/src/internal/FetcherBase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,10 +214,9 @@ const request_form_data_body = (input: Record<string, any>): FormData => {
const encoded: FormData = new FormData();
const append = (key: string) => (value: any) => {
if (value === undefined) return;
else if (value instanceof Blob)
if (value instanceof File) encoded.append(key, value, value.name);
else encoded.append(key, value);
else encoded.append(key, String(value));
else if (typeof File === "function" && value instanceof File)
encoded.append(key, value, value.name);
else encoded.append(key, value);
};
for (const [key, value] of Object.entries(input))
if (Array.isArray(value)) value.map(append(key));
Expand Down
2 changes: 1 addition & 1 deletion packages/migrate/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@
"typescript-transform-paths": "^3.4.6"
},
"dependencies": {
"@samchon/openapi": "^1.0.0",
"@samchon/openapi": "^1.0.2",
"commander": "10.0.0",
"inquirer": "8.2.5",
"prettier": "^3.2.5",
Expand Down
12 changes: 6 additions & 6 deletions packages/sdk/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@nestia/sdk",
"version": "3.13.0",
"version": "3.14.0-dev.20240918",
"description": "Nestia SDK and Swagger generator",
"main": "lib/index.js",
"typings": "lib/index.d.ts",
Expand Down Expand Up @@ -32,9 +32,9 @@
},
"homepage": "https://nestia.io",
"dependencies": {
"@nestia/core": "^3.13.0",
"@nestia/fetcher": "^3.13.0",
"@samchon/openapi": "^1.0.0",
"@nestia/core": "^3.14.0-dev.20240918",
"@nestia/fetcher": "^3.14.0-dev.20240918",
"@samchon/openapi": "^1.0.2",
"cli": "^1.0.1",
"get-function-location": "^2.0.0",
"glob": "^7.2.0",
Expand All @@ -47,8 +47,8 @@
"typia": "^6.10.0"
},
"peerDependencies": {
"@nestia/core": ">=3.13.0",
"@nestia/fetcher": ">=3.13.0",
"@nestia/core": ">=3.14.0-dev.20240918",
"@nestia/fetcher": ">=3.14.0-dev.20240918",
"@nestjs/common": ">=7.0.1",
"@nestjs/core": ">=7.0.1",
"reflect-metadata": ">=0.1.12",
Expand Down
24 changes: 20 additions & 4 deletions packages/sdk/src/generates/internal/SdkAliasCollection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,17 @@ export namespace SdkAliasCollection {
export const input =
(project: INestiaProject) =>
(importer: ImportDictionary) =>
(param: ITypedHttpRouteParameter): ts.TypeNode => {
if (project.config.clone === true)
return from(project)(importer)(param.metadata);
(param: ITypedHttpRouteParameter.IBody): ts.TypeNode => {
if (project.config.clone === true) {
const type: ts.TypeNode = from(project)(importer)(param.metadata);
return param.contentType === "multipart/form-data"
? formDataInput(importer)(type)
: type;
}
const type: ts.TypeNode = name(param);
if (project.config.primitive === false) return type;
if (param.contentType === "multipart/form-data")
return formDataInput(importer)(type);
else if (project.config.primitive === false) return type;
return ts.factory.createTypeReferenceNode(
importer.external({
type: true,
Expand Down Expand Up @@ -161,6 +167,16 @@ export namespace SdkAliasCollection {
propagate: false,
},
})(importer)(route);

const formDataInput = (importer: ImportDictionary) => (type: ts.TypeNode) =>
ts.factory.createTypeReferenceNode(
importer.external({
type: true,
library: "@nestia/fetcher",
instance: "FormDataInput",
}),
[type],
);
}

interface IBranch {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* @nestia Generated by Nestia - https://github.com/samchon/nestia
*/
//================================================================
import type { IConnection, Resolved, Primitive } from "@nestia/fetcher";
import type { IConnection, FormDataInput, Primitive } from "@nestia/fetcher";
import { PlainFetcher } from "@nestia/fetcher/lib/PlainFetcher";

import type { IMultipart } from "../../structures/IMultipart";
Expand All @@ -29,7 +29,7 @@ export async function post(
);
}
export namespace post {
export type Input = Resolved<IMultipart>;
export type Input = FormDataInput<IMultipart>;
export type Output = Primitive<IMultipart.IContent>;

export const METADATA = {
Expand Down
2 changes: 1 addition & 1 deletion test/features/multipart-form-data-fastify/swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
}
],
"info": {
"version": "3.13.0-dev.2024910",
"version": "3.14.0-dev.20240917",
"title": "@samchon/nestia-test",
"description": "Test program of Nestia",
"license": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* @nestia Generated by Nestia - https://github.com/samchon/nestia
*/
//================================================================
import type { IConnection, Resolved, Primitive } from "@nestia/fetcher";
import type { IConnection, FormDataInput, Primitive } from "@nestia/fetcher";
import { PlainFetcher } from "@nestia/fetcher/lib/PlainFetcher";

import type { IMultipart } from "../../structures/IMultipart";
Expand All @@ -29,7 +29,7 @@ export async function post(
);
}
export namespace post {
export type Input = Resolved<IMultipart>;
export type Input = FormDataInput<IMultipart>;
export type Output = Primitive<IMultipart.IContent>;

export const METADATA = {
Expand Down
2 changes: 1 addition & 1 deletion test/features/multipart-form-data/swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
}
],
"info": {
"version": "3.13.0-dev.2024910",
"version": "3.14.0-dev.20240917",
"title": "@samchon/nestia-test",
"description": "Test program of Nestia",
"license": {
Expand Down
10 changes: 5 additions & 5 deletions test/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"private": true,
"name": "@samchon/nestia-test",
"version": "3.13.0",
"version": "3.14.0-dev.20240918",
"description": "Test program of Nestia",
"main": "index.js",
"scripts": {
Expand All @@ -26,9 +26,9 @@
},
"homepage": "https://nestia.io",
"devDependencies": {
"@nestia/sdk": "^3.13.0",
"@nestia/sdk": "^3.14.0-dev.20240918",
"@nestjs/swagger": "^7.4.0",
"@samchon/openapi": "^1.0.1",
"@samchon/openapi": "^1.0.2",
"@types/express": "^4.17.17",
"@types/node": "20.11.16",
"@types/uuid": "^9.0.8",
Expand All @@ -40,9 +40,9 @@
},
"dependencies": {
"@fastify/multipart": "^8.1.0",
"@nestia/core": "^3.13.0",
"@nestia/core": "^3.14.0-dev.20240918",
"@nestia/e2e": "^0.7.0",
"@nestia/fetcher": "^3.13.0",
"@nestia/fetcher": "^3.14.0-dev.20240918",
"@nestjs/common": "^10.4.1",
"@nestjs/core": "^10.4.1",
"@nestjs/platform-express": "^10.4.1",
Expand Down
2 changes: 1 addition & 1 deletion website/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
"@mui/material": "5.15.6",
"@mui/system": "5.15.6",
"@nestia/migrate": "^0.18.0",
"@samchon/openapi": "^1.0.1",
"@samchon/openapi": "^1.0.2",
"@stackblitz/sdk": "^1.9.0",
"js-yaml": "^4.1.0",
"next": "14.2.5",
Expand Down

0 comments on commit c0d2858

Please sign in to comment.