diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 9d80b8609..e487448c1 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, diff --git a/src/SDK/Language/ReactNative.php b/src/SDK/Language/ReactNative.php index d0b4cb8f6..3c5fd4019 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,16 @@ 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' => 'src/multipart.ts', + 'template' => 'react-native/src/multipart.ts.twig', + ], [ 'scope' => 'default', 'destination' => 'README.md', @@ -145,8 +153,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 +188,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 +207,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/client.ts.twig b/templates/deno/src/client.ts.twig index e2ef628cc..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,12 +128,12 @@ export class Client { } if (response.headers.get('content-type')?.includes('multipart/form-data')) { - const boundary = multipart.getBoundary( + const boundary = getBoundary( response.headers.get("content-type") || "" ); const body = new Uint8Array(await response.arrayBuffer()); - 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/node/src/client.ts.twig b/templates/node/src/client.ts.twig index 98fc49e4d..81fd85c19 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; @@ -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 boundary = multipart.getBoundary( + const body = await response.arrayBuffer(); + const boundary = getBoundary( response.headers.get("content-type") || "" ); - const parts = multipart.parse(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/package.json.twig b/templates/react-native/package.json.twig index 685663876..1a088479f 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", @@ -34,6 +33,7 @@ }, "dependencies": { "expo-file-system": "16.0.8", + "parse-multipart-data": "^1.5.0", "react-native": "^0.73.6" }, "peerDependencies": { diff --git a/templates/react-native/src/client.ts.twig b/templates/react-native/src/client.ts.twig index 3e0bd8eb7..cae8a892a 100644 --- a/templates/react-native/src/client.ts.twig +++ b/templates/react-native/src/client.ts.twig @@ -1,8 +1,11 @@ -import { Models } from './models'; -import { Service } from './service'; import { Platform } from 'react-native'; +import { getBoundary, parse as parseMultipart} from './multipart'; +import { Service } from './service'; +import { Payload } from './payload'; +import { Models } from './models'; + -type Payload = { +type Params = { [key: string]: any; } @@ -345,7 +348,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); @@ -397,6 +400,36 @@ 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 boundary = getBoundary( + response.headers.get("content-type") || "" + ); + + const body = new Uint8Array(await response.arrayBuffer()); + 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() @@ -425,4 +458,4 @@ class Client { } export { Client, {{spec.title | caseUcfirst}}Exception }; -export type { Models, Payload }; +export type { Models, 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/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 new file mode 100644 index 000000000..facd1d3b4 --- /dev/null +++ b/templates/react-native/src/payload.ts.twig @@ -0,0 +1,82 @@ +interface ReactNativeFileObject { + uri: string; + type?: string; + name?: string; +} + +export class Payload { + public uri: string; + public size: number; + public filename?: string; + public type?: string; + + constructor(uri: string, filename?: string, type?: string, size?: number) { + this.uri = uri; + this.filename = filename; + 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): 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 bytes; + } else if (length === undefined) { + return bytes.subarray(offset); + } else { + return bytes.subarray(offset, offset + length); + } + } + + public toFileObject(): ReactNativeFileObject { + return { + uri: this.uri, + type: this.type, + name: this.filename, + }; + } + + public toJson(): T { + return JSON.parse(this.toString()); + } + + public toString(): string { + const binary = this.toBinary(); + return new TextDecoder().decode(binary); + } + + 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 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 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 fromFileObject(file: ReactNativeFileObject): Payload { + return new Payload(file.uri, file.name, file.type); + } +} 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..6ede42bfe 100644 --- a/templates/react-native/src/services/template.ts.twig +++ b/templates/react-native/src/services/template.ts.twig @@ -1,26 +1,27 @@ 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, Payload } from '../client'; +import type { UploadProgress, Params } from '../client'; 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 { @@ -28,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 %}; - const payload: Payload = {}; + {%~ 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') { - payload['{{ parameter.name }}'] = {{ parameter.name | caseCamel | escapeKeyword }}; + 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') { - payload['{{ parameter.name }}'] = {{ parameter.name | caseCamel | escapeKeyword }}; + 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 %} - payload['{{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(payload))) { + 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 %} - }, payload); - } - - 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); @@ -125,8 +118,8 @@ export class {{ service.name | caseUcfirst }} extends Service { } catch(e) { } } -{% endif %} -{% endfor %} + {%~ endif %} + {%~ endfor %} let timestamp = new Date().getTime(); while (offset < size) { @@ -137,20 +130,22 @@ 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 = btoa(String.fromCharCode(...chunkBuffer)); + + 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}); } - 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 }}.filename, + 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({ @@ -164,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 %} - }, payload); -{% endif %} -{% endif %} + {%~ endif %} + {%~ endfor %} + {%~ else %} + return await this.client.call('{{ method.method | caseLower }}', uri, apiHeaders, params); + {%~ endif %} + {%~ endif %} } -{% endfor %} + {%~ endfor %} }; diff --git a/templates/react-native/tsconfig.json.twig b/templates/react-native/tsconfig.json.twig index 8a27d1f04..d469a2fda 100644 --- a/templates/react-native/tsconfig.json.twig +++ b/templates/react-native/tsconfig.json.twig @@ -1,22 +1,33 @@ { "compilerOptions": { + "target": "esnext", + "module": "esnext", + "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", + "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/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 7ef2affc9..a4f8bf425 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/WebChromiumTest.php b/tests/WebChromiumTest.php index 63248195f..56e5a16e4 100644 --- a/tests/WebChromiumTest.php +++ b/tests/WebChromiumTest.php @@ -12,10 +12,9 @@ class WebChromiumTest extends Base protected string $language = 'web'; protected string $class = 'Appwrite\SDK\Language\Web'; protected array $build = [ - 'cp tests/languages/web/tests.js tests/sdks/web/tests.js', - 'cp tests/languages/web/node.js tests/sdks/web/node.js', - 'cp tests/languages/web/index.html tests/sdks/web/index.html', - 'docker run --rm -v $(pwd):/app -w /app/tests/sdks/web mcr.microsoft.com/playwright:v1.46.0-jammy sh -c "npm install && npm run build"', + 'cp -R tests/languages/web/* tests/sdks/web/', + 'docker run --rm -v $(pwd):/app -w /app/tests/sdks/web mcr.microsoft.com/playwright:v1.46.0-jammy npm install playwright@1.46.0', + 'docker run --rm -v $(pwd):/app -w /app/tests/sdks/web mcr.microsoft.com/playwright:v1.46.0-jammy npm run build', ]; protected string $command = 'docker run --network="mockapi" --rm -v $(pwd):/app -e BROWSER=chromium -w /app/tests/sdks/web mcr.microsoft.com/playwright:v1.46.0-jammy node tests.js'; diff --git a/tests/WebNodeTest.php b/tests/WebNodeTest.php index 8720ae8ae..9852b6150 100644 --- a/tests/WebNodeTest.php +++ b/tests/WebNodeTest.php @@ -12,11 +12,9 @@ class WebNodeTest extends Base protected string $language = 'web'; protected string $class = 'Appwrite\SDK\Language\Web'; protected array $build = [ - 'cp tests/languages/web/tests.js tests/sdks/web/tests.js', - 'cp tests/languages/web/node.js tests/sdks/web/node.js', - 'cp tests/languages/web/index.html tests/sdks/web/index.html', - 'docker run --rm -v $(pwd):/app -w /app/tests/sdks/web mcr.microsoft.com/playwright:v1.46.0-jammy npm install', // npm list --depth 0 && - 'docker run --rm -v $(pwd):/app -w /app/tests/sdks/web mcr.microsoft.com/playwright:v1.46.0-jammy npm run build', + 'cp -R tests/languages/web/* tests/sdks/web/', + 'docker run --rm -v $(pwd):/app -w /app/tests/sdks/web node:18-alpine npm install', + 'docker run --rm -v $(pwd):/app -w /app/tests/sdks/web node:18-alpine npm run build', ]; protected string $command = 'docker run --network="mockapi" --rm -v $(pwd):/app -w /app/tests/sdks/web node:18-alpine node node.js'; diff --git a/tests/resources/spec.json b/tests/resources/spec.json index bac025c03..fe343204f 100644 --- a/tests/resources/spec.json +++ b/tests/resources/spec.json @@ -1618,6 +1618,9 @@ "get": { "summary": "MultipartCompiled", "operationId": "generalMultipartCompiled", + "consumes": [ + "application\/json" + ], "produces": [ "multipart\/form-data" ],