From ae5bec1ffe6526a301ba5759dc23f0f7bcf2b4a1 Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Tue, 17 Sep 2024 13:00:52 +0100 Subject: [PATCH 01/11] feat: react native tests --- .github/workflows/tests.yml | 3 +- templates/react-native/package.json.twig | 1 - templates/react-native/src/client.ts.twig | 6 +- templates/web/package.json.twig | 1 - tests/Node16Test.php | 2 +- tests/ReactNativeStableTest.php | 35 ++++++ tests/WebChromiumTest.php | 7 +- tests/WebNodeTest.php | 8 +- tests/languages/react-native/node.js | 147 ++++++++++++++++++++++ 9 files changed, 194 insertions(+), 16 deletions(-) create mode 100644 tests/ReactNativeStableTest.php create mode 100644 tests/languages/react-native/node.js diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 5fcd67958..eb2d9ada7 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -24,8 +24,8 @@ jobs: Deno1303, DotNet60, DotNet80, - FlutterStable, FlutterBeta, + FlutterStable, Go122, KotlinJava8, KotlinJava11, @@ -38,6 +38,7 @@ jobs: Python38, Python39, Python310, + ReactNativeStable, Ruby27, Ruby30, Ruby31, diff --git a/templates/react-native/package.json.twig b/templates/react-native/package.json.twig index 685663876..0c81f8ce9 100644 --- a/templates/react-native/package.json.twig +++ b/templates/react-native/package.json.twig @@ -26,7 +26,6 @@ }, "devDependencies": { "@rollup/plugin-typescript": "8.3.2", - "playwright": "1.15.0", "rollup": "2.75.4", "serve-handler": "6.1.0", "tslib": "2.4.0", diff --git a/templates/react-native/src/client.ts.twig b/templates/react-native/src/client.ts.twig index 3e0bd8eb7..3544aa3ea 100644 --- a/templates/react-native/src/client.ts.twig +++ b/templates/react-native/src/client.ts.twig @@ -2,7 +2,7 @@ import { Models } from './models'; import { Service } from './service'; import { Platform } from 'react-native'; -type Payload = { +type Params = { [key: string]: any; } @@ -345,7 +345,7 @@ class Client { } } - async call(method: string, url: URL, headers: Headers = {}, params: Payload = {}): Promise { + async call(method: string, url: URL, headers: Headers = {}, params: Params = {}): Promise { method = method.toUpperCase(); headers = Object.assign({}, this.headers, headers); @@ -425,4 +425,4 @@ class Client { } export { Client, {{spec.title | caseUcfirst}}Exception }; -export type { Models, Payload }; +export type { Models, Params }; diff --git a/templates/web/package.json.twig b/templates/web/package.json.twig index da04619f4..8477d199e 100644 --- a/templates/web/package.json.twig +++ b/templates/web/package.json.twig @@ -26,7 +26,6 @@ }, "devDependencies": { "@rollup/plugin-typescript": "8.3.2", - "playwright": "1.46.0", "rollup": "2.75.4", "serve-handler": "6.1.0", "tslib": "2.4.0", diff --git a/tests/Node16Test.php b/tests/Node16Test.php index c37b55930..fbaf03c69 100644 --- a/tests/Node16Test.php +++ b/tests/Node16Test.php @@ -12,7 +12,7 @@ class Node16Test extends Base protected string $language = 'node'; protected string $class = 'Appwrite\SDK\Language\Node'; protected array $build = [ - 'cp tests/languages/node/test.js tests/sdks/node/test.js', + 'cp -R tests/languages/node/* tests/sdks/node/', 'docker run --rm -v $(pwd):/app -w /app/tests/sdks/node node:16-alpine npm install', 'docker run --rm -v $(pwd):/app -w /app/tests/sdks/node node:16-alpine npm run build' ]; diff --git a/tests/ReactNativeStableTest.php b/tests/ReactNativeStableTest.php new file mode 100644 index 000000000..fee9d6fa3 --- /dev/null +++ b/tests/ReactNativeStableTest.php @@ -0,0 +1,35 @@ + Date: Tue, 17 Sep 2024 14:19:12 +0100 Subject: [PATCH 02/11] feat: params to payload --- src/SDK/Language/ReactNative.php | 12 +++- templates/deno/src/service.ts.twig | 2 +- templates/react-native/src/client.ts.twig | 1 + templates/react-native/src/index.ts.twig | 3 +- templates/react-native/src/models.ts.twig | 2 + templates/react-native/src/payload.ts.twig | 63 +++++++++++++++++++ templates/react-native/src/service.ts.twig | 8 +-- .../src/services/template.ts.twig | 21 ++++--- tests/ReactNativeStableTest.php | 8 +-- 9 files changed, 97 insertions(+), 23 deletions(-) create mode 100644 templates/react-native/src/payload.ts.twig diff --git a/src/SDK/Language/ReactNative.php b/src/SDK/Language/ReactNative.php index d0b4cb8f6..289309c91 100644 --- a/src/SDK/Language/ReactNative.php +++ b/src/SDK/Language/ReactNative.php @@ -2,8 +2,6 @@ namespace Appwrite\SDK\Language; -use Twig\TwigFilter; - class ReactNative extends Web { /** @@ -65,6 +63,11 @@ public function getFiles(): array 'destination' => 'src/query.ts', 'template' => 'react-native/src/query.ts.twig', ], + [ + 'scope' => 'default', + 'destination' => 'src/payload.ts', + 'template' => 'react-native/src/payload.ts.twig', + ], [ 'scope' => 'default', 'destination' => 'README.md', @@ -145,8 +148,9 @@ public function getTypeName(array $parameter, array $spec = []): string return $this->getTypeName($parameter['array']) . '[]'; } return 'string[]'; + case self::TYPE_PAYLOAD: case self::TYPE_FILE: - return '{name: string, type: string, size: number, uri: string}'; + return 'Payload'; } return $parameter['type']; @@ -179,6 +183,7 @@ public function getParamExample(array $param): string case self::TYPE_OBJECT: $output .= '{}'; break; + case self::TYPE_PAYLOAD: case self::TYPE_FILE: $output .= "await pickSingle()"; break; @@ -197,6 +202,7 @@ public function getParamExample(array $param): string case self::TYPE_STRING: $output .= "'{$example}'"; break; + case self::TYPE_PAYLOAD: case self::TYPE_FILE: $output .= "await pickSingle()"; break; diff --git a/templates/deno/src/service.ts.twig b/templates/deno/src/service.ts.twig index 300fa827a..dbdd758d4 100644 --- a/templates/deno/src/service.ts.twig +++ b/templates/deno/src/service.ts.twig @@ -1,4 +1,4 @@ -import { Client } from "./client.ts"; + import { Client } from "./client.ts"; export abstract class Service { client: Client; diff --git a/templates/react-native/src/client.ts.twig b/templates/react-native/src/client.ts.twig index 3544aa3ea..28dac59d9 100644 --- a/templates/react-native/src/client.ts.twig +++ b/templates/react-native/src/client.ts.twig @@ -1,5 +1,6 @@ import { Models } from './models'; import { Service } from './service'; +import { Payload } from './payload'; import { Platform } from 'react-native'; type Params = { diff --git a/templates/react-native/src/index.ts.twig b/templates/react-native/src/index.ts.twig index 2a31f330c..53b2a0c71 100644 --- a/templates/react-native/src/index.ts.twig +++ b/templates/react-native/src/index.ts.twig @@ -2,12 +2,13 @@ export { Client, {{spec.title | caseUcfirst}}Exception } from './client'; {% for service in spec.services %} export { {{service.name | caseUcfirst}} } from './services/{{service.name | caseDash}}'; {% endfor %} -export type { Models, Payload, RealtimeResponseEvent, UploadProgress } from './client'; +export type { Models, Params, RealtimeResponseEvent, UploadProgress } from './client'; export type { QueryTypes, QueryTypesList } from './query'; export { Query } from './query'; export { Permission } from './permission'; export { Role } from './role'; export { ID } from './id'; +export { Payload } from './payload'; {% for enum in spec.enums %} export { {{ enum.name | caseUcfirst }} } from './enums/{{enum.name | caseDash}}'; {% endfor %} \ No newline at end of file diff --git a/templates/react-native/src/models.ts.twig b/templates/react-native/src/models.ts.twig index 4b0f63f8b..c933904b7 100644 --- a/templates/react-native/src/models.ts.twig +++ b/templates/react-native/src/models.ts.twig @@ -1,3 +1,5 @@ +import { Payload } from './payload'; + export namespace Models { {% for definition in spec.definitions %} /** diff --git a/templates/react-native/src/payload.ts.twig b/templates/react-native/src/payload.ts.twig new file mode 100644 index 000000000..ae1996070 --- /dev/null +++ b/templates/react-native/src/payload.ts.twig @@ -0,0 +1,63 @@ +interface ReactNativeFileObject { + uri: string; + type?: string; + name?: string; +} + +export class Payload { + private data: Buffer; + public filename?: string; + public size: number; + + constructor(data: Buffer, filename?: string) { + this.data = data; + this.filename = filename; + this.size = data.byteLength; + } + + public toBinary(offset: number = 0, length?: number): Buffer { + if (offset === 0 && length === undefined) { + return this.data; + } else if (length === undefined) { + return this.data.subarray(offset); + } else { + return this.data.subarray(offset, offset + length); + } + } + + public toFileObject(type: string): ReactNativeFileObject { + return { + uri: `data:${type};base64,${this.data.toString("base64")}`, + type: type, + name: this.filename, + }; + } + + public toJson(): Promise { + return JSON.parse(this.toString()); + } + + public toString(): string { + return this.data.toString("utf-8"); + } + + public static fromBinary(bytes: Buffer, name?: string): Payload { + return new Payload(bytes, name); + } + + public static fromJson(object: any, name?: string): Payload { + const data = Buffer.from(JSON.stringify(object), "utf-8"); + return new Payload(data, name); + } + + public static fromString(text: string, name?: string): Payload { + const data = Buffer.from(text, "utf-8"); + return new Payload(data, name); + } + + public static async fromFileObject(file: ReactNativeFileObject): Promise { + const response = await fetch(file.uri); + const data = Buffer.from(await response.arrayBuffer()); + return new Payload(data, file.name); + } +} \ No newline at end of file diff --git a/templates/react-native/src/service.ts.twig b/templates/react-native/src/service.ts.twig index fe1769929..cbc2701ba 100644 --- a/templates/react-native/src/service.ts.twig +++ b/templates/react-native/src/service.ts.twig @@ -1,5 +1,5 @@ import { Client } from './client'; -import type { Payload } from './client'; +import type { Params } from './client'; export class Service { static CHUNK_SIZE = 5*1024*1024; // 5MB @@ -10,9 +10,9 @@ export class Service { this.client = client; } - static flatten(data: Payload, prefix = ''): Payload { - let output: Payload = {}; - + static flatten(data: Params, prefix = ''): Params { + let output: Params = {}; + for (const [key, value] of Object.entries(data)) { let finalKey = prefix ? prefix + '[' + key +']' : key; if (Array.isArray(value)) { diff --git a/templates/react-native/src/services/template.ts.twig b/templates/react-native/src/services/template.ts.twig index 347ecfb73..66025fdf2 100644 --- a/templates/react-native/src/services/template.ts.twig +++ b/templates/react-native/src/services/template.ts.twig @@ -1,7 +1,8 @@ import { Service } from '../service'; import { {{ spec.title | caseUcfirst}}Exception, Client } from '../client'; +import { Payload } from '../payload'; import type { Models } from '../models'; -import type { UploadProgress, Payload } from '../client'; +import type { UploadProgress, Params } from '../client'; import * as FileSystem from 'expo-file-system'; import { Platform } from 'react-native'; @@ -54,17 +55,17 @@ export class {{ service.name | caseUcfirst }} extends Service { {% endif %} {% endfor %} const apiPath = '{{ method.path }}'{% for parameter in method.parameters.path %}.replace('{{ '{' }}{{ parameter.name | caseCamel | escapeKeyword }}{{ '}' }}', {{ parameter.name | caseCamel | escapeKeyword }}){% endfor %}; - const payload: Payload = {}; + const params: Params = {}; {% for parameter in method.parameters.query %} if (typeof {{ parameter.name | caseCamel | escapeKeyword }} !== 'undefined') { - payload['{{ parameter.name }}'] = {{ parameter.name | caseCamel | escapeKeyword }}; + params['{{ parameter.name }}'] = {{ parameter.name | caseCamel | escapeKeyword }}; } {% endfor %} {% for parameter in method.parameters.body %} if (typeof {{ parameter.name | caseCamel | escapeKeyword }} !== 'undefined') { - payload['{{ parameter.name }}'] = {{ parameter.name | caseCamel | escapeKeyword }}; + params['{{ parameter.name }}'] = {{ parameter.name | caseCamel | escapeKeyword }}; } {% endfor %} @@ -73,13 +74,13 @@ export class {{ service.name | caseUcfirst }} extends Service { {% if method.auth|length > 0 %} {% for node in method.auth %} {% for key,header in node|keys %} - payload['{{header|caseLower}}'] = this.client.config.{{header|caseLower}}; + params['{{header|caseLower}}'] = this.client.config.{{header|caseLower}}; {% endfor %} {% endfor %} {% endif %} - for (const [key, value] of Object.entries(Service.flatten(payload))) { + for (const [key, value] of Object.entries(Service.flatten(params))) { uri.searchParams.append(key, value); } {% endif %} @@ -102,7 +103,7 @@ export class {{ service.name | caseUcfirst }} extends Service { {% for key, header in method.headers %} '{{ key }}': '{{ header }}', {% endfor %} - }, payload); + }, params); } const apiHeaders: { [header: string]: string } = { @@ -148,9 +149,9 @@ export class {{ service.name | caseUcfirst }} extends Service { await FileSystem.writeAsStringAsync(path, chunk, {encoding: FileSystem.EncodingType.Base64}); } - payload['{{ parameter.name }}'] = {{ '{' }} uri: path, name: {{ parameter.name | caseCamel | escapeKeyword }}.name, type: {{ parameter.name | caseCamel | escapeKeyword }}.type {{ '}' }}; + params['{{ parameter.name }}'] = {{ '{' }} uri: path, name: {{ parameter.name | caseCamel | escapeKeyword }}.name, type: {{ parameter.name | caseCamel | escapeKeyword }}.type {{ '}' }}; - response = await this.client.call('{{ method.method | caseLower }}', uri, apiHeaders, payload); + response = await this.client.call('{{ method.method | caseLower }}', uri, apiHeaders, params); if (onProgress) { onProgress({ @@ -174,7 +175,7 @@ export class {{ service.name | caseUcfirst }} extends Service { {% for key, header in method.headers %} '{{ key }}': '{{ header }}', {% endfor %} - }, payload); + }, params); {% endif %} {% endif %} } diff --git a/tests/ReactNativeStableTest.php b/tests/ReactNativeStableTest.php index fee9d6fa3..215afadf1 100644 --- a/tests/ReactNativeStableTest.php +++ b/tests/ReactNativeStableTest.php @@ -9,15 +9,15 @@ class ReactNativeStableTest extends Base protected string $sdkLanguage = 'react-native'; protected string $version = '0.0.1'; - protected string $language = 'javascript'; + protected string $language = 'react-native'; protected string $class = 'Appwrite\SDK\Language\ReactNative'; protected array $build = [ 'cp -R tests/languages/react-native/* tests/sdks/react-native/', - 'docker run --rm -v $(pwd):/app -w /app/tests/sdks/react-native node:16-alpine npm install', - 'docker run --rm -v $(pwd):/app -w /app/tests/sdks/react-native node:16-alpine npm run build' + 'docker run --rm -v $(pwd):/app -w /app/tests/sdks/react-native node:18-alpine npm install', + 'docker run --rm -v $(pwd):/app -w /app/tests/sdks/react-native node:18-alpine npm run build' ]; protected string $command = - 'docker run --rm -v $(pwd):/app -w /app/tests/sdks/react-native node:16-alpine node node.js'; + 'docker run --rm -v $(pwd):/app -w /app/tests/sdks/react-native node:18-alpine node node.js'; protected array $expectedOutput = [ ...Base::FOO_RESPONSES, From 0129058762b436ced4faba587c7fb2749de73425 Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Tue, 17 Sep 2024 18:49:25 +0100 Subject: [PATCH 03/11] fix: payload use --- templates/react-native/src/payload.ts.twig | 133 +++++++------- .../src/services/template.ts.twig | 167 ++++++++---------- 2 files changed, 151 insertions(+), 149 deletions(-) diff --git a/templates/react-native/src/payload.ts.twig b/templates/react-native/src/payload.ts.twig index ae1996070..f17b265ad 100644 --- a/templates/react-native/src/payload.ts.twig +++ b/templates/react-native/src/payload.ts.twig @@ -1,63 +1,78 @@ interface ReactNativeFileObject { - uri: string; - type?: string; - name?: string; + uri: string; + type?: string; + name?: string; } export class Payload { - private data: Buffer; - public filename?: string; - public size: number; - - constructor(data: Buffer, filename?: string) { - this.data = data; - this.filename = filename; - this.size = data.byteLength; - } - - public toBinary(offset: number = 0, length?: number): Buffer { - if (offset === 0 && length === undefined) { - return this.data; - } else if (length === undefined) { - return this.data.subarray(offset); - } else { - return this.data.subarray(offset, offset + length); - } - } - - public toFileObject(type: string): ReactNativeFileObject { - return { - uri: `data:${type};base64,${this.data.toString("base64")}`, - type: type, - name: this.filename, - }; - } - - public toJson(): Promise { - return JSON.parse(this.toString()); - } - - public toString(): string { - return this.data.toString("utf-8"); - } - - public static fromBinary(bytes: Buffer, name?: string): Payload { - return new Payload(bytes, name); - } - - public static fromJson(object: any, name?: string): Payload { - const data = Buffer.from(JSON.stringify(object), "utf-8"); - return new Payload(data, name); - } - - public static fromString(text: string, name?: string): Payload { - const data = Buffer.from(text, "utf-8"); - return new Payload(data, name); - } - - public static async fromFileObject(file: ReactNativeFileObject): Promise { - const response = await fetch(file.uri); - const data = Buffer.from(await response.arrayBuffer()); - return new Payload(data, file.name); - } -} \ No newline at end of file + private data: Buffer; + public filename?: string; + public size: number; + public type?: string; + + constructor(data: Buffer, filename?: string, type?: string) { + this.data = data; + this.filename = filename; + this.size = data.byteLength; + this.type = type; + } + + public toBinary(offset: number = 0, length?: number): Buffer { + if (offset === 0 && length === undefined) { + return this.data; + } else if (length === undefined) { + return this.data.subarray(offset); + } else { + return this.data.subarray(offset, offset + length); + } + } + + public toFileObject(): ReactNativeFileObject { + const base64Data = this.data.toString("base64"); + const uri = `data:${this.type};base64,${base64Data}`; + return { + uri: uri, + type: this.type, + name: this.filename, + }; + } + + public toJson(): T { + return JSON.parse(this.toString()); + } + + public toString(): string { + return this.data.toString("utf-8"); + } + + public static fromBinary( + bytes: Buffer, + name?: string, + type?: string + ): Payload { + return new Payload(bytes, name, type); + } + + public static fromJson(object: any, name?: string): Payload { + const data = Buffer.from(JSON.stringify(object), "utf-8"); + return new Payload(data, name, "application/json"); + } + + public static fromString( + text: string, + name?: string, + type?: string + ): Payload { + const data = Buffer.from(text, "utf-8"); + return new Payload(data, name, type || "text/plain"); + } + + public static async fromFileObject( + file: ReactNativeFileObject + ): Promise { + const response = await fetch(file.uri); + const arrayBuffer = await response.arrayBuffer(); + const data = Buffer.from(arrayBuffer); + return new Payload(data, file.name, file.type); + } +} diff --git a/templates/react-native/src/services/template.ts.twig b/templates/react-native/src/services/template.ts.twig index 66025fdf2..a51bac97d 100644 --- a/templates/react-native/src/services/template.ts.twig +++ b/templates/react-native/src/services/template.ts.twig @@ -1,5 +1,5 @@ import { Service } from '../service'; -import { {{ spec.title | caseUcfirst}}Exception, Client } from '../client'; +import { {{ spec.title | caseUcfirst }}Exception, Client } from '../client'; import { Payload } from '../payload'; import type { Models } from '../models'; import type { UploadProgress, Params } from '../client'; @@ -7,21 +7,21 @@ import * as FileSystem from 'expo-file-system'; import { Platform } from 'react-native'; {% set added = [] %} -{% for method in service.methods %} -{% for parameter in method.parameters.all %} -{% if parameter.enumValues is not empty %} -{% if parameter.enumName is not empty %} -{% set name = parameter.enumName %} -{% else %} -{% set name = parameter.name %} -{% endif %} -{% if name not in added %} +{%~ for method in service.methods %} + {%~ for parameter in method.parameters.all %} + {%~ if parameter.enumValues is not empty %} + {%~ if parameter.enumName is not empty %} + {% set name = parameter.enumName %} + {% else %} + {% set name = parameter.name %} + {%- endif %} + {%~ if name not in added -%} import { {{ name | caseUcfirst }} } from '../enums/{{ name | caseDash }}'; -{% set added = added|merge([name]) %} -{% endif %} -{% endif %} -{% endfor %} -{% endfor %} + {%~ set added = added|merge([name]) -%} + {%- endif %} + {%- endif %} + {%- endfor %} +{%- endfor %} export class {{ service.name | caseUcfirst }} extends Service { @@ -29,96 +29,88 @@ export class {{ service.name | caseUcfirst }} extends Service { { super(client); } -{% for method in service.methods %} + {%~ for method in service.methods %} /** * {{ method.title }} * -{% if method.description %} -{{ method.description|comment2 }} -{% endif %} + {%~ if method.description %} + * {{ method.description }} + {%~ endif %} * -{% for parameter in method.parameters.all %} + {%~ for parameter in method.parameters.all %} * @param {{ '{' }}{{ parameter | getPropertyType(method) | raw }}{{ '}' }} {{ parameter.name | caseCamel | escapeKeyword }} -{% endfor %} - * @throws {{ '{' }}{{ spec.title | caseUcfirst}}Exception} + {%~ endfor %} + * @throws {{ '{' }}{{ spec.title | caseUcfirst }}Exception} * @returns {% if method.type == 'webAuth' %}{void|string}{% elseif method.type == 'location' %}{URL}{% else %}{Promise}{% endif %} */ - {% if method.type != 'location' and method.type != 'webAuth'%}async {% endif %}{{ method.name | caseCamel }}{{ method.responseModel | getGenerics(spec) | raw }}({% for parameter in method.parameters.all %}{{ parameter.name | caseCamel | escapeKeyword }}{% if not parameter.required or parameter.nullable %}?{% endif %}: {{ parameter | getPropertyType(method) | raw }}{% if not loop.last %}, {% endif %}{% endfor %}{% if 'multipart/form-data' in method.consumes %}, onProgress = (progress: UploadProgress) => {}{% endif %}): {{ method | getReturn(spec) | raw }} { -{% for parameter in method.parameters.all %} -{% if parameter.required %} + {% if method.type != 'location' and method.type != 'webAuth'%}async {% endif %}{{ method.name | caseCamel }}{{ method.responseModel | getGenerics(spec) | raw }}({%~ for parameter in method.parameters.all %}{{ parameter.name | caseCamel | escapeKeyword }}{%~ if not parameter.required or parameter.nullable %}?{%- endif %}: {{ parameter | getPropertyType(method) | raw }}{%~ if not loop.last %}, {%- endif %}{%- endfor %}{%~ if 'multipart/form-data' in method.consumes %}, onProgress = (progress: UploadProgress) => {}{%- endif %}): {{ method | getReturn(spec) | raw }} { + {%~ for parameter in method.parameters.all %} + {%~ if parameter.required %} if (typeof {{ parameter.name | caseCamel | escapeKeyword }} === 'undefined') { throw new {{spec.title | caseUcfirst}}Exception('Missing required parameter: "{{ parameter.name | caseCamel | escapeKeyword }}"'); } -{% endif %} -{% endfor %} - const apiPath = '{{ method.path }}'{% for parameter in method.parameters.path %}.replace('{{ '{' }}{{ parameter.name | caseCamel | escapeKeyword }}{{ '}' }}', {{ parameter.name | caseCamel | escapeKeyword }}){% endfor %}; + {%~ endif %} + {%~ endfor %} + const apiPath = '{{ method.path }}'{%~ for parameter in method.parameters.path %}.replace('{{ '{' }}{{ parameter.name | caseCamel | escapeKeyword }}{{ '}' }}', {{ parameter.name | caseCamel | escapeKeyword }}){%- endfor %}; const params: Params = {}; -{% for parameter in method.parameters.query %} + {%~ for parameter in method.parameters.query %} if (typeof {{ parameter.name | caseCamel | escapeKeyword }} !== 'undefined') { params['{{ parameter.name }}'] = {{ parameter.name | caseCamel | escapeKeyword }}; } -{% endfor %} -{% for parameter in method.parameters.body %} + {%~ endfor %} + {%~ for parameter in method.parameters.body %} if (typeof {{ parameter.name | caseCamel | escapeKeyword }} !== 'undefined') { params['{{ parameter.name }}'] = {{ parameter.name | caseCamel | escapeKeyword }}; } -{% endfor %} + {%~ endfor %} const uri = new URL(this.client.config.endpoint + apiPath); -{% if method.type == 'location' or method.type == 'webAuth' %} -{% if method.auth|length > 0 %} -{% for node in method.auth %} -{% for key,header in node|keys %} - params['{{header|caseLower}}'] = this.client.config.{{header|caseLower}}; -{% endfor %} -{% endfor %} -{% endif %} + {%~ if method.type == 'location' or method.type == 'webAuth' %} + {%~ if method.auth|length > 0 %} + {%~ for node in method.auth %} + {%~ for key,header in node|keys %} + params['{{header|caseLower}}'] = this.client.config.{{header|caseLower}}; + {%~ endfor %} + {%~ endfor %} + {%~ endif %} for (const [key, value] of Object.entries(Service.flatten(params))) { uri.searchParams.append(key, value); } -{% endif %} -{% if method.type == 'webAuth' %} + {%~ endif %} + {%~ if method.type == 'webAuth' or method.type == 'location' %} return uri; -{% elseif method.type == 'location' %} - return uri; -{% else %} -{% if 'multipart/form-data' in method.consumes %} -{% for parameter in method.parameters.all %} -{% if parameter.type == 'file' %} + {%~ else %} + const apiHeaders: { [header: string]: string } = { + {%~ for parameter in method.parameters.header %} + '{{ parameter.name | caseCamel | escapeKeyword }}': this.client.${{ parameter.name | caseCamel | escapeKeyword }}, + {%- endfor %} + {%~ for key, header in method.headers %} + '{{ key }}': '{{ header }}', + {%~ endfor %} + } + + {%~ if 'multipart/form-data' in method.consumes and method.type == 'upload' %} + {%~ for parameter in method.parameters.all %} + {%~ if parameter.type == 'file' %} const size = {{ parameter.name | caseCamel | escapeKeyword }}.size; if (size <= Service.CHUNK_SIZE) { - return await this.client.call('{{ method.method | caseLower }}', uri, { -{% for parameter in method.parameters.header %} - '{{ parameter.name | caseCamel | escapeKeyword }}': this.client.${{ parameter.name | caseCamel | escapeKeyword }}, -{% endfor %} -{% for key, header in method.headers %} - '{{ key }}': '{{ header }}', -{% endfor %} - }, params); - } - - const apiHeaders: { [header: string]: string } = { -{% for parameter in method.parameters.header %} - '{{ parameter.name | caseCamel | escapeKeyword }}': this.client.${{ parameter.name | caseCamel | escapeKeyword }}, -{% endfor %} -{% for key, header in method.headers %} - '{{ key }}': '{{ header }}', -{% endfor %} + params['{{ parameter.name }}'] = {{ parameter.name | caseCamel | escapeKeyword }}.toFileObject(); + return await this.client.call('{{ method.method | caseLower }}', uri, apiHeaders, params); } let offset = 0; let response = undefined; -{% for parameter in method.parameters.all %} -{% if parameter.isUploadID %} + {%~ for parameter in method.parameters.all %} + {%~ if parameter.isUploadID %} if({{ parameter.name | caseCamel | escapeKeyword }} != 'unique()') { try { response = await this.client.call('GET', new URL(this.client.config.endpoint + apiPath + '/' + {{ parameter.name }}), apiHeaders); @@ -126,8 +118,8 @@ export class {{ service.name | caseUcfirst }} extends Service { } catch(e) { } } -{% endif %} -{% endfor %} + {%~ endif %} + {%~ endfor %} let timestamp = new Date().getTime(); while (offset < size) { @@ -138,18 +130,20 @@ export class {{ service.name | caseUcfirst }} extends Service { apiHeaders['x-{{spec.title | caseLower }}-id'] = response.$id; } - let chunk = await FileSystem.readAsStringAsync({{ parameter.name | caseCamel | escapeKeyword }}.uri, { - encoding: FileSystem.EncodingType.Base64, - position: offset, - length: Service.CHUNK_SIZE - }); - var path = `data:${{'{'}}{{ parameter.name | caseCamel | escapeKeyword }}.type{{'}'}};base64,${{'{'}}chunk{{'}'}}`; + let chunkBuffer = {{ parameter.name | caseCamel | escapeKeyword }}.toBinary(offset, end - offset + 1); + let chunk = chunkBuffer.toString('base64'); + + var path = `data:${{ parameter.name | caseCamel | escapeKeyword }}.type};base64,${chunk}`; if (Platform.OS.toLowerCase() === 'android') { path = FileSystem.cacheDirectory + '/tmp_chunk_' + timestamp; await FileSystem.writeAsStringAsync(path, chunk, {encoding: FileSystem.EncodingType.Base64}); } - params['{{ parameter.name }}'] = {{ '{' }} uri: path, name: {{ parameter.name | caseCamel | escapeKeyword }}.name, type: {{ parameter.name | caseCamel | escapeKeyword }}.type {{ '}' }}; + params['{{ parameter.name }}'] = { + uri: path, + name: {{ parameter.name | caseCamel | escapeKeyword }}.filename, + type: {{ parameter.name | caseCamel | escapeKeyword }}.type + }; response = await this.client.call('{{ method.method | caseLower }}', uri, apiHeaders, params); @@ -165,19 +159,12 @@ export class {{ service.name | caseUcfirst }} extends Service { offset += Service.CHUNK_SIZE; } return response; -{% endif %} -{% endfor %} -{% else %} - return await this.client.call('{{ method.method | caseLower }}', uri, { -{% for parameter in method.parameters.header %} - '{{ parameter.name | caseCamel | escapeKeyword }}': this.client.${{ parameter.name | caseCamel | escapeKeyword }}, -{% endfor %} -{% for key, header in method.headers %} - '{{ key }}': '{{ header }}', -{% endfor %} - }, params); -{% endif %} -{% endif %} + {%~ endif %} + {%~ endfor %} + {%~ else %} + return await this.client.call('{{ method.method | caseLower }}', uri, apiHeaders, params); + {%~ endif %} + {%~ endif %} } -{% endfor %} + {%~ endfor %} }; From 643a26ad18ae52421d266779cd0b2021d6f65a57 Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Tue, 17 Sep 2024 19:13:10 +0100 Subject: [PATCH 04/11] feat: multipart parsing --- templates/node/src/client.ts.twig | 6 ++-- templates/react-native/src/client.ts.twig | 39 +++++++++++++++++++++-- 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/templates/node/src/client.ts.twig b/templates/node/src/client.ts.twig index 98fc49e4d..afbd70aa1 100644 --- a/templates/node/src/client.ts.twig +++ b/templates/node/src/client.ts.twig @@ -1,8 +1,8 @@ import { fetch, FormData, Blob } from 'node-fetch-native-with-agent'; +import { getBoundary, parse as parseMultipart} from 'parse-multipart-data'; import { createAgent } from 'node-fetch-native-with-agent/agent'; import { Models } from './models'; import { Payload } from './payload'; -import * as multipart from 'parse-multipart-data'; type Params = { [key: string]: any; @@ -290,10 +290,10 @@ class Client { chunks.push(chunk instanceof Buffer ? chunk : Buffer.from(chunk)); } const body = Buffer.concat(chunks); - const boundary = multipart.getBoundary( + const boundary = getBoundary( response.headers.get("content-type") || "" ); - const parts = multipart.parse(body, boundary); + const parts = parseMultipart(body, boundary); const partsObject: { [key: string]: any } = {}; for (const part of parts) { diff --git a/templates/react-native/src/client.ts.twig b/templates/react-native/src/client.ts.twig index 28dac59d9..afa90f8e4 100644 --- a/templates/react-native/src/client.ts.twig +++ b/templates/react-native/src/client.ts.twig @@ -1,7 +1,9 @@ -import { Models } from './models'; +import { Platform } from 'react-native'; +import { getBoundary, parse as parseMultipart} from 'parse-multipart-data'; import { Service } from './service'; import { Payload } from './payload'; -import { Platform } from 'react-native'; +import { Models } from './models'; + type Params = { [key: string]: any; @@ -398,6 +400,39 @@ class Client { if (response.headers.get('content-type')?.includes('application/json')) { data = await response.json(); + } else if (response.headers.get('content-type')?.includes('multipart/form-data')) { + const chunks = []; + for await (const chunk of (response.body as AsyncIterable)) { + chunks.push(chunk instanceof Buffer ? chunk : Buffer.from(chunk)); + } + const body = Buffer.concat(chunks); + const boundary = getBoundary( + response.headers.get("content-type") || "" + ); + const parts = parseMultipart(body, boundary); + const partsObject: { [key: string]: any } = {}; + + for (const part of parts) { + if (!part.name) { + continue; + } + if (part.name === "responseBody") { + partsObject[part.name] = Payload.fromBinary(part.data, part.filename); + } else if (part.name === "responseStatusCode") { + partsObject[part.name] = parseInt(part.data.toString()); + } else if (part.name === "duration") { + partsObject[part.name] = parseFloat(part.data.toString()); + } else if (part.type === 'application/json') { + try { + partsObject[part.name] = JSON.parse(part.data.toString()); + } catch (e) { + throw new Error(`Error parsing JSON for part ${part.name}: ${e instanceof Error ? e.message : 'Unknown error'}`); + } + } else { + partsObject[part.name] = part.data.toString(); + } + } + data = partsObject; } else { data = { message: await response.text() From 854d74b5b5e6366c357f5a43534a15419aa35168 Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Tue, 17 Sep 2024 19:15:27 +0100 Subject: [PATCH 05/11] feat: missing dep --- templates/react-native/package.json.twig | 1 + 1 file changed, 1 insertion(+) diff --git a/templates/react-native/package.json.twig b/templates/react-native/package.json.twig index 0c81f8ce9..1a088479f 100644 --- a/templates/react-native/package.json.twig +++ b/templates/react-native/package.json.twig @@ -33,6 +33,7 @@ }, "dependencies": { "expo-file-system": "16.0.8", + "parse-multipart-data": "^1.5.0", "react-native": "^0.73.6" }, "peerDependencies": { From bbb95059a47cd58ff79b2e1db27711682eb62cab Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Tue, 17 Sep 2024 20:09:21 +0100 Subject: [PATCH 06/11] fix: parsing --- templates/deno/src/client.ts.twig | 5 ++--- templates/node/src/client.ts.twig | 8 ++------ templates/react-native/src/client.ts.twig | 8 ++------ 3 files changed, 6 insertions(+), 15 deletions(-) diff --git a/templates/deno/src/client.ts.twig b/templates/deno/src/client.ts.twig index e2ef628cc..4b4990216 100644 --- a/templates/deno/src/client.ts.twig +++ b/templates/deno/src/client.ts.twig @@ -128,12 +128,11 @@ export class Client { } if (response.headers.get('content-type')?.includes('multipart/form-data')) { + const body = await response.arrayBuffer(); const boundary = multipart.getBoundary( response.headers.get("content-type") || "" ); - - const body = new Uint8Array(await response.arrayBuffer()); - const parts = multipart.parse(body, boundary); + const parts = multipart.parse(Buffer.from(body), boundary); const partsObject: { [key: string]: any } = {}; for (const part of parts) { diff --git a/templates/node/src/client.ts.twig b/templates/node/src/client.ts.twig index afbd70aa1..81fd85c19 100644 --- a/templates/node/src/client.ts.twig +++ b/templates/node/src/client.ts.twig @@ -285,15 +285,11 @@ class Client { } else if (responseType === 'arrayBuffer') { data = await response.arrayBuffer(); } else if (response.headers.get('content-type')?.includes('multipart/form-data')) { - const chunks = []; - for await (const chunk of (response.body as AsyncIterable)) { - chunks.push(chunk instanceof Buffer ? chunk : Buffer.from(chunk)); - } - const body = Buffer.concat(chunks); + const body = await response.arrayBuffer(); const boundary = getBoundary( response.headers.get("content-type") || "" ); - const parts = parseMultipart(body, boundary); + const parts = parseMultipart(Buffer.from(body), boundary); const partsObject: { [key: string]: any } = {}; for (const part of parts) { diff --git a/templates/react-native/src/client.ts.twig b/templates/react-native/src/client.ts.twig index afa90f8e4..af8171bd0 100644 --- a/templates/react-native/src/client.ts.twig +++ b/templates/react-native/src/client.ts.twig @@ -401,15 +401,11 @@ class Client { if (response.headers.get('content-type')?.includes('application/json')) { data = await response.json(); } else if (response.headers.get('content-type')?.includes('multipart/form-data')) { - const chunks = []; - for await (const chunk of (response.body as AsyncIterable)) { - chunks.push(chunk instanceof Buffer ? chunk : Buffer.from(chunk)); - } - const body = Buffer.concat(chunks); + const body = await response.arrayBuffer(); const boundary = getBoundary( response.headers.get("content-type") || "" ); - const parts = parseMultipart(body, boundary); + const parts = parseMultipart(Buffer.from(body), boundary); const partsObject: { [key: string]: any } = {}; for (const part of parts) { From e6674a918bba252619294650b8ea8238d72aaf47 Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Tue, 17 Sep 2024 20:33:53 +0100 Subject: [PATCH 07/11] feat: missing import --- templates/react-native/src/client.ts.twig | 1 + 1 file changed, 1 insertion(+) diff --git a/templates/react-native/src/client.ts.twig b/templates/react-native/src/client.ts.twig index af8171bd0..656c9eca9 100644 --- a/templates/react-native/src/client.ts.twig +++ b/templates/react-native/src/client.ts.twig @@ -1,5 +1,6 @@ import { Platform } from 'react-native'; import { getBoundary, parse as parseMultipart} from 'parse-multipart-data'; +import { Buffer } from 'buffer'; import { Service } from './service'; import { Payload } from './payload'; import { Models } from './models'; From ea631d2847133bf6d9b9e8502c1c6c47fd3f939b Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Tue, 17 Sep 2024 22:22:13 +0100 Subject: [PATCH 08/11] feat: fixes --- src/SDK/Language/ReactNative.php | 5 + templates/deno/src/client.ts.twig | 9 +- templates/react-native/src/client.ts.twig | 8 +- templates/react-native/src/multipart.ts.twig | 214 ++++++++++++++++++ templates/react-native/src/payload.ts.twig | 76 ++++--- .../src/services/template.ts.twig | 2 +- templates/react-native/tsconfig.json.twig | 40 ++-- tests/languages/kotlin/Tests.kt | 2 +- tests/resources/spec.json | 2 +- 9 files changed, 297 insertions(+), 61 deletions(-) create mode 100644 templates/react-native/src/multipart.ts.twig diff --git a/src/SDK/Language/ReactNative.php b/src/SDK/Language/ReactNative.php index 289309c91..3c5fd4019 100644 --- a/src/SDK/Language/ReactNative.php +++ b/src/SDK/Language/ReactNative.php @@ -68,6 +68,11 @@ public function getFiles(): array 'destination' => 'src/payload.ts', 'template' => 'react-native/src/payload.ts.twig', ], + [ + 'scope' => 'default', + 'destination' => 'src/multipart.ts', + 'template' => 'react-native/src/multipart.ts.twig', + ], [ 'scope' => 'default', 'destination' => 'README.md', diff --git a/templates/deno/src/client.ts.twig b/templates/deno/src/client.ts.twig index 4b4990216..4993a36b4 100644 --- a/templates/deno/src/client.ts.twig +++ b/templates/deno/src/client.ts.twig @@ -1,6 +1,6 @@ import { {{ spec.title | caseUcfirst}}Exception } from './exception.ts'; import { Payload } from './payload.ts'; -import * as multipart from './multipart.ts'; +import { getBoundary, parse as parseMultipart } from './multipart.ts'; export interface Params { [key: string]: any; @@ -128,11 +128,12 @@ export class Client { } if (response.headers.get('content-type')?.includes('multipart/form-data')) { - const body = await response.arrayBuffer(); - const boundary = multipart.getBoundary( + const boundary = getBoundary( response.headers.get("content-type") || "" ); - const parts = multipart.parse(Buffer.from(body), boundary); + + const body = new Uint8Array(await response.arrayBuffer()); + const parts = parseMultipart(body, boundary); const partsObject: { [key: string]: any } = {}; for (const part of parts) { diff --git a/templates/react-native/src/client.ts.twig b/templates/react-native/src/client.ts.twig index 656c9eca9..cae8a892a 100644 --- a/templates/react-native/src/client.ts.twig +++ b/templates/react-native/src/client.ts.twig @@ -1,6 +1,5 @@ import { Platform } from 'react-native'; -import { getBoundary, parse as parseMultipart} from 'parse-multipart-data'; -import { Buffer } from 'buffer'; +import { getBoundary, parse as parseMultipart} from './multipart'; import { Service } from './service'; import { Payload } from './payload'; import { Models } from './models'; @@ -402,11 +401,12 @@ class Client { if (response.headers.get('content-type')?.includes('application/json')) { data = await response.json(); } else if (response.headers.get('content-type')?.includes('multipart/form-data')) { - const body = await response.arrayBuffer(); const boundary = getBoundary( response.headers.get("content-type") || "" ); - const parts = parseMultipart(Buffer.from(body), boundary); + + const body = new Uint8Array(await response.arrayBuffer()); + const parts = parseMultipart(body, boundary); const partsObject: { [key: string]: any } = {}; for (const part of parts) { diff --git a/templates/react-native/src/multipart.ts.twig b/templates/react-native/src/multipart.ts.twig new file mode 100644 index 000000000..21ec6e319 --- /dev/null +++ b/templates/react-native/src/multipart.ts.twig @@ -0,0 +1,214 @@ +/** + * Port of: https://github.com/nachomazzara/parse-multipart-data/blob/master/src/multipart.ts + * Includes few changes for Deno compatibility. Textdiff should show the changes. + * Copied from master with commit 56052e860bc4e3fa7fe4763f69e88ec79b295a3c + * + * + * Multipart Parser (Finite State Machine) + * usage: + * const multipart = require('./multipart.js'); + * const body = multipart.DemoData(); // raw body + * const body = Buffer.from(event['body-json'].toString(),'base64'); // AWS case + * const boundary = multipart.getBoundary(event.params.header['content-type']); + * const parts = multipart.Parse(body,boundary); + * each part is: + * { filename: 'A.txt', type: 'text/plain', data: } + * or { name: 'key', data: } + */ + +type Part = { + contentDispositionHeader: string; + contentTypeHeader: string; + part: number[]; +}; + +type Input = { + filename?: string; + name?: string; + type: string; + data: Uint8Array; +}; + +enum ParsingState { + INIT, + READING_HEADERS, + READING_DATA, + READING_PART_SEPARATOR, +} + +export function parse( + multipartBodyBuffer: Uint8Array, + boundary: string +): Input[] { + let lastline = ""; + let contentDispositionHeader = ""; + let contentTypeHeader = ""; + let state: ParsingState = ParsingState.INIT; + let buffer: number[] = []; + const allParts: Input[] = []; + + let currentPartHeaders: string[] = []; + + for (let i = 0; i < multipartBodyBuffer.length; i++) { + const oneByte: number = multipartBodyBuffer[i]; + const prevByte: number | null = i > 0 ? multipartBodyBuffer[i - 1] : null; + // 0x0a => \n + // 0x0d => \r + const newLineDetected: boolean = oneByte === 0x0a && prevByte === 0x0d; + const newLineChar: boolean = oneByte === 0x0a || oneByte === 0x0d; + + if (!newLineChar) lastline += String.fromCharCode(oneByte); + if (ParsingState.INIT === state && newLineDetected) { + // searching for boundary + if ("--" + boundary === lastline) { + state = ParsingState.READING_HEADERS; // found boundary. start reading headers + } + lastline = ""; + } else if (ParsingState.READING_HEADERS === state && newLineDetected) { + // parsing headers. Headers are separated by an empty line from the content. Stop reading headers when the line is empty + if (lastline.length) { + currentPartHeaders.push(lastline); + } else { + // found empty line. search for the headers we want and set the values + for (const h of currentPartHeaders) { + if (h.toLowerCase().startsWith("content-disposition:")) { + contentDispositionHeader = h; + } else if (h.toLowerCase().startsWith("content-type:")) { + contentTypeHeader = h; + } + } + state = ParsingState.READING_DATA; + buffer = []; + } + lastline = ""; + } else if (ParsingState.READING_DATA === state) { + // parsing data + if (lastline.length > boundary.length + 4) { + lastline = ""; // mem save + } + if ("--" + boundary === lastline) { + const j = buffer.length - lastline.length; + const part = buffer.slice(0, j - 1); + + allParts.push( + process({ contentDispositionHeader, contentTypeHeader, part }) + ); + buffer = []; + currentPartHeaders = []; + lastline = ""; + state = ParsingState.READING_PART_SEPARATOR; + contentDispositionHeader = ""; + contentTypeHeader = ""; + } else { + buffer.push(oneByte); + } + if (newLineDetected) { + lastline = ""; + } + } else if (ParsingState.READING_PART_SEPARATOR === state) { + if (newLineDetected) { + state = ParsingState.READING_HEADERS; + } + } + } + return allParts; +} + +// read the boundary from the content-type header sent by the http client +// this value may be similar to: +// 'multipart/form-data; boundary=----WebKitFormBoundaryvm5A9tzU1ONaGP5B', +export function getBoundary(header: string): string { + const items = header.split(";"); + if (items) { + for (let i = 0; i < items.length; i++) { + const item = new String(items[i]).trim(); + if (item.indexOf("boundary") >= 0) { + const k = item.split("="); + return new String(k[1]).trim().replace(/^["']|["']$/g, ""); + } + } + } + return ""; +} + +export function DemoData(): { body: Uint8Array; boundary: string } { + let body = "trash1\r\n"; + body += "------WebKitFormBoundaryvef1fLxmoUdYZWXp\r\n"; + body += "Content-Type: text/plain\r\n"; + body += + 'Content-Disposition: form-data; name="uploads[]"; filename="A.txt"\r\n'; + body += "\r\n"; + body += "@11X"; + body += "111Y\r\n"; + body += "111Z\rCCCC\nCCCC\r\nCCCCC@\r\n\r\n"; + body += "------WebKitFormBoundaryvef1fLxmoUdYZWXp\r\n"; + body += "Content-Type: text/plain\r\n"; + body += + 'Content-Disposition: form-data; name="uploads[]"; filename="B.txt"\r\n'; + body += "\r\n"; + body += "@22X"; + body += "222Y\r\n"; + body += "222Z\r222W\n2220\r\n666@\r\n"; + body += "------WebKitFormBoundaryvef1fLxmoUdYZWXp\r\n"; + body += 'Content-Disposition: form-data; name="input1"\r\n'; + body += "\r\n"; + body += "value1\r\n"; + body += "------WebKitFormBoundaryvef1fLxmoUdYZWXp--\r\n"; + + return { + body: new TextEncoder().encode(body), + boundary: "----WebKitFormBoundaryvef1fLxmoUdYZWXp", + }; +} + +function process(part: Part): Input { + // will transform this object: + // { header: 'Content-Disposition: form-data; name="uploads[]"; filename="A.txt"', + // info: 'Content-Type: text/plain', + // part: 'AAAABBBB' } + // into this one: + // { filename: 'A.txt', type: 'text/plain', data: } + const obj = function (str: string) { + const k = str.split("="); + const a = k[0].trim(); + + const b = JSON.parse(k[1].trim()); + const o = {}; + Object.defineProperty(o, a, { + value: b, + writable: true, + enumerable: true, + configurable: true, + }); + return o; + }; + const header = part.contentDispositionHeader.split(";"); + + const filenameData = header[2]; + let input = {}; + if (filenameData) { + input = obj(filenameData); + const contentType = part.contentTypeHeader.split(":")[1].trim(); + Object.defineProperty(input, "type", { + value: contentType, + writable: true, + enumerable: true, + configurable: true, + }); + } + // always process the name field + Object.defineProperty(input, "name", { + value: header[1].split("=")[1].replace(/"/g, ""), + writable: true, + enumerable: true, + configurable: true, + }); + + Object.defineProperty(input, "data", { + value: new Uint8Array(part.part), + writable: true, + enumerable: true, + configurable: true, + }); + return input as Input; +} diff --git a/templates/react-native/src/payload.ts.twig b/templates/react-native/src/payload.ts.twig index f17b265ad..facd1d3b4 100644 --- a/templates/react-native/src/payload.ts.twig +++ b/templates/react-native/src/payload.ts.twig @@ -5,33 +5,44 @@ interface ReactNativeFileObject { } export class Payload { - private data: Buffer; - public filename?: string; + public uri: string; public size: number; + public filename?: string; public type?: string; - constructor(data: Buffer, filename?: string, type?: string) { - this.data = data; + constructor(uri: string, filename?: string, type?: string, size?: number) { + this.uri = uri; this.filename = filename; - this.size = data.byteLength; this.type = type; + + if (size === undefined) { + const base64Data = uri.split(',')[1]; + const binary = atob(base64Data); + this.size = binary.length; + } else { + this.size = size; + } } - public toBinary(offset: number = 0, length?: number): Buffer { + public toBinary(offset: number = 0, length?: number): Uint8Array { + const base64Data = this.uri.split(',')[1]; + const binary = atob(base64Data); + const bytes = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i++) { + bytes[i] = binary.charCodeAt(i); + } if (offset === 0 && length === undefined) { - return this.data; + return bytes; } else if (length === undefined) { - return this.data.subarray(offset); + return bytes.subarray(offset); } else { - return this.data.subarray(offset, offset + length); + return bytes.subarray(offset, offset + length); } } public toFileObject(): ReactNativeFileObject { - const base64Data = this.data.toString("base64"); - const uri = `data:${this.type};base64,${base64Data}`; return { - uri: uri, + uri: this.uri, type: this.type, name: this.filename, }; @@ -42,37 +53,30 @@ export class Payload { } public toString(): string { - return this.data.toString("utf-8"); + const binary = this.toBinary(); + return new TextDecoder().decode(binary); } - public static fromBinary( - bytes: Buffer, - name?: string, - type?: string - ): Payload { - return new Payload(bytes, name, type); + public static fromJson(object: any, name?: string): Payload { + const jsonString = JSON.stringify(object); + const base64Data = btoa(jsonString); + const dataUri = `data:application/json;base64,${base64Data}`; + return new Payload(dataUri, name, 'application/json'); } - public static fromJson(object: any, name?: string): Payload { - const data = Buffer.from(JSON.stringify(object), "utf-8"); - return new Payload(data, name, "application/json"); + public static fromString(text: string, name?: string, type?: string): Payload { + const base64Data = btoa(text); + const dataUri = `data:${type || 'text/plain'};base64,${base64Data}`; + return new Payload(dataUri, name, type || 'text/plain'); } - public static fromString( - text: string, - name?: string, - type?: string - ): Payload { - const data = Buffer.from(text, "utf-8"); - return new Payload(data, name, type || "text/plain"); + public static fromBinary(binary: Uint8Array, name?: string, type?: string): Payload { + const base64Data = btoa(String.fromCharCode(...binary)); + const dataUri = `data:${type || 'application/octet-stream'};base64,${base64Data}`; + return new Payload(dataUri, name, type || 'application/octet-stream'); } - public static async fromFileObject( - file: ReactNativeFileObject - ): Promise { - const response = await fetch(file.uri); - const arrayBuffer = await response.arrayBuffer(); - const data = Buffer.from(arrayBuffer); - return new Payload(data, file.name, file.type); + public static fromFileObject(file: ReactNativeFileObject): Payload { + return new Payload(file.uri, file.name, file.type); } } diff --git a/templates/react-native/src/services/template.ts.twig b/templates/react-native/src/services/template.ts.twig index a51bac97d..6ede42bfe 100644 --- a/templates/react-native/src/services/template.ts.twig +++ b/templates/react-native/src/services/template.ts.twig @@ -131,7 +131,7 @@ export class {{ service.name | caseUcfirst }} extends Service { } let chunkBuffer = {{ parameter.name | caseCamel | escapeKeyword }}.toBinary(offset, end - offset + 1); - let chunk = chunkBuffer.toString('base64'); + let chunk = btoa(String.fromCharCode(...chunkBuffer)); var path = `data:${{ parameter.name | caseCamel | escapeKeyword }}.type};base64,${chunk}`; if (Platform.OS.toLowerCase() === 'android') { diff --git a/templates/react-native/tsconfig.json.twig b/templates/react-native/tsconfig.json.twig index 8a27d1f04..34d5613f7 100644 --- a/templates/react-native/tsconfig.json.twig +++ b/templates/react-native/tsconfig.json.twig @@ -1,22 +1,34 @@ { "compilerOptions": { + "target": "esnext", + "module": "commonjs", + "types": ["react-native"], + "lib": [ + "dom", + "es2019", + "es2020.bigint", + "es2020.date", + "es2020.number", + "es2020.promise", + "es2020.string", + "es2020.symbol.wellknown", + "es2021.promise", + "es2021.string", + "es2021.weakref", + "es2022.array", + "es2022.object", + "es2022.string" + ], "allowJs": true, + "jsx": "react-native", + "noEmit": true, + "isolatedModules": true, + "strict": true, + "moduleResolution": "node", + "resolveJsonModule": true, "allowSyntheticDefaultImports": true, - "baseUrl": "src", - "declaration": false, "esModuleInterop": true, - "inlineSourceMap": false, - "lib": ["ESNext", "DOM"], - "listEmittedFiles": false, - "listFiles": false, - "moduleResolution": "node", - "noFallthroughCasesInSwitch": true, - "pretty": true, - "rootDir": "src", - "skipLibCheck": true, - "strict": true, - "target": "ES6", - "traceResolution": false, + "skipLibCheck": true }, "compileOnSave": false, "exclude": ["node_modules", "dist"], diff --git a/tests/languages/kotlin/Tests.kt b/tests/languages/kotlin/Tests.kt index 25022b0fe..52c9ef318 100644 --- a/tests/languages/kotlin/Tests.kt +++ b/tests/languages/kotlin/Tests.kt @@ -136,7 +136,7 @@ class ServiceTest { writeToFile(url) // Multipart tests - val mp = general.multipartComplied() + val mp = general.multipartCompiled() writeToFile((mp as Map)["x"] as String) writeToFile(md5(((mp as Map)["responseBody"] as Payload).toBinary())) diff --git a/tests/resources/spec.json b/tests/resources/spec.json index b0894a3d4..419aa9df4 100644 --- a/tests/resources/spec.json +++ b/tests/resources/spec.json @@ -1634,7 +1634,7 @@ } }, "x-appwrite": { - "method": "multipartComplied", + "method": "multipartCompiled", "weight": 278, "cookies": false, "type": "", From 9fe63be8b19cbbe4857efd793e1aae936c063e26 Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Wed, 18 Sep 2024 10:19:09 +0100 Subject: [PATCH 09/11] fix: build --- .github/workflows/tests.yml | 1 - templates/react-native/tsconfig.json.twig | 3 +- tests/ReactNativeStableTest.php | 35 ------ tests/languages/react-native/node.js | 147 ---------------------- 4 files changed, 1 insertion(+), 185 deletions(-) delete mode 100644 tests/ReactNativeStableTest.php delete mode 100644 tests/languages/react-native/node.js diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index eb2d9ada7..06f10a51a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -38,7 +38,6 @@ jobs: Python38, Python39, Python310, - ReactNativeStable, Ruby27, Ruby30, Ruby31, diff --git a/templates/react-native/tsconfig.json.twig b/templates/react-native/tsconfig.json.twig index 34d5613f7..d469a2fda 100644 --- a/templates/react-native/tsconfig.json.twig +++ b/templates/react-native/tsconfig.json.twig @@ -1,7 +1,7 @@ { "compilerOptions": { "target": "esnext", - "module": "commonjs", + "module": "esnext", "types": ["react-native"], "lib": [ "dom", @@ -21,7 +21,6 @@ ], "allowJs": true, "jsx": "react-native", - "noEmit": true, "isolatedModules": true, "strict": true, "moduleResolution": "node", diff --git a/tests/ReactNativeStableTest.php b/tests/ReactNativeStableTest.php deleted file mode 100644 index 215afadf1..000000000 --- a/tests/ReactNativeStableTest.php +++ /dev/null @@ -1,35 +0,0 @@ - Date: Wed, 18 Sep 2024 10:22:09 +0100 Subject: [PATCH 10/11] fix: dev --- mock-server/docker-compose.yml | 2 -- templates/deno/src/service.ts.twig | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/mock-server/docker-compose.yml b/mock-server/docker-compose.yml index 2440e86a9..36b711ae7 100644 --- a/mock-server/docker-compose.yml +++ b/mock-server/docker-compose.yml @@ -1,8 +1,6 @@ services: mockapi: container_name: mockapi - ports: - - 8080:80 build: context: . args: diff --git a/templates/deno/src/service.ts.twig b/templates/deno/src/service.ts.twig index dbdd758d4..300fa827a 100644 --- a/templates/deno/src/service.ts.twig +++ b/templates/deno/src/service.ts.twig @@ -1,4 +1,4 @@ - import { Client } from "./client.ts"; +import { Client } from "./client.ts"; export abstract class Service { client: Client; From 025ea7c29e7667a58e5200d7e72c5869121e7e66 Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Tue, 1 Oct 2024 14:24:40 +0100 Subject: [PATCH 11/11] fix: multipart compiled --- mock-server/app/http.php | 2 +- tests/resources/spec.json | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/mock-server/app/http.php b/mock-server/app/http.php index 49a94388c..0b779d23c 100644 --- a/mock-server/app/http.php +++ b/mock-server/app/http.php @@ -387,7 +387,7 @@ }); App::get('/v1/mock/tests/general/multipart') - ->alias('/v1/mock/tests/general/multipartcomplied') + ->alias('/v1/mock/tests/general/multipart-compiled') ->desc('Multipart') ->groups(['mock']) ->label('scope', 'public') diff --git a/tests/resources/spec.json b/tests/resources/spec.json index 419aa9df4..61bee846c 100644 --- a/tests/resources/spec.json +++ b/tests/resources/spec.json @@ -1614,10 +1614,10 @@ ] } }, - "\/mock\/tests\/general\/multipartcomplied": { + "\/mock\/tests\/general\/multipart-compiled": { "get": { - "summary": "MultipartComplied", - "operationId": "generalMultipartComplied", + "summary": "MultipartCompiled", + "operationId": "generalMultipartCompiled", "consumes": [ "application\/json" ],