From 67087793a2ece05ea8c64a37b7b5f0748b6c3b39 Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Wed, 11 Oct 2023 16:08:04 +0100 Subject: [PATCH 01/29] feat: setSession helper for web --- templates/web/src/client.ts.twig | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/templates/web/src/client.ts.twig b/templates/web/src/client.ts.twig index f952c31c2..c6d1b8985 100644 --- a/templates/web/src/client.ts.twig +++ b/templates/web/src/client.ts.twig @@ -124,6 +124,22 @@ class Client { return this; } + /** + * Set Session + * + * Set the session for the current user. + * + * @param {{userId: string, secret: string}} + * + * @returns {this} + */ + setSession({userId, secret}: {userId: string, secret: string}) { + const encodedSession = btoa(JSON.stringify({userId, secret})); + this.headers['X-Fallback-Cookies'] = JSON.stringify({ [`a_session_${this.config.project}`]: encodedSession }); + + return this; + } + /** * Set Realtime Endpoint * From 1c1d3f9065653b817adbd5167d67d18233453582 Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Wed, 22 Nov 2023 10:03:28 +0000 Subject: [PATCH 02/29] feat: simpler setSession --- templates/node/lib/client.js.twig | 15 +++++++++++++++ templates/web/src/client.ts.twig | 11 +++++------ 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/templates/node/lib/client.js.twig b/templates/node/lib/client.js.twig index b2538e8e8..d7069ea15 100644 --- a/templates/node/lib/client.js.twig +++ b/templates/node/lib/client.js.twig @@ -70,6 +70,21 @@ class Client { return this; } + /** + * Set Session + * + * Set the session for the current user. + * + * @param {string} encodedSession + * + * @returns {this} + */ + setSession(encodedSession: string) { + this.headers['x-fallback-cookies'] = JSON.stringify({ [`a_session_${this.config.project}`]: encodedSession }); + + return this; + } + /** * @param {string} key * @param {string} value diff --git a/templates/web/src/client.ts.twig b/templates/web/src/client.ts.twig index c6d1b8985..848443fc3 100644 --- a/templates/web/src/client.ts.twig +++ b/templates/web/src/client.ts.twig @@ -129,13 +129,12 @@ class Client { * * Set the session for the current user. * - * @param {{userId: string, secret: string}} + * @param {string} encodedSession * * @returns {this} */ - setSession({userId, secret}: {userId: string, secret: string}) { - const encodedSession = btoa(JSON.stringify({userId, secret})); - this.headers['X-Fallback-Cookies'] = JSON.stringify({ [`a_session_${this.config.project}`]: encodedSession }); + setSession(encodedSession: string) { + this.headers['x-fallback-cookies'] = JSON.stringify({ [`a_session_${this.config.project}`]: encodedSession }); return this; } @@ -365,7 +364,7 @@ class Client { }; if (typeof window !== 'undefined' && window.localStorage) { - headers['X-Fallback-Cookies'] = window.localStorage.getItem('cookieFallback') ?? ''; + headers['x-fallback-cookies'] = window.localStorage.getItem('cookieFallback') ?? ''; } if (method === 'GET') { @@ -413,7 +412,7 @@ class Client { throw new {{spec.title | caseUcfirst}}Exception(data?.message, response.status, data?.type, data); } - const cookieFallback = response.headers.get('X-Fallback-Cookies'); + const cookieFallback = response.headers.get('x-fallback-cookies'); if (typeof window !== 'undefined' && window.localStorage && cookieFallback) { window.console.warn('{{spec.title | caseUcfirst}} is using localStorage for session management. Increase your security by adding a custom domain as your API endpoint.'); From db438b28f9139a2d996d82dff680eb2815176ee6 Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Tue, 5 Dec 2023 15:54:01 +0100 Subject: [PATCH 03/29] feat: set templates --- .../library/src/main/java/io/appwrite/Client.kt.twig | 12 ++++++++++++ templates/node/index.d.ts.twig | 11 +++++++++++ templates/node/lib/client.js.twig | 4 ++-- 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/templates/android/library/src/main/java/io/appwrite/Client.kt.twig b/templates/android/library/src/main/java/io/appwrite/Client.kt.twig index 210ef4fbe..8ad0036e5 100644 --- a/templates/android/library/src/main/java/io/appwrite/Client.kt.twig +++ b/templates/android/library/src/main/java/io/appwrite/Client.kt.twig @@ -188,6 +188,18 @@ class Client @JvmOverloads constructor( return this } + /** + * Set the user session. + * + * @param session + * @return this + */ + fun setSession(encodedSession: String): Client { + //this.addHeader() + + return this + } + /** * Set realtime endpoint * diff --git a/templates/node/index.d.ts.twig b/templates/node/index.d.ts.twig index adae9cf5d..7cb76ae57 100644 --- a/templates/node/index.d.ts.twig +++ b/templates/node/index.d.ts.twig @@ -90,6 +90,17 @@ declare module "{{ language.params.npmPackage|caseDash }}" { */ setEndpoint(endpoint: string): Client; + /** + * Set Session + * + * Set the session for the current user. + * + * @param {string} encodedSession + * + * @returns {this} + */ + setSession(encodedSession: string): Client; + /** * Set self signed. * diff --git a/templates/node/lib/client.js.twig b/templates/node/lib/client.js.twig index d7069ea15..d3860a110 100644 --- a/templates/node/lib/client.js.twig +++ b/templates/node/lib/client.js.twig @@ -61,7 +61,7 @@ class Client { * * @param {string} endpoint * - * @return this + * @return {this} */ setEndpoint(endpoint) { @@ -79,7 +79,7 @@ class Client { * * @returns {this} */ - setSession(encodedSession: string) { + setSession(encodedSession) { this.headers['x-fallback-cookies'] = JSON.stringify({ [`a_session_${this.config.project}`]: encodedSession }); return this; From 838799f7946e095feaa21c536bd7731eefaf561d Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Tue, 12 Dec 2023 15:45:26 +0000 Subject: [PATCH 04/29] chore: revert set session --- .../src/main/java/io/appwrite/Client.kt.twig | 12 ------------ templates/node/index.d.ts.twig | 11 ----------- templates/node/lib/client.js.twig | 17 +---------------- templates/web/src/client.ts.twig | 19 ++----------------- 4 files changed, 3 insertions(+), 56 deletions(-) diff --git a/templates/android/library/src/main/java/io/appwrite/Client.kt.twig b/templates/android/library/src/main/java/io/appwrite/Client.kt.twig index 8ad0036e5..210ef4fbe 100644 --- a/templates/android/library/src/main/java/io/appwrite/Client.kt.twig +++ b/templates/android/library/src/main/java/io/appwrite/Client.kt.twig @@ -188,18 +188,6 @@ class Client @JvmOverloads constructor( return this } - /** - * Set the user session. - * - * @param session - * @return this - */ - fun setSession(encodedSession: String): Client { - //this.addHeader() - - return this - } - /** * Set realtime endpoint * diff --git a/templates/node/index.d.ts.twig b/templates/node/index.d.ts.twig index 7cb76ae57..adae9cf5d 100644 --- a/templates/node/index.d.ts.twig +++ b/templates/node/index.d.ts.twig @@ -90,17 +90,6 @@ declare module "{{ language.params.npmPackage|caseDash }}" { */ setEndpoint(endpoint: string): Client; - /** - * Set Session - * - * Set the session for the current user. - * - * @param {string} encodedSession - * - * @returns {this} - */ - setSession(encodedSession: string): Client; - /** * Set self signed. * diff --git a/templates/node/lib/client.js.twig b/templates/node/lib/client.js.twig index d3860a110..b2538e8e8 100644 --- a/templates/node/lib/client.js.twig +++ b/templates/node/lib/client.js.twig @@ -61,7 +61,7 @@ class Client { * * @param {string} endpoint * - * @return {this} + * @return this */ setEndpoint(endpoint) { @@ -70,21 +70,6 @@ class Client { return this; } - /** - * Set Session - * - * Set the session for the current user. - * - * @param {string} encodedSession - * - * @returns {this} - */ - setSession(encodedSession) { - this.headers['x-fallback-cookies'] = JSON.stringify({ [`a_session_${this.config.project}`]: encodedSession }); - - return this; - } - /** * @param {string} key * @param {string} value diff --git a/templates/web/src/client.ts.twig b/templates/web/src/client.ts.twig index 848443fc3..f952c31c2 100644 --- a/templates/web/src/client.ts.twig +++ b/templates/web/src/client.ts.twig @@ -124,21 +124,6 @@ class Client { return this; } - /** - * Set Session - * - * Set the session for the current user. - * - * @param {string} encodedSession - * - * @returns {this} - */ - setSession(encodedSession: string) { - this.headers['x-fallback-cookies'] = JSON.stringify({ [`a_session_${this.config.project}`]: encodedSession }); - - return this; - } - /** * Set Realtime Endpoint * @@ -364,7 +349,7 @@ class Client { }; if (typeof window !== 'undefined' && window.localStorage) { - headers['x-fallback-cookies'] = window.localStorage.getItem('cookieFallback') ?? ''; + headers['X-Fallback-Cookies'] = window.localStorage.getItem('cookieFallback') ?? ''; } if (method === 'GET') { @@ -412,7 +397,7 @@ class Client { throw new {{spec.title | caseUcfirst}}Exception(data?.message, response.status, data?.type, data); } - const cookieFallback = response.headers.get('x-fallback-cookies'); + const cookieFallback = response.headers.get('X-Fallback-Cookies'); if (typeof window !== 'undefined' && window.localStorage && cookieFallback) { window.console.warn('{{spec.title | caseUcfirst}} is using localStorage for session management. Increase your security by adding a custom domain as your API endpoint.'); From e6e763413b707f678fa7986a25ec08f2c624847e Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Tue, 12 Dec 2023 15:54:18 +0000 Subject: [PATCH 05/29] feat: use unidici over axios --- templates/node/lib/client.js.twig | 65 ++++++++++++++++++------------- templates/node/package.json.twig | 2 +- 2 files changed, 40 insertions(+), 27 deletions(-) diff --git a/templates/node/lib/client.js.twig b/templates/node/lib/client.js.twig index b2538e8e8..976f2ef22 100644 --- a/templates/node/lib/client.js.twig +++ b/templates/node/lib/client.js.twig @@ -1,6 +1,6 @@ const os = require('os'); const URL = require('url').URL; -const axios = require('axios'); +const {fetch} = require('undici'); const FormData = require('form-data'); const {{spec.title | caseUcfirst}}Exception = require('./exception.js'); @@ -92,7 +92,7 @@ class Client { let formData = null; - // Compute FormData for axios and appwrite. + // Compute FormData if (contentType.startsWith('multipart/form-data')) { const form = new FormData(); @@ -116,32 +116,45 @@ class Client { formData = form; } - let options = { + const url = this.endpoint + path; + const isGetMethod = method.toUpperCase() === "GET"; + const isJson = contentType.startsWith("application/json"); + const isMultipart = contentType.startsWith("multipart/form-data"); + + let body = null; + if (!isGetMethod && !isMultipart) { + body = isJson ? JSON.stringify(params) : params; + } else if (!isGetMethod && isMultipart) { + body = formData; + } + + try { + const response = await fetch(url, { method: method.toUpperCase(), - url: this.endpoint + path, - params: (method.toUpperCase() === 'GET') ? params : {}, headers: headers, - data: (method.toUpperCase() === 'GET' || contentType.startsWith('multipart/form-data')) ? formData : params, - json: (contentType.startsWith('application/json')), - responseType: responseType - }; - try { - let response = await axios(options); - return response.data; - } catch(error) { - if('response' in error && error.response !== undefined) { - if(error.response && 'data' in error.response) { - if (typeof(error.response.data) === 'string') { - throw new {{spec.title | caseUcfirst}}Exception(error.response.data, error.response.status, '', error.response.data); - } else { - throw new {{spec.title | caseUcfirst}}Exception(error.response.data.message, error.response.status, error.response.data.type, error.response.data); - } - } else { - throw new {{spec.title | caseUcfirst}}Exception(error.response.statusText, error.response.status, error.response.data); - } - } else { - throw new {{spec.title | caseUcfirst}}Exception(error.message); - } + body: body, + }); + + let data; + if (responseType === "json") { + data = await response.json(); + } else { + data = await response.text(); + } + + return data; + } catch (error) { + if (error.response) { + const errorData = await error.response.json(); + throw new {{spec.title | caseUcfirst}}Exception( + errorData.message, + error.response.status, + errorData.type, + errorData + ); + } else { + throw new {{spec.title | caseUcfirst}}Exception(error.message); + } } } diff --git a/templates/node/package.json.twig b/templates/node/package.json.twig index 6a8eae40c..21fd14d83 100644 --- a/templates/node/package.json.twig +++ b/templates/node/package.json.twig @@ -14,7 +14,7 @@ "@types/node": "^18.16.1" }, "dependencies": { - "axios": "^1.4.0", + "undici": "^6.0.1", "form-data": "^4.0.0" } } From 4431e1a5134c1cc4527b89bac25c19796287423b Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Wed, 13 Dec 2023 10:53:43 +0000 Subject: [PATCH 06/29] feat: replace form-data --- templates/node/lib/client.js.twig | 9 ++++++--- templates/node/package.json.twig | 1 - 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/templates/node/lib/client.js.twig b/templates/node/lib/client.js.twig index 976f2ef22..e4775ebdd 100644 --- a/templates/node/lib/client.js.twig +++ b/templates/node/lib/client.js.twig @@ -1,7 +1,6 @@ const os = require('os'); const URL = require('url').URL; -const {fetch} = require('undici'); -const FormData = require('form-data'); +const {fetch, FormData} = require('undici'); const {{spec.title | caseUcfirst}}Exception = require('./exception.js'); class Client { @@ -71,8 +70,12 @@ class Client { } /** + * Sets a header for requests. + * * @param {string} key * @param {string} value + * + * @return this */ addHeader(key, value) { this.headers[key.toLowerCase()] = value; @@ -116,7 +119,7 @@ class Client { formData = form; } - const url = this.endpoint + path; + const url = new URL(path, this.endpoint).toString(); const isGetMethod = method.toUpperCase() === "GET"; const isJson = contentType.startsWith("application/json"); const isMultipart = contentType.startsWith("multipart/form-data"); diff --git a/templates/node/package.json.twig b/templates/node/package.json.twig index 21fd14d83..13e502f59 100644 --- a/templates/node/package.json.twig +++ b/templates/node/package.json.twig @@ -15,6 +15,5 @@ }, "dependencies": { "undici": "^6.0.1", - "form-data": "^4.0.0" } } From 57c684b13e348c5bddb30dcfeb87ba7bc2571463 Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Wed, 13 Dec 2023 14:36:17 +0000 Subject: [PATCH 07/29] feat: use undici for cli --- templates/cli/lib/client.js.twig | 123 ++++++++++++--------------- templates/cli/package.json.twig | 3 +- templates/deno/src/client.ts.twig | 70 +++++++-------- templates/deno/src/inputFile.ts.twig | 6 ++ templates/node/lib/client.js.twig | 116 ++++++++++++------------- templates/node/lib/inputFile.js.twig | 3 +- 6 files changed, 150 insertions(+), 171 deletions(-) diff --git a/templates/cli/lib/client.js.twig b/templates/cli/lib/client.js.twig index 32142337f..dc29ae34c 100644 --- a/templates/cli/lib/client.js.twig +++ b/templates/cli/lib/client.js.twig @@ -1,10 +1,6 @@ const os = require('os'); -const https = require("https"); -const axios = require("axios"); -const JSONbig = require("json-bigint")({ storeAsString: false }); -const FormData = require("form-data"); +const {fetch, FormData, Agent} = require("undici"); const {{spec.title | caseUcfirst}}Exception = require("./exception.js"); -const { globalConfig } = require("./config.js"); class Client { static CHUNK_SIZE = 5*1024*1024; // 5MB @@ -94,84 +90,71 @@ class Client { return this; } - async call( - method, - path = "", - headers = {}, - params = {}, - responseType = "json" - ) { - headers = Object.assign({}, this.headers, headers); + async call(method, path = "", headers = {}, params = {}) { + const url = new URL(path, this.endpoint); - let contentType = headers["content-type"].toLowerCase(); + let body = undefined; - let formData = null; + if (method.toUpperCase() === "GET") { + url.search = new URLSearchParams(this.flatten(params)).toString(); + } else if (headers["content-type"]?.toLowerCase().startsWith("multipart/form-data")) { + delete headers["content-type"]; + const formData = new FormData(); - if (contentType.startsWith("multipart/form-data")) { - const form = new FormData(); + const flatParams = this.flatten(params); - let flatParams = Client.flatten(params); - - for (const key in flatParams) { - form.append(key, flatParams[key]); + for (const [key, value] of flatParams.entries()) { + if (value && value.type && value.type === "file") { + formData.append(key, value.file, value.filename); + } else { + formData.append(key, value); + } } - headers = { - ...headers, - ...form.getHeaders(), - }; - - formData = form; + body = formData; + } else { + body = JSON.stringify(params); } - let options = { - method: method.toUpperCase(), - url: this.endpoint + path, - params: method.toUpperCase() === "GET" ? params : {}, - headers: headers, - data: - method.toUpperCase() === "GET" || contentType.startsWith("multipart/form-data") ? formData : params, - json: contentType.startsWith("application/json"), - transformRequest: method.toUpperCase() === "GET" || contentType.startsWith("multipart/form-data") ? undefined : (data) => JSONbig.stringify(data), - transformResponse: [ (data) => data ? JSONbig.parse(data) : data ], - responseType: responseType, - }; - if (this.selfSigned == true) { - // Allow self signed requests - options.httpsAgent = new https.Agent({ rejectUnauthorized: false }); - } try { - let response = await axios(options); - if (response.headers["set-cookie"]) { - globalConfig.setCookie(response.headers["set-cookie"][0]); - } - return response.data; - } catch (error) { - if ("response" in error && error.response !== undefined) { - if (error.response && "data" in error.response) { - if (typeof error.response.data === "string") { - throw new {{spec.title | caseUcfirst}}Exception( - error.response.data, - error.response.status, - error.response.data - ); - } else { - throw new {{spec.title | caseUcfirst}}Exception( - error.response.data.message, - error.response.status, - error.response.data - ); - } - } else { - throw new {{spec.title | caseUcfirst}}Exception( - error.response.statusText, - error.response.status, - error.response.data + const response = await fetch(url.toString(), { + method, + headers, + body, + dispatcher: new Agent({ + connect: { + rejectUnauthorized: !this.selfSigned, + }, + }) + }); + const contentType = response.headers.get("content-type"); + + if (contentType && contentType.includes("application/json")) { + if (response.status >= 400) { + const json = await response.json(); + throw new {{ spec.title | caseUcfirst}}Exception( + json.message, + json.status, + json.type ?? "", + json ); } + + return response.json(); } else { - throw new {{spec.title | caseUcfirst}}Exception(error.message); + if (response.status >= 400) { + const text = await response.text(); + throw new {{ spec.title | caseUcfirst}}Exception(text, response.status, "", null); + } + return response; } + } catch (error) { + throw new {{ spec.title | caseUcfirst}}Exception( + error?.response?.message || error.message, + error?.response?.code, + error?.response?.type, + error.response + ); } } diff --git a/templates/cli/package.json.twig b/templates/cli/package.json.twig index b4c51eb16..b1b24e269 100644 --- a/templates/cli/package.json.twig +++ b/templates/cli/package.json.twig @@ -22,11 +22,10 @@ "windows-arm64": "pkg -t node16-win-arm64 -o build/appwrite-cli-win-arm64.exe package.json" }, "dependencies": { - "axios": "1.5.0", + "undici": "^6.0.1", "chalk": "4.1.2", "cli-table3": "^0.6.2", "commander": "^9.2.0", - "form-data": "^4.0.0", "json-bigint": "^1.0.0", "inquirer": "^8.2.4", "tar": "^6.1.11", diff --git a/templates/deno/src/client.ts.twig b/templates/deno/src/client.ts.twig index dc899958a..862e23027 100644 --- a/templates/deno/src/client.ts.twig +++ b/templates/deno/src/client.ts.twig @@ -61,66 +61,66 @@ export class Client { return this; } - withoutHeader(key: string, headers: Payload): Payload { - return Object.keys(headers).reduce((acc: Payload, cv: string) => { - if (cv === 'content-type') return acc; - acc[cv] = headers[cv]; - return acc; - }, {}) - } + async call(method: string, path: string = "", headers: Payload = {}, params: Payload = {}) { + const url = new URL(path, this.endpoint); - async call(method: string, path: string = '', headers: Payload = {}, params: Payload = {}) { - headers = Object.assign({}, this.headers, headers); + let body: string | FormData | undefined = undefined; - let body; - const url = new URL(this.endpoint + path); - if (method.toUpperCase() === 'GET') { + if (method.toUpperCase() === "GET") { url.search = new URLSearchParams(this.flatten(params)).toString(); - body = null; - } else if (headers['content-type'].toLowerCase().startsWith('multipart/form-data')) { - headers = this.withoutHeader('content-type', headers); + } else if (headers["content-type"]?.toLowerCase().startsWith("multipart/form-data")) { + delete headers["content-type"]; const formData = new FormData(); + const flatParams = this.flatten(params); - for (const key in flatParams) { - const value = flatParams[key]; - if(value && value.type && value.type === 'file') { + for (const [key, value] of flatParams.entries()) { + if (value && value.type && value.type === "file") { formData.append(key, value.file, value.filename); } else { - formData.append(key, flatParams[key]); + formData.append(key, value); } } + body = formData; } else { body = JSON.stringify(params); } - const options = { - method: method.toUpperCase(), - headers: headers, - body: body, - }; - try { - let response = await fetch(url.toString(), options); - const contentType = response.headers.get('content-type'); - - if (contentType && contentType.includes('application/json')) { + const response = await fetch(url.toString(), { + method, + headers, + body, + }); + const contentType = response.headers.get("content-type"); + + if (contentType && contentType.includes("application/json")) { if (response.status >= 400) { - let res = await response.json(); - throw new {{ spec.title | caseUcfirst}}Exception(res.message, res.status, res.type ?? "", res); + const json = await response.json(); + throw new {{ spec.title | caseUcfirst}}Exception( + json.message, + json.status, + json.type ?? "", + json + ); } return response.json(); } else { if (response.status >= 400) { - let res = await response.text(); - throw new {{ spec.title | caseUcfirst}}Exception(res, response.status, "", null); + const text = await response.text(); + throw new {{ spec.title | caseUcfirst}}Exception(text, response.status, "", null); } return response; } - } catch(error) { - throw new {{ spec.title | caseUcfirst}}Exception(error?.response?.message || error.message, error?.response?.code, error?.response?.type, error.response); + } catch (error) { + throw new {{ spec.title | caseUcfirst}}Exception( + error?.response?.message || error.message, + error?.response?.code, + error?.response?.type, + error.response + ); } } diff --git a/templates/deno/src/inputFile.ts.twig b/templates/deno/src/inputFile.ts.twig index fdda97d4c..3aa128b64 100644 --- a/templates/deno/src/inputFile.ts.twig +++ b/templates/deno/src/inputFile.ts.twig @@ -19,6 +19,12 @@ export class InputFile { return new InputFile(stream, filename, size); }; + static fromBlob = async (blob: Blob, filename: string) => { + const arrayBuffer = await blob.arrayBuffer(); + const buffer = Buffer.from(arrayBuffer); + return InputFile.fromBuffer(buffer, filename); + }; + static fromBuffer = (buffer: Uint8Array, filename: string): InputFile => { const stream = _bufferToString(buffer); const size = buffer.byteLength; diff --git a/templates/node/lib/client.js.twig b/templates/node/lib/client.js.twig index e4775ebdd..0fe0b7cb3 100644 --- a/templates/node/lib/client.js.twig +++ b/templates/node/lib/client.js.twig @@ -1,6 +1,6 @@ const os = require('os'); const URL = require('url').URL; -const {fetch, FormData} = require('undici'); +const {fetch, FormData, Agent} = require('undici'); const {{spec.title | caseUcfirst}}Exception = require('./exception.js'); class Client { @@ -83,81 +83,71 @@ class Client { return this; } - async call(method, path = '', headers = {}, params = {}, responseType = 'json') { - if(this.selfSigned) { // Allow self signed requests - process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = 0; - } - + async call(method, path = "", headers = {}, params = {}) { + const url = new URL(path, this.endpoint); - headers = Object.assign({}, this.headers, headers); + let body = undefined; - let contentType = headers['content-type'].toLowerCase(); + if (method.toUpperCase() === "GET") { + url.search = new URLSearchParams(this.flatten(params)).toString(); + } else if (headers["content-type"]?.toLowerCase().startsWith("multipart/form-data")) { + delete headers["content-type"]; + const formData = new FormData(); - let formData = null; + const flatParams = this.flatten(params); - // Compute FormData - if (contentType.startsWith('multipart/form-data')) { - const form = new FormData(); - - let flatParams = Client.flatten(params); - - for (const key in flatParams) { - const value = flatParams[key]; - - if(value && value.type && value.type === 'file') { - form.append(key, value.file, { filename: value.filename }); + for (const [key, value] of flatParams.entries()) { + if (value && value.type && value.type === "file") { + formData.append(key, value.file, value.filename); } else { - form.append(key, flatParams[key]); + formData.append(key, value); } } - headers = { - ...headers, - ...form.getHeaders() - }; - - formData = form; + body = formData; + } else { + body = JSON.stringify(params); } - const url = new URL(path, this.endpoint).toString(); - const isGetMethod = method.toUpperCase() === "GET"; - const isJson = contentType.startsWith("application/json"); - const isMultipart = contentType.startsWith("multipart/form-data"); - - let body = null; - if (!isGetMethod && !isMultipart) { - body = isJson ? JSON.stringify(params) : params; - } else if (!isGetMethod && isMultipart) { - body = formData; - } - try { - const response = await fetch(url, { - method: method.toUpperCase(), - headers: headers, - body: body, - }); - - let data; - if (responseType === "json") { - data = await response.json(); - } else { - data = await response.text(); - } - - return data; + const response = await fetch(url.toString(), { + method, + headers, + body, + dispatcher: new Agent({ + connect: { + rejectUnauthorized: !this.selfSigned, + }, + }) + }); + const contentType = response.headers.get("content-type"); + + if (contentType && contentType.includes("application/json")) { + if (response.status >= 400) { + const json = await response.json(); + throw new {{ spec.title | caseUcfirst}}Exception( + json.message, + json.status, + json.type ?? "", + json + ); + } + + return response.json(); + } else { + if (response.status >= 400) { + const text = await response.text(); + throw new {{ spec.title | caseUcfirst}}Exception(text, response.status, "", null); + } + return response; + } } catch (error) { - if (error.response) { - const errorData = await error.response.json(); - throw new {{spec.title | caseUcfirst}}Exception( - errorData.message, - error.response.status, - errorData.type, - errorData + throw new {{ spec.title | caseUcfirst}}Exception( + error?.response?.message || error.message, + error?.response?.code, + error?.response?.type, + error.response ); - } else { - throw new {{spec.title | caseUcfirst}}Exception(error.message); - } } } diff --git a/templates/node/lib/inputFile.js.twig b/templates/node/lib/inputFile.js.twig index de7f0d9c7..9246386e0 100644 --- a/templates/node/lib/inputFile.js.twig +++ b/templates/node/lib/inputFile.js.twig @@ -18,12 +18,13 @@ class InputFile { return new InputFile(stream, filename, size); }; - static fromBlob = async (blob, filename) => { + static fromBlob = async (blob: Blob, filename: string) => { const arrayBuffer = await blob.arrayBuffer(); const buffer = Buffer.from(arrayBuffer); return InputFile.fromBuffer(buffer, filename); }; + static fromStream = (stream, filename, size) => { return new InputFile(stream, filename, size); }; From 503ce5d35dc6def779d7c9c5f7ec30711837d76a Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Wed, 13 Dec 2023 14:45:08 +0000 Subject: [PATCH 08/29] chore: use earlier undici --- templates/cli/package.json.twig | 2 +- templates/node/package.json.twig | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/cli/package.json.twig b/templates/cli/package.json.twig index b1b24e269..87c11d373 100644 --- a/templates/cli/package.json.twig +++ b/templates/cli/package.json.twig @@ -22,7 +22,7 @@ "windows-arm64": "pkg -t node16-win-arm64 -o build/appwrite-cli-win-arm64.exe package.json" }, "dependencies": { - "undici": "^6.0.1", + "undici": "^5.28.2", "chalk": "4.1.2", "cli-table3": "^0.6.2", "commander": "^9.2.0", diff --git a/templates/node/package.json.twig b/templates/node/package.json.twig index 13e502f59..1e8ae1844 100644 --- a/templates/node/package.json.twig +++ b/templates/node/package.json.twig @@ -14,6 +14,6 @@ "@types/node": "^18.16.1" }, "dependencies": { - "undici": "^6.0.1", + "undici": "^5.28.2", } } From 14888848bfc3cc9303ad3960f86ccf587e279bd2 Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Wed, 13 Dec 2023 14:51:48 +0000 Subject: [PATCH 09/29] fix: deno fromBlob --- templates/deno/src/inputFile.ts.twig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/deno/src/inputFile.ts.twig b/templates/deno/src/inputFile.ts.twig index 3aa128b64..b12230284 100644 --- a/templates/deno/src/inputFile.ts.twig +++ b/templates/deno/src/inputFile.ts.twig @@ -21,7 +21,7 @@ export class InputFile { static fromBlob = async (blob: Blob, filename: string) => { const arrayBuffer = await blob.arrayBuffer(); - const buffer = Buffer.from(arrayBuffer); + const buffer = new Uint8Array(arrayBuffer); return InputFile.fromBuffer(buffer, filename); }; From bae06f8a5b339840b1e496417eb6bb4211e9af26 Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Wed, 13 Dec 2023 14:56:43 +0000 Subject: [PATCH 10/29] feat: depr node 12, add node 18 --- .github/workflows/tests.yml | 2 +- example.php | 81 ++++++++++-------------- tests/{Node12Test.php => Node18Test.php} | 7 +- 3 files changed, 36 insertions(+), 54 deletions(-) rename tests/{Node12Test.php => Node18Test.php} (81%) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 95188b5fa..9fa696df6 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -13,7 +13,7 @@ jobs: fail-fast: false matrix: php-version: ['8.1'] - sdk: [Android11Java8, Android11Java11, Android12Java8, Android12Java11, CLINode14, CLINode16, DartBeta, DartStable, Deno1193, Deno1303, DotNet60, DotNet70, FlutterStable, FlutterBeta, Go112, Go118, KotlinJava8, KotlinJava11, KotlinJava17, Node12, Node14, Node16, PHP74, PHP80, Python38, Python39, Python310, Ruby27, Ruby30, Ruby31, AppleSwift55, Swift55, WebChromium, WebNode] + sdk: [Android11Java8, Android11Java11, Android12Java8, Android12Java11, CLINode14, CLINode16, DartBeta, DartStable, Deno1193, Deno1303, DotNet60, DotNet70, FlutterStable, FlutterBeta, Go112, Go118, KotlinJava8, KotlinJava11, KotlinJava17, Node14, Node16, Node18, PHP74, PHP80, Python38, Python39, Python310, Ruby27, Ruby30, Ruby31, AppleSwift55, Swift55, WebChromium, WebNode] steps: - name: Checkout repository diff --git a/example.php b/example.php index 6af298baf..6f387e51b 100644 --- a/example.php +++ b/example.php @@ -24,7 +24,8 @@ try { - function getSSLPage($url) { + function getSSLPage($url) + { $ch = curl_init(); curl_setopt($ch, CURLOPT_HEADER, false); curl_setopt($ch, CURLOPT_URL, $url); @@ -37,13 +38,13 @@ function getSSLPage($url) { } // Leave the platform you want uncommented - $platform = 'client'; + // $platform = 'client'; // $platform = 'console'; - // $platform = 'server'; + $platform = 'server'; - $spec = getSSLPage("https://raw.githubusercontent.com/appwrite/appwrite/master/app/config/specs/swagger2-latest-{$platform}.json"); + $spec = getSSLPage("https://raw.githubusercontent.com/appwrite/appwrite/feat-ssr/app/config/specs/swagger2-latest-server.json"); - if(empty($spec)) { + if (empty($spec)) { throw new Exception('Failed to fetch spec from Appwrite server'); } @@ -69,8 +70,7 @@ function getSSLPage($url) { ->setDiscord('564160730845151244', 'https://appwrite.io/discord') ->setDefaultHeaders([ 'X-Appwrite-Response-Format' => '1.2.0', - ]) - ; + ]); $sdk->generate(__DIR__ . '/examples/php'); @@ -94,8 +94,7 @@ function getSSLPage($url) { ->setDiscord('564160730845151244', 'https://appwrite.io/discord') ->setDefaultHeaders([ 'X-Appwrite-Response-Format' => '1.2.0', - ]) - ; + ]); $sdk->generate(__DIR__ . '/examples/web'); @@ -118,16 +117,16 @@ function getSSLPage($url) { ->setDiscord('564160730845151244', 'https://appwrite.io/discord') ->setDefaultHeaders([ 'X-Appwrite-Response-Format' => '1.2.0', - ]) - ; + ]); $sdk->generate(__DIR__ . '/examples/deno'); // Node - $sdk = new SDK(new Node(), new Swagger2($spec)); + $node = new Node(); + $node->setNPMPackage('node-appwrite-local'); + $sdk = new SDK($node, new Swagger2($spec)); - $sdk - ->setName('NAME') + $sdk->setName('NAME') ->setDescription('Repo description goes here') ->setShortDescription('Repo short description goes here') ->setURL('https://example.com') @@ -141,8 +140,7 @@ function getSSLPage($url) { ->setDiscord('564160730845151244', 'https://appwrite.io/discord') ->setDefaultHeaders([ 'X-Appwrite-Response-Format' => '1.2.0', - ]) - ; + ]); $sdk->generate(__DIR__ . '/examples/node'); @@ -186,8 +184,7 @@ function getSSLPage($url) { ->setDiscord('564160730845151244', 'https://appwrite.io/discord') ->setDefaultHeaders([ 'X-Appwrite-Response-Format' => '0.15.0', - ]) - ; + ]); $sdk->generate(__DIR__ . '/examples/cli'); @@ -209,8 +206,7 @@ function getSSLPage($url) { ->setDiscord('564160730845151244', 'https://appwrite.io/discord') ->setDefaultHeaders([ 'X-Appwrite-Response-Format' => '1.2.0', - ]) - ; + ]); $sdk->generate(__DIR__ . '/examples/ruby'); @@ -232,8 +228,7 @@ function getSSLPage($url) { ->setDiscord('564160730845151244', 'https://appwrite.io/discord') ->setDefaultHeaders([ 'X-Appwrite-Response-Format' => '1.2.0', - ]) - ; + ]); $sdk->generate(__DIR__ . '/examples/python'); @@ -260,8 +255,7 @@ function getSSLPage($url) { ->setDiscord('564160730845151244', 'https://appwrite.io/discord') ->setDefaultHeaders([ 'X-Appwrite-Response-Format' => '1.2.0', - ]) - ; + ]); $sdk->generate(__DIR__ . '/examples/dart'); @@ -287,8 +281,7 @@ function getSSLPage($url) { ->setDiscord('564160730845151244', 'https://appwrite.io/discord') ->setDefaultHeaders([ 'X-Appwrite-Response-Format' => '1.2.0', - ]) - ; + ]); $sdk->generate(__DIR__ . '/examples/flutter'); @@ -312,8 +305,7 @@ function getSSLPage($url) { ->setDiscord('564160730845151244', 'https://appwrite.io/discord') ->setDefaultHeaders([ 'X-Appwrite-Response-Format' => '1.2.0', - ]) - ; + ]); $sdk->generate(__DIR__ . '/examples/go'); @@ -337,8 +329,7 @@ function getSSLPage($url) { ->setDiscord('564160730845151244', 'https://appwrite.io/discord') ->setDefaultHeaders([ 'X-Appwrite-Response-Format' => '1.2.0', - ]) - ; + ]); $sdk->generate(__DIR__ . '/examples/swift-server'); @@ -361,11 +352,10 @@ function getSSLPage($url) { ->setDiscord('564160730845151244', 'https://appwrite.io/discord') ->setDefaultHeaders([ 'X-Appwrite-Response-Format' => '1.2.0', - ]) - ; + ]); $sdk->generate(__DIR__ . '/examples/apple'); - + // DotNet $sdk = new SDK(new DotNet(), new Swagger2($spec)); @@ -385,8 +375,7 @@ function getSSLPage($url) { ->setDiscord('564160730845151244', 'https://appwrite.io/discord') ->setDefaultHeaders([ 'X-Appwrite-Response-Format' => '1.2.0', - ]) - ; + ]); $sdk->generate(__DIR__ . '/examples/dotnet'); @@ -406,15 +395,14 @@ function getSSLPage($url) { ->setGitUserName('repoowner') ->setGitRepoName('reponame') ->setTwitter('appwrite_io') - ->setDiscord('564160730845151244', 'https://appwrite.io/discord') - ; + ->setDiscord('564160730845151244', 'https://appwrite.io/discord'); $sdk->generate(__DIR__ . '/examples/REST'); // Android $sdk = new SDK(new Android(), new Swagger2($spec)); - + $sdk ->setName('Android') ->setNamespace('io appwrite') @@ -432,13 +420,12 @@ function getSSLPage($url) { ->setDiscord('564160730845151244', 'https://appwrite.io/discord') ->setDefaultHeaders([ 'x-appwrite-response-format' => '0.7.0', - ]) - ; + ]); $sdk->generate(__DIR__ . '/examples/android'); // Kotlin $sdk = new SDK(new Kotlin(), new Swagger2($spec)); - + $sdk ->setName('Kotlin') ->setNamespace('io appwrite') @@ -456,8 +443,7 @@ function getSSLPage($url) { ->setDiscord('564160730845151244', 'https://appwrite.io/discord') ->setDefaultHeaders([ 'x-appwrite-response-format' => '0.8.0', - ]) - ; + ]); $sdk->generate(__DIR__ . '/examples/kotlin'); // GraphQL @@ -466,14 +452,11 @@ function getSSLPage($url) { $sdk ->setName('GraphQL') ->setDescription('Appwrite is an open-source backend as a service server that abstract and simplify complex and repetitive development tasks behind a very simple to use REST API. Appwrite aims to help you develop your apps faster and in a more secure way. Use the Flutter SDK to integrate your app with the Appwrite server to easily start interacting with all of Appwrite backend APIs and tools. For full API documentation and tutorials go to https://appwrite.io/docs') - ->setLogo('https://appwrite.io/v1/images/console.png') - ; + ->setLogo('https://appwrite.io/v1/images/console.png'); $sdk->generate(__DIR__ . '/examples/graphql'); -} -catch (Exception $exception) { +} catch (Exception $exception) { echo 'Error: ' . $exception->getMessage() . ' on ' . $exception->getFile() . ':' . $exception->getLine() . "\n"; -} -catch (Throwable $exception) { +} catch (Throwable $exception) { echo 'Error: ' . $exception->getMessage() . ' on ' . $exception->getFile() . ':' . $exception->getLine() . "\n"; } diff --git a/tests/Node12Test.php b/tests/Node18Test.php similarity index 81% rename from tests/Node12Test.php rename to tests/Node18Test.php index 9c0e6818e..e507f1e5f 100644 --- a/tests/Node12Test.php +++ b/tests/Node18Test.php @@ -2,7 +2,7 @@ namespace Tests; -class Node12Test extends Base +class Node16Test extends Base { protected string $sdkName = 'node.js'; protected string $sdkPlatform = 'server'; @@ -10,13 +10,12 @@ class Node12Test extends Base protected string $version = '0.0.1'; protected string $language = 'node'; - protected string $class = 'Appwrite\SDK\Language\Node'; protected array $build = [ - 'docker run --rm -v $(pwd):/app -w /app/tests/sdks/node node:12-alpine npm install', + 'docker run --rm -v $(pwd):/app -w /app/tests/sdks/node node:18-alpine npm install', ]; protected string $command = - 'docker run --network="mockapi" --rm -v $(pwd):/app -w /app node:12-alpine node tests/languages/node/test.js'; + 'docker run --network="mockapi" --rm -v $(pwd):/app -w /app node:16-alpine node tests/languages/node/test.js'; protected array $expectedOutput = [ ...Base::FOO_RESPONSES, From f417e13d4224189276bc588e4d5b5b5db539aaef Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Wed, 13 Dec 2023 15:08:54 +0000 Subject: [PATCH 11/29] fix: node --- templates/node/lib/inputFile.js.twig | 2 +- templates/node/package.json.twig | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/node/lib/inputFile.js.twig b/templates/node/lib/inputFile.js.twig index 9246386e0..ce4e151f1 100644 --- a/templates/node/lib/inputFile.js.twig +++ b/templates/node/lib/inputFile.js.twig @@ -18,7 +18,7 @@ class InputFile { return new InputFile(stream, filename, size); }; - static fromBlob = async (blob: Blob, filename: string) => { + static fromBlob = async (blob, filename) => { const arrayBuffer = await blob.arrayBuffer(); const buffer = Buffer.from(arrayBuffer); return InputFile.fromBuffer(buffer, filename); diff --git a/templates/node/package.json.twig b/templates/node/package.json.twig index 1e8ae1844..468b0094e 100644 --- a/templates/node/package.json.twig +++ b/templates/node/package.json.twig @@ -14,6 +14,6 @@ "@types/node": "^18.16.1" }, "dependencies": { - "undici": "^5.28.2", + "undici": "^5.28.2" } } From 935312ea50df762c9bda8933dcd80ebeb46b3bf5 Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Wed, 13 Dec 2023 15:32:15 +0000 Subject: [PATCH 12/29] fix: headers --- templates/cli/lib/client.js.twig | 1 + templates/deno/src/client.ts.twig | 1 + templates/node/lib/client.js.twig | 1 + tests/Node18Test.php | 4 ++-- 4 files changed, 5 insertions(+), 2 deletions(-) diff --git a/templates/cli/lib/client.js.twig b/templates/cli/lib/client.js.twig index dc29ae34c..24b96b0e1 100644 --- a/templates/cli/lib/client.js.twig +++ b/templates/cli/lib/client.js.twig @@ -91,6 +91,7 @@ class Client { } async call(method, path = "", headers = {}, params = {}) { + headers = {...this.headers, ...headers}; const url = new URL(path, this.endpoint); let body = undefined; diff --git a/templates/deno/src/client.ts.twig b/templates/deno/src/client.ts.twig index 862e23027..087a56a26 100644 --- a/templates/deno/src/client.ts.twig +++ b/templates/deno/src/client.ts.twig @@ -62,6 +62,7 @@ export class Client { } async call(method: string, path: string = "", headers: Payload = {}, params: Payload = {}) { + headers = {...this.headers, ...headers}; const url = new URL(path, this.endpoint); let body: string | FormData | undefined = undefined; diff --git a/templates/node/lib/client.js.twig b/templates/node/lib/client.js.twig index 0fe0b7cb3..363518962 100644 --- a/templates/node/lib/client.js.twig +++ b/templates/node/lib/client.js.twig @@ -84,6 +84,7 @@ class Client { } async call(method, path = "", headers = {}, params = {}) { + headers = {...this.headers, ...headers}; const url = new URL(path, this.endpoint); let body = undefined; diff --git a/tests/Node18Test.php b/tests/Node18Test.php index e507f1e5f..9df997ba5 100644 --- a/tests/Node18Test.php +++ b/tests/Node18Test.php @@ -2,7 +2,7 @@ namespace Tests; -class Node16Test extends Base +class Node18Test extends Base { protected string $sdkName = 'node.js'; protected string $sdkPlatform = 'server'; @@ -15,7 +15,7 @@ class Node16Test extends Base 'docker run --rm -v $(pwd):/app -w /app/tests/sdks/node node:18-alpine npm install', ]; protected string $command = - 'docker run --network="mockapi" --rm -v $(pwd):/app -w /app node:16-alpine node tests/languages/node/test.js'; + 'docker run --network="mockapi" --rm -v $(pwd):/app -w /app node:18-alpine node tests/languages/node/test.js'; protected array $expectedOutput = [ ...Base::FOO_RESPONSES, From 51de21478de6c41a563802d70b2df6e178df4013 Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Wed, 13 Dec 2023 15:42:49 +0000 Subject: [PATCH 13/29] fix: flattenParams --- templates/cli/lib/client.js.twig | 2 +- templates/deno/src/client.ts.twig | 4 ++-- templates/node/lib/client.js.twig | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/templates/cli/lib/client.js.twig b/templates/cli/lib/client.js.twig index 24b96b0e1..42161baae 100644 --- a/templates/cli/lib/client.js.twig +++ b/templates/cli/lib/client.js.twig @@ -102,7 +102,7 @@ class Client { delete headers["content-type"]; const formData = new FormData(); - const flatParams = this.flatten(params); + const flatParams = Client.flatten(params); for (const [key, value] of flatParams.entries()) { if (value && value.type && value.type === "file") { diff --git a/templates/deno/src/client.ts.twig b/templates/deno/src/client.ts.twig index 087a56a26..dfc9b2ddf 100644 --- a/templates/deno/src/client.ts.twig +++ b/templates/deno/src/client.ts.twig @@ -73,7 +73,7 @@ export class Client { delete headers["content-type"]; const formData = new FormData(); - const flatParams = this.flatten(params); + const flatParams = Client.flatten(params); for (const [key, value] of flatParams.entries()) { if (value && value.type && value.type === "file") { @@ -125,7 +125,7 @@ export class Client { } } - flatten(data: Payload, prefix = '') { + static flatten(data: Payload, prefix = '') { let output: Payload = {}; for (const key in data) { diff --git a/templates/node/lib/client.js.twig b/templates/node/lib/client.js.twig index 363518962..afeba19f8 100644 --- a/templates/node/lib/client.js.twig +++ b/templates/node/lib/client.js.twig @@ -95,7 +95,7 @@ class Client { delete headers["content-type"]; const formData = new FormData(); - const flatParams = this.flatten(params); + const flatParams = Client.flatten(params); for (const [key, value] of flatParams.entries()) { if (value && value.type && value.type === "file") { From 38017fe69628fbd96ad7433eda2d1215fb63abd9 Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Wed, 13 Dec 2023 15:47:10 +0000 Subject: [PATCH 14/29] fix: flatten bugs --- templates/cli/lib/client.js.twig | 2 +- templates/deno/src/client.ts.twig | 4 ++-- templates/node/lib/client.js.twig | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/templates/cli/lib/client.js.twig b/templates/cli/lib/client.js.twig index 42161baae..d4c3364b3 100644 --- a/templates/cli/lib/client.js.twig +++ b/templates/cli/lib/client.js.twig @@ -97,7 +97,7 @@ class Client { let body = undefined; if (method.toUpperCase() === "GET") { - url.search = new URLSearchParams(this.flatten(params)).toString(); + url.search = new URLSearchParams(Client.flatten(params)).toString(); } else if (headers["content-type"]?.toLowerCase().startsWith("multipart/form-data")) { delete headers["content-type"]; const formData = new FormData(); diff --git a/templates/deno/src/client.ts.twig b/templates/deno/src/client.ts.twig index dfc9b2ddf..8846cf888 100644 --- a/templates/deno/src/client.ts.twig +++ b/templates/deno/src/client.ts.twig @@ -68,7 +68,7 @@ export class Client { let body: string | FormData | undefined = undefined; if (method.toUpperCase() === "GET") { - url.search = new URLSearchParams(this.flatten(params)).toString(); + url.search = new URLSearchParams(Client.flatten(params)).toString(); } else if (headers["content-type"]?.toLowerCase().startsWith("multipart/form-data")) { delete headers["content-type"]; const formData = new FormData(); @@ -133,7 +133,7 @@ export class Client { let finalKey = prefix ? prefix + '[' + key +']' : key; if (Array.isArray(value)) { - output = { ...output, ...this.flatten(value, finalKey) }; // @todo: handle name collision here if needed + output = { ...output, ...Client.flatten(value, finalKey) }; // @todo: handle name collision here if needed } else { output[finalKey] = value; diff --git a/templates/node/lib/client.js.twig b/templates/node/lib/client.js.twig index afeba19f8..fecc082e9 100644 --- a/templates/node/lib/client.js.twig +++ b/templates/node/lib/client.js.twig @@ -90,7 +90,7 @@ class Client { let body = undefined; if (method.toUpperCase() === "GET") { - url.search = new URLSearchParams(this.flatten(params)).toString(); + url.search = new URLSearchParams(Client.flatten(params)).toString(); } else if (headers["content-type"]?.toLowerCase().startsWith("multipart/form-data")) { delete headers["content-type"]; const formData = new FormData(); From 6fd840601ca8417833ba1450854394b9ae2e60ca Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Thu, 14 Dec 2023 11:59:07 +0000 Subject: [PATCH 15/29] fix: path building --- templates/cli/lib/client.js.twig | 4 ++-- templates/deno/src/client.ts.twig | 4 ++-- templates/node/lib/client.js.twig | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/templates/cli/lib/client.js.twig b/templates/cli/lib/client.js.twig index d4c3364b3..7064f23f0 100644 --- a/templates/cli/lib/client.js.twig +++ b/templates/cli/lib/client.js.twig @@ -92,7 +92,7 @@ class Client { async call(method, path = "", headers = {}, params = {}) { headers = {...this.headers, ...headers}; - const url = new URL(path, this.endpoint); + const url = new URL(this.endpoint + path); let body = undefined; @@ -104,7 +104,7 @@ class Client { const flatParams = Client.flatten(params); - for (const [key, value] of flatParams.entries()) { + for (const [key, value] of Object.entries(flatParams)) { if (value && value.type && value.type === "file") { formData.append(key, value.file, value.filename); } else { diff --git a/templates/deno/src/client.ts.twig b/templates/deno/src/client.ts.twig index 8846cf888..3f044a063 100644 --- a/templates/deno/src/client.ts.twig +++ b/templates/deno/src/client.ts.twig @@ -63,7 +63,7 @@ export class Client { async call(method: string, path: string = "", headers: Payload = {}, params: Payload = {}) { headers = {...this.headers, ...headers}; - const url = new URL(path, this.endpoint); + const url = new URL(this.endpoint + path); let body: string | FormData | undefined = undefined; @@ -75,7 +75,7 @@ export class Client { const flatParams = Client.flatten(params); - for (const [key, value] of flatParams.entries()) { + for (const [key, value] of Object.entries(flatParams)) { if (value && value.type && value.type === "file") { formData.append(key, value.file, value.filename); } else { diff --git a/templates/node/lib/client.js.twig b/templates/node/lib/client.js.twig index fecc082e9..64e667392 100644 --- a/templates/node/lib/client.js.twig +++ b/templates/node/lib/client.js.twig @@ -85,7 +85,7 @@ class Client { async call(method, path = "", headers = {}, params = {}) { headers = {...this.headers, ...headers}; - const url = new URL(path, this.endpoint); + const url = new URL(this.endpoint + path); let body = undefined; @@ -97,7 +97,7 @@ class Client { const flatParams = Client.flatten(params); - for (const [key, value] of flatParams.entries()) { + for (const [key, value] of Object.entries(flatParams)) { if (value && value.type && value.type === "file") { formData.append(key, value.file, value.filename); } else { @@ -112,7 +112,7 @@ class Client { try { const response = await fetch(url.toString(), { - method, + method: method.toUpperCase(), headers, body, dispatcher: new Agent({ From 3a007954ee7efad071072bcf509e95adc2c2af7f Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Thu, 14 Dec 2023 14:21:34 +0000 Subject: [PATCH 16/29] feat: node chunked uploads with undici --- templates/node/base/requests/file.twig | 125 +++++++++---------------- templates/node/lib/inputFile.js.twig | 43 +++++---- 2 files changed, 64 insertions(+), 104 deletions(-) diff --git a/templates/node/base/requests/file.twig b/templates/node/base/requests/file.twig index 1f6f4a85b..76a20f0d7 100644 --- a/templates/node/base/requests/file.twig +++ b/templates/node/base/requests/file.twig @@ -1,7 +1,8 @@ {% for parameter in method.parameters.all %} {% if parameter.type == 'file' %} - const size = {{ parameter.name | caseCamel | escapeKeyword }}.size; + const size = {{ parameter.name | caseCamel | escapeKeyword }}.size; + const apiHeaders = { {% for parameter in method.parameters.header %} '{{ parameter.name }}': ${{ parameter.name | caseCamel | escapeKeyword }}, @@ -28,117 +29,77 @@ {% endif %} {% endfor %} - let currentChunk = Buffer.from(''); - let currentChunkSize = 0; - let currentChunkStart = 0; + let currentChunk = 1; + let currentPosition = 0; + let uploadableChunk = new Uint8Array(client.CHUNK_SIZE); + - const selfClient = this.client; - - async function uploadChunk(lastUpload = false) { - if(chunksUploaded - 1 >= currentChunkStart / client.CHUNK_SIZE) { + const uploadChunk = async (lastUpload = false) => { + if(currentChunk <= chunksUploaded) { return; } - - const start = currentChunkStart; - const end = currentChunkStart + currentChunkSize - 1; - if(!lastUpload || currentChunkStart !== 0) { + const start = ((currentChunk - 1) * client.CHUNK_SIZE); + let end = start + currentPosition - 1; + + if(!lastUpload || currentChunk !== 1) { apiHeaders['content-range'] = 'bytes ' + start + '-' + end + '/' + size; } + let uploadableChunkTrimmed; + + if(currentPosition + 1 >= client.CHUNK_SIZE) { + uploadableChunkTrimmed = uploadableChunk; + } else { + uploadableChunkTrimmed = new Uint8Array(currentPosition); + for(let i = 0; i <= currentPosition; i++) { + uploadableChunkTrimmed[i] = uploadableChunk[i]; + } + } + if (id) { apiHeaders['x-{{spec.title | caseLower }}-id'] = id; } - payload['{{ parameter.name }}'] = { - type: 'file', - file: currentChunk, - filename: {{ parameter.name }}.filename, - size: currentChunkSize - }; + payload['{{ parameter.name }}'] = { type: 'file', file: new File([uploadableChunkTrimmed], {{ parameter.name | caseCamel | escapeKeyword }}.filename), filename: {{ parameter.name | caseCamel | escapeKeyword }}.filename }; - response = await selfClient.call('{{ method.method | caseLower }}', apiPath, apiHeaders, payload{% if method.type == 'location' %}, 'arraybuffer'{% endif %}); + response = await this.client.call('{{ method.method | caseLower }}', apiPath, apiHeaders, payload{% if method.type == 'location' %}, 'arraybuffer'{% endif %}); if (!id) { id = response['$id']; } - + if (onProgress !== null) { onProgress({ $id: response['$id'], - progress: Math.min((start+client.CHUNK_SIZE) * client.CHUNK_SIZE, size) / size * 100, + progress: Math.min((currentChunk) * client.CHUNK_SIZE, size) / size * 100, sizeUploaded: end+1, chunksTotal: response['chunksTotal'], chunksUploaded: response['chunksUploaded'] }); } - currentChunkStart += client.CHUNK_SIZE; + uploadableChunk = new Uint8Array(client.CHUNK_SIZE); + currentChunk++; + currentPosition = 0; } - return await new Promise((resolve, reject) => { - const writeStream = new Stream.Writable(); - writeStream._write = async (mainChunk, encoding, callback) => { - try { - // Segment incoming chunk into up to 5MB chunks - const mainChunkSize = Buffer.byteLength(mainChunk); - const chunksCount = Math.ceil(mainChunkSize / client.CHUNK_SIZE); - const chunks = []; - - for(let i = 0; i < chunksCount; i++) { - const chunk = mainChunk.slice(i * client.CHUNK_SIZE, (i + 1) * client.CHUNK_SIZE); - chunks.push(chunk); - } - - for (const chunk of chunks) { - const chunkSize = Buffer.byteLength(chunk); - - if(chunkSize + currentChunkSize == client.CHUNK_SIZE) { - // Upload chunk - currentChunk = Buffer.concat([currentChunk, chunk]); - currentChunkSize = Buffer.byteLength(currentChunk); - await uploadChunk(); - currentChunk = Buffer.from(''); - currentChunkSize = 0; - } else if(chunkSize + currentChunkSize > client.CHUNK_SIZE) { - // Upload chunk, put rest into next chunk - const bytesToUpload = client.CHUNK_SIZE - currentChunkSize; - const newChunkSection = chunk.slice(0, bytesToUpload); - currentChunk = Buffer.concat([currentChunk, newChunkSection]); - currentChunkSize = Buffer.byteLength(currentChunk); - await uploadChunk(); - currentChunk = chunk.slice(bytesToUpload, undefined); - currentChunkSize = chunkSize - bytesToUpload; - } else { - // Append into current chunk - currentChunk = Buffer.concat([currentChunk, chunk]); - currentChunkSize = chunkSize + currentChunkSize; - } - } - - callback(); - } catch (e) { - callback(e); + for await (const chunk of {{ parameter.name | caseCamel | escapeKeyword }}.stream) { + for(const b of chunk) { + uploadableChunk[currentPosition] = b; + + currentPosition++; + if(currentPosition >= client.CHUNK_SIZE) { + await uploadChunk(); + currentPosition = 0; } } + } - writeStream.on("finish", async () => { - if(currentChunkSize > 0) { - try { - await uploadChunk(true); - } catch (e) { - reject(e); - } - } - - resolve(response); - }); + if (currentPosition > 0) { // Check if there's any remaining data for the last chunk + await uploadChunk(true); + } - writeStream.on("error", (err) => { - reject(err); - }); - - {{ parameter.name | caseCamel | escapeKeyword }}.stream.pipe(writeStream); - }); + return response; {% endif %} {% endfor %} diff --git a/templates/node/lib/inputFile.js.twig b/templates/node/lib/inputFile.js.twig index ce4e151f1..b2d71d774 100644 --- a/templates/node/lib/inputFile.js.twig +++ b/templates/node/lib/inputFile.js.twig @@ -1,38 +1,37 @@ -const { Readable } = require('stream'); -const fs = require('fs'); +const { readFileSync } = require("fs"); +const { ReadableStream } = require("stream/web"); + +const _bufferToString = (buffer) => { + return new ReadableStream({ + start(controller) { + controller.enqueue(buffer); + controller.close(); + }, + }); +}; class InputFile { - stream; // Content of file, readable stream + stream; // Content of file as a stream size; // Total final size of the file content filename; // File name static fromPath = (filePath, filename) => { - const stream = fs.createReadStream(filePath); - const { size } = fs.statSync(filePath); + const data = readFileSync(filePath); + const stream = _bufferToString(data); + const size = data.byteLength; return new InputFile(stream, filename, size); }; static fromBuffer = (buffer, filename) => { - const stream = Readable.from(buffer); - const size = Buffer.byteLength(buffer); - return new InputFile(stream, filename, size); - }; - - static fromBlob = async (blob, filename) => { - const arrayBuffer = await blob.arrayBuffer(); - const buffer = Buffer.from(arrayBuffer); - return InputFile.fromBuffer(buffer, filename); - }; - - - static fromStream = (stream, filename, size) => { + const stream = _bufferToString(buffer); + const size = buffer.byteLength; return new InputFile(stream, filename, size); }; static fromPlainText = (content, filename) => { - const buffer = Buffer.from(content, "utf-8"); - const stream = Readable.from(buffer); - const size = Buffer.byteLength(buffer); + const buffer = new TextEncoder().encode(content); + const stream = _bufferToString(buffer); + const size = buffer.byteLength; return new InputFile(stream, filename, size); }; @@ -43,4 +42,4 @@ class InputFile { } } -module.exports = InputFile; +module.exports = InputFile; \ No newline at end of file From 1f82372b392a99831cc2386d1eeb5d273da90838 Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Thu, 14 Dec 2023 14:34:29 +0000 Subject: [PATCH 17/29] fix: missing import --- templates/node/lib/services/service.js.twig | 1 + 1 file changed, 1 insertion(+) diff --git a/templates/node/lib/services/service.js.twig b/templates/node/lib/services/service.js.twig index acfa340a3..71da69a55 100644 --- a/templates/node/lib/services/service.js.twig +++ b/templates/node/lib/services/service.js.twig @@ -5,6 +5,7 @@ const client = require('../client.js'); const Stream = require('stream'); const { promisify } = require('util'); const fs = require('fs'); +const { File } = require('undici'); class {{ service.name | caseUcfirst }} extends Service { From 6b7098d98ac5bb2735827582e5c6da324ddb5ac1 Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Thu, 14 Dec 2023 15:33:24 +0000 Subject: [PATCH 18/29] test: replace cli node 14 with node 18 --- .github/workflows/tests.yml | 2 +- tests/{CLINode14Test.php => CLINode18Test.php} | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) rename tests/{CLINode14Test.php => CLINode18Test.php} (91%) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 9fa696df6..606fc2ed0 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -13,7 +13,7 @@ jobs: fail-fast: false matrix: php-version: ['8.1'] - sdk: [Android11Java8, Android11Java11, Android12Java8, Android12Java11, CLINode14, CLINode16, DartBeta, DartStable, Deno1193, Deno1303, DotNet60, DotNet70, FlutterStable, FlutterBeta, Go112, Go118, KotlinJava8, KotlinJava11, KotlinJava17, Node14, Node16, Node18, PHP74, PHP80, Python38, Python39, Python310, Ruby27, Ruby30, Ruby31, AppleSwift55, Swift55, WebChromium, WebNode] + sdk: [Android11Java8, Android11Java11, Android12Java8, Android12Java11, CLINode16, CLINode18, DartBeta, DartStable, Deno1193, Deno1303, DotNet60, DotNet70, FlutterStable, FlutterBeta, Go112, Go118, KotlinJava8, KotlinJava11, KotlinJava17, Node14, Node16, Node18, PHP74, PHP80, Python38, Python39, Python310, Ruby27, Ruby30, Ruby31, AppleSwift55, Swift55, WebChromium, WebNode] steps: - name: Checkout repository diff --git a/tests/CLINode14Test.php b/tests/CLINode18Test.php similarity index 91% rename from tests/CLINode14Test.php rename to tests/CLINode18Test.php index 6b92eb423..c02953213 100644 --- a/tests/CLINode14Test.php +++ b/tests/CLINode18Test.php @@ -5,7 +5,7 @@ use Appwrite\SDK\Language; use Appwrite\SDK\Language\CLI; -class CLINode14Test extends Base +class CLINode16Test extends Base { protected string $sdkName = 'cli'; protected string $sdkPlatform = 'server'; @@ -15,11 +15,11 @@ class CLINode14Test extends Base protected string $language = 'cli'; protected string $class = 'Appwrite\SDK\Language\CLI'; protected array $build = [ - 'docker run --rm -v $(pwd):/app -w /app/tests/sdks/cli node:16-alpine npm install', + 'docker run --rm -v $(pwd):/app -w /app/tests/sdks/cli node:18-alpine npm install', 'cp tests/languages/cli/test.js tests/sdks/cli/test.js' ]; protected string $command = - 'docker run --network="mockapi" --rm -v $(pwd):/app -w /app/tests/sdks/cli node:14-alpine node test.js'; + 'docker run --network="mockapi" --rm -v $(pwd):/app -w /app/tests/sdks/cli node:18-alpine node test.js'; protected array $expectedOutput = [ ...Base::FOO_RESPONSES, From 13ec5ec850a3b16c0d21a2a4ce12ab1422d2fbd3 Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Thu, 14 Dec 2023 18:04:23 +0000 Subject: [PATCH 19/29] chore: revert cli changes --- .github/workflows/tests.yml | 2 +- example.php | 81 +++++++----- templates/cli/lib/client.js.twig | 124 ++++++++++-------- templates/cli/package.json.twig | 3 +- .../{CLINode18Test.php => CLINode14Test.php} | 6 +- 5 files changed, 125 insertions(+), 91 deletions(-) rename tests/{CLINode18Test.php => CLINode14Test.php} (91%) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 606fc2ed0..9fa696df6 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -13,7 +13,7 @@ jobs: fail-fast: false matrix: php-version: ['8.1'] - sdk: [Android11Java8, Android11Java11, Android12Java8, Android12Java11, CLINode16, CLINode18, DartBeta, DartStable, Deno1193, Deno1303, DotNet60, DotNet70, FlutterStable, FlutterBeta, Go112, Go118, KotlinJava8, KotlinJava11, KotlinJava17, Node14, Node16, Node18, PHP74, PHP80, Python38, Python39, Python310, Ruby27, Ruby30, Ruby31, AppleSwift55, Swift55, WebChromium, WebNode] + sdk: [Android11Java8, Android11Java11, Android12Java8, Android12Java11, CLINode14, CLINode16, DartBeta, DartStable, Deno1193, Deno1303, DotNet60, DotNet70, FlutterStable, FlutterBeta, Go112, Go118, KotlinJava8, KotlinJava11, KotlinJava17, Node14, Node16, Node18, PHP74, PHP80, Python38, Python39, Python310, Ruby27, Ruby30, Ruby31, AppleSwift55, Swift55, WebChromium, WebNode] steps: - name: Checkout repository diff --git a/example.php b/example.php index 6f387e51b..6af298baf 100644 --- a/example.php +++ b/example.php @@ -24,8 +24,7 @@ try { - function getSSLPage($url) - { + function getSSLPage($url) { $ch = curl_init(); curl_setopt($ch, CURLOPT_HEADER, false); curl_setopt($ch, CURLOPT_URL, $url); @@ -38,13 +37,13 @@ function getSSLPage($url) } // Leave the platform you want uncommented - // $platform = 'client'; + $platform = 'client'; // $platform = 'console'; - $platform = 'server'; + // $platform = 'server'; - $spec = getSSLPage("https://raw.githubusercontent.com/appwrite/appwrite/feat-ssr/app/config/specs/swagger2-latest-server.json"); + $spec = getSSLPage("https://raw.githubusercontent.com/appwrite/appwrite/master/app/config/specs/swagger2-latest-{$platform}.json"); - if (empty($spec)) { + if(empty($spec)) { throw new Exception('Failed to fetch spec from Appwrite server'); } @@ -70,7 +69,8 @@ function getSSLPage($url) ->setDiscord('564160730845151244', 'https://appwrite.io/discord') ->setDefaultHeaders([ 'X-Appwrite-Response-Format' => '1.2.0', - ]); + ]) + ; $sdk->generate(__DIR__ . '/examples/php'); @@ -94,7 +94,8 @@ function getSSLPage($url) ->setDiscord('564160730845151244', 'https://appwrite.io/discord') ->setDefaultHeaders([ 'X-Appwrite-Response-Format' => '1.2.0', - ]); + ]) + ; $sdk->generate(__DIR__ . '/examples/web'); @@ -117,16 +118,16 @@ function getSSLPage($url) ->setDiscord('564160730845151244', 'https://appwrite.io/discord') ->setDefaultHeaders([ 'X-Appwrite-Response-Format' => '1.2.0', - ]); + ]) + ; $sdk->generate(__DIR__ . '/examples/deno'); // Node - $node = new Node(); - $node->setNPMPackage('node-appwrite-local'); - $sdk = new SDK($node, new Swagger2($spec)); + $sdk = new SDK(new Node(), new Swagger2($spec)); - $sdk->setName('NAME') + $sdk + ->setName('NAME') ->setDescription('Repo description goes here') ->setShortDescription('Repo short description goes here') ->setURL('https://example.com') @@ -140,7 +141,8 @@ function getSSLPage($url) ->setDiscord('564160730845151244', 'https://appwrite.io/discord') ->setDefaultHeaders([ 'X-Appwrite-Response-Format' => '1.2.0', - ]); + ]) + ; $sdk->generate(__DIR__ . '/examples/node'); @@ -184,7 +186,8 @@ function getSSLPage($url) ->setDiscord('564160730845151244', 'https://appwrite.io/discord') ->setDefaultHeaders([ 'X-Appwrite-Response-Format' => '0.15.0', - ]); + ]) + ; $sdk->generate(__DIR__ . '/examples/cli'); @@ -206,7 +209,8 @@ function getSSLPage($url) ->setDiscord('564160730845151244', 'https://appwrite.io/discord') ->setDefaultHeaders([ 'X-Appwrite-Response-Format' => '1.2.0', - ]); + ]) + ; $sdk->generate(__DIR__ . '/examples/ruby'); @@ -228,7 +232,8 @@ function getSSLPage($url) ->setDiscord('564160730845151244', 'https://appwrite.io/discord') ->setDefaultHeaders([ 'X-Appwrite-Response-Format' => '1.2.0', - ]); + ]) + ; $sdk->generate(__DIR__ . '/examples/python'); @@ -255,7 +260,8 @@ function getSSLPage($url) ->setDiscord('564160730845151244', 'https://appwrite.io/discord') ->setDefaultHeaders([ 'X-Appwrite-Response-Format' => '1.2.0', - ]); + ]) + ; $sdk->generate(__DIR__ . '/examples/dart'); @@ -281,7 +287,8 @@ function getSSLPage($url) ->setDiscord('564160730845151244', 'https://appwrite.io/discord') ->setDefaultHeaders([ 'X-Appwrite-Response-Format' => '1.2.0', - ]); + ]) + ; $sdk->generate(__DIR__ . '/examples/flutter'); @@ -305,7 +312,8 @@ function getSSLPage($url) ->setDiscord('564160730845151244', 'https://appwrite.io/discord') ->setDefaultHeaders([ 'X-Appwrite-Response-Format' => '1.2.0', - ]); + ]) + ; $sdk->generate(__DIR__ . '/examples/go'); @@ -329,7 +337,8 @@ function getSSLPage($url) ->setDiscord('564160730845151244', 'https://appwrite.io/discord') ->setDefaultHeaders([ 'X-Appwrite-Response-Format' => '1.2.0', - ]); + ]) + ; $sdk->generate(__DIR__ . '/examples/swift-server'); @@ -352,10 +361,11 @@ function getSSLPage($url) ->setDiscord('564160730845151244', 'https://appwrite.io/discord') ->setDefaultHeaders([ 'X-Appwrite-Response-Format' => '1.2.0', - ]); + ]) + ; $sdk->generate(__DIR__ . '/examples/apple'); - + // DotNet $sdk = new SDK(new DotNet(), new Swagger2($spec)); @@ -375,7 +385,8 @@ function getSSLPage($url) ->setDiscord('564160730845151244', 'https://appwrite.io/discord') ->setDefaultHeaders([ 'X-Appwrite-Response-Format' => '1.2.0', - ]); + ]) + ; $sdk->generate(__DIR__ . '/examples/dotnet'); @@ -395,14 +406,15 @@ function getSSLPage($url) ->setGitUserName('repoowner') ->setGitRepoName('reponame') ->setTwitter('appwrite_io') - ->setDiscord('564160730845151244', 'https://appwrite.io/discord'); + ->setDiscord('564160730845151244', 'https://appwrite.io/discord') + ; $sdk->generate(__DIR__ . '/examples/REST'); // Android $sdk = new SDK(new Android(), new Swagger2($spec)); - + $sdk ->setName('Android') ->setNamespace('io appwrite') @@ -420,12 +432,13 @@ function getSSLPage($url) ->setDiscord('564160730845151244', 'https://appwrite.io/discord') ->setDefaultHeaders([ 'x-appwrite-response-format' => '0.7.0', - ]); + ]) + ; $sdk->generate(__DIR__ . '/examples/android'); // Kotlin $sdk = new SDK(new Kotlin(), new Swagger2($spec)); - + $sdk ->setName('Kotlin') ->setNamespace('io appwrite') @@ -443,7 +456,8 @@ function getSSLPage($url) ->setDiscord('564160730845151244', 'https://appwrite.io/discord') ->setDefaultHeaders([ 'x-appwrite-response-format' => '0.8.0', - ]); + ]) + ; $sdk->generate(__DIR__ . '/examples/kotlin'); // GraphQL @@ -452,11 +466,14 @@ function getSSLPage($url) $sdk ->setName('GraphQL') ->setDescription('Appwrite is an open-source backend as a service server that abstract and simplify complex and repetitive development tasks behind a very simple to use REST API. Appwrite aims to help you develop your apps faster and in a more secure way. Use the Flutter SDK to integrate your app with the Appwrite server to easily start interacting with all of Appwrite backend APIs and tools. For full API documentation and tutorials go to https://appwrite.io/docs') - ->setLogo('https://appwrite.io/v1/images/console.png'); + ->setLogo('https://appwrite.io/v1/images/console.png') + ; $sdk->generate(__DIR__ . '/examples/graphql'); -} catch (Exception $exception) { +} +catch (Exception $exception) { echo 'Error: ' . $exception->getMessage() . ' on ' . $exception->getFile() . ':' . $exception->getLine() . "\n"; -} catch (Throwable $exception) { +} +catch (Throwable $exception) { echo 'Error: ' . $exception->getMessage() . ' on ' . $exception->getFile() . ':' . $exception->getLine() . "\n"; } diff --git a/templates/cli/lib/client.js.twig b/templates/cli/lib/client.js.twig index 7064f23f0..32142337f 100644 --- a/templates/cli/lib/client.js.twig +++ b/templates/cli/lib/client.js.twig @@ -1,6 +1,10 @@ const os = require('os'); -const {fetch, FormData, Agent} = require("undici"); +const https = require("https"); +const axios = require("axios"); +const JSONbig = require("json-bigint")({ storeAsString: false }); +const FormData = require("form-data"); const {{spec.title | caseUcfirst}}Exception = require("./exception.js"); +const { globalConfig } = require("./config.js"); class Client { static CHUNK_SIZE = 5*1024*1024; // 5MB @@ -90,72 +94,84 @@ class Client { return this; } - async call(method, path = "", headers = {}, params = {}) { - headers = {...this.headers, ...headers}; - const url = new URL(this.endpoint + path); + async call( + method, + path = "", + headers = {}, + params = {}, + responseType = "json" + ) { + headers = Object.assign({}, this.headers, headers); - let body = undefined; + let contentType = headers["content-type"].toLowerCase(); - if (method.toUpperCase() === "GET") { - url.search = new URLSearchParams(Client.flatten(params)).toString(); - } else if (headers["content-type"]?.toLowerCase().startsWith("multipart/form-data")) { - delete headers["content-type"]; - const formData = new FormData(); + let formData = null; - const flatParams = Client.flatten(params); + if (contentType.startsWith("multipart/form-data")) { + const form = new FormData(); - for (const [key, value] of Object.entries(flatParams)) { - if (value && value.type && value.type === "file") { - formData.append(key, value.file, value.filename); - } else { - formData.append(key, value); - } + let flatParams = Client.flatten(params); + + for (const key in flatParams) { + form.append(key, flatParams[key]); } - body = formData; - } else { - body = JSON.stringify(params); + headers = { + ...headers, + ...form.getHeaders(), + }; + + formData = form; } + let options = { + method: method.toUpperCase(), + url: this.endpoint + path, + params: method.toUpperCase() === "GET" ? params : {}, + headers: headers, + data: + method.toUpperCase() === "GET" || contentType.startsWith("multipart/form-data") ? formData : params, + json: contentType.startsWith("application/json"), + transformRequest: method.toUpperCase() === "GET" || contentType.startsWith("multipart/form-data") ? undefined : (data) => JSONbig.stringify(data), + transformResponse: [ (data) => data ? JSONbig.parse(data) : data ], + responseType: responseType, + }; + if (this.selfSigned == true) { + // Allow self signed requests + options.httpsAgent = new https.Agent({ rejectUnauthorized: false }); + } try { - const response = await fetch(url.toString(), { - method, - headers, - body, - dispatcher: new Agent({ - connect: { - rejectUnauthorized: !this.selfSigned, - }, - }) - }); - const contentType = response.headers.get("content-type"); - - if (contentType && contentType.includes("application/json")) { - if (response.status >= 400) { - const json = await response.json(); - throw new {{ spec.title | caseUcfirst}}Exception( - json.message, - json.status, - json.type ?? "", - json + let response = await axios(options); + if (response.headers["set-cookie"]) { + globalConfig.setCookie(response.headers["set-cookie"][0]); + } + return response.data; + } catch (error) { + if ("response" in error && error.response !== undefined) { + if (error.response && "data" in error.response) { + if (typeof error.response.data === "string") { + throw new {{spec.title | caseUcfirst}}Exception( + error.response.data, + error.response.status, + error.response.data + ); + } else { + throw new {{spec.title | caseUcfirst}}Exception( + error.response.data.message, + error.response.status, + error.response.data + ); + } + } else { + throw new {{spec.title | caseUcfirst}}Exception( + error.response.statusText, + error.response.status, + error.response.data ); } - - return response.json(); } else { - if (response.status >= 400) { - const text = await response.text(); - throw new {{ spec.title | caseUcfirst}}Exception(text, response.status, "", null); - } - return response; + throw new {{spec.title | caseUcfirst}}Exception(error.message); } - } catch (error) { - throw new {{ spec.title | caseUcfirst}}Exception( - error?.response?.message || error.message, - error?.response?.code, - error?.response?.type, - error.response - ); } } diff --git a/templates/cli/package.json.twig b/templates/cli/package.json.twig index 87c11d373..b4c51eb16 100644 --- a/templates/cli/package.json.twig +++ b/templates/cli/package.json.twig @@ -22,10 +22,11 @@ "windows-arm64": "pkg -t node16-win-arm64 -o build/appwrite-cli-win-arm64.exe package.json" }, "dependencies": { - "undici": "^5.28.2", + "axios": "1.5.0", "chalk": "4.1.2", "cli-table3": "^0.6.2", "commander": "^9.2.0", + "form-data": "^4.0.0", "json-bigint": "^1.0.0", "inquirer": "^8.2.4", "tar": "^6.1.11", diff --git a/tests/CLINode18Test.php b/tests/CLINode14Test.php similarity index 91% rename from tests/CLINode18Test.php rename to tests/CLINode14Test.php index c02953213..9986758d1 100644 --- a/tests/CLINode18Test.php +++ b/tests/CLINode14Test.php @@ -5,7 +5,7 @@ use Appwrite\SDK\Language; use Appwrite\SDK\Language\CLI; -class CLINode16Test extends Base +class CLINode14Test extends Base { protected string $sdkName = 'cli'; protected string $sdkPlatform = 'server'; @@ -15,11 +15,11 @@ class CLINode16Test extends Base protected string $language = 'cli'; protected string $class = 'Appwrite\SDK\Language\CLI'; protected array $build = [ - 'docker run --rm -v $(pwd):/app -w /app/tests/sdks/cli node:18-alpine npm install', + 'docker run --rm -v $(pwd):/app -w /app/tests/sdks/cli node:16-alpine npm install', 'cp tests/languages/cli/test.js tests/sdks/cli/test.js' ]; protected string $command = - 'docker run --network="mockapi" --rm -v $(pwd):/app -w /app/tests/sdks/cli node:18-alpine node test.js'; + 'docker run --rm -v $(pwd):/app -w /app/tests/sdks/cli node:14-alpine node test.js'; protected array $expectedOutput = [ ...Base::FOO_RESPONSES, From 0b367db14d4e4166821dbef830c60888398eab9c Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Thu, 14 Dec 2023 18:11:11 +0000 Subject: [PATCH 20/29] chore: revert cli change --- tests/CLINode14Test.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/CLINode14Test.php b/tests/CLINode14Test.php index 9986758d1..6b92eb423 100644 --- a/tests/CLINode14Test.php +++ b/tests/CLINode14Test.php @@ -19,7 +19,7 @@ class CLINode14Test extends Base 'cp tests/languages/cli/test.js tests/sdks/cli/test.js' ]; protected string $command = - 'docker run --rm -v $(pwd):/app -w /app/tests/sdks/cli node:14-alpine node test.js'; + 'docker run --network="mockapi" --rm -v $(pwd):/app -w /app/tests/sdks/cli node:14-alpine node test.js'; protected array $expectedOutput = [ ...Base::FOO_RESPONSES, From ffcd357840ea61aa83a33a318eea5d1901e16bec Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Thu, 14 Dec 2023 18:13:24 +0000 Subject: [PATCH 21/29] test: enable node 20 disable 14 --- .github/workflows/tests.yml | 2 +- tests/{Node14Test.php => Node20Test.php} | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) rename tests/{Node14Test.php => Node20Test.php} (81%) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 9fa696df6..41b5e3291 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -13,7 +13,7 @@ jobs: fail-fast: false matrix: php-version: ['8.1'] - sdk: [Android11Java8, Android11Java11, Android12Java8, Android12Java11, CLINode14, CLINode16, DartBeta, DartStable, Deno1193, Deno1303, DotNet60, DotNet70, FlutterStable, FlutterBeta, Go112, Go118, KotlinJava8, KotlinJava11, KotlinJava17, Node14, Node16, Node18, PHP74, PHP80, Python38, Python39, Python310, Ruby27, Ruby30, Ruby31, AppleSwift55, Swift55, WebChromium, WebNode] + sdk: [Android11Java8, Android11Java11, Android12Java8, Android12Java11, CLINode14, CLINode16, DartBeta, DartStable, Deno1193, Deno1303, DotNet60, DotNet70, FlutterStable, FlutterBeta, Go112, Go118, KotlinJava8, KotlinJava11, KotlinJava17, Node16, Node18, Node20, PHP74, PHP80, Python38, Python39, Python310, Ruby27, Ruby30, Ruby31, AppleSwift55, Swift55, WebChromium, WebNode] steps: - name: Checkout repository diff --git a/tests/Node14Test.php b/tests/Node20Test.php similarity index 81% rename from tests/Node14Test.php rename to tests/Node20Test.php index 4e78c97f6..9868117f9 100644 --- a/tests/Node14Test.php +++ b/tests/Node20Test.php @@ -2,7 +2,7 @@ namespace Tests; -class Node14Test extends Base +class Node18Test extends Base { protected string $sdkName = 'node.js'; protected string $sdkPlatform = 'server'; @@ -12,10 +12,10 @@ class Node14Test extends Base protected string $language = 'node'; protected string $class = 'Appwrite\SDK\Language\Node'; protected array $build = [ - 'docker run --rm -v $(pwd):/app -w /app/tests/sdks/node node:14-alpine npm install', + 'docker run --rm -v $(pwd):/app -w /app/tests/sdks/node node:20-alpine npm install', ]; protected string $command = - 'docker run --network="mockapi" --rm -v $(pwd):/app -w /app node:14-alpine node tests/languages/node/test.js'; + 'docker run --network="mockapi" --rm -v $(pwd):/app -w /app node:20-alpine node tests/languages/node/test.js'; protected array $expectedOutput = [ ...Base::FOO_RESPONSES, From db88d1f4a1fcd643c2273a5485c258f594307da4 Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Thu, 14 Dec 2023 18:18:17 +0000 Subject: [PATCH 22/29] chore: rename class --- tests/Node20Test.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Node20Test.php b/tests/Node20Test.php index 9868117f9..ef473edbb 100644 --- a/tests/Node20Test.php +++ b/tests/Node20Test.php @@ -2,7 +2,7 @@ namespace Tests; -class Node18Test extends Base +class Node20Test extends Base { protected string $sdkName = 'node.js'; protected string $sdkPlatform = 'server'; From 30faacbe5747347777c88332c6b19673f96d7e75 Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Mon, 18 Dec 2023 12:17:09 +0000 Subject: [PATCH 23/29] chore: condense throws --- templates/node/lib/client.js.twig | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/templates/node/lib/client.js.twig b/templates/node/lib/client.js.twig index aad8a51ff..a77c32e28 100644 --- a/templates/node/lib/client.js.twig +++ b/templates/node/lib/client.js.twig @@ -112,7 +112,7 @@ class Client { try { const response = await fetch(url.toString(), { - method, + method: method.toUpperCase(), headers, body, dispatcher: new Agent({ @@ -124,21 +124,15 @@ class Client { if (response.headers.get("content-type") === "application/json") { const json = await response.json(); - if (response.status >= 400) { - throw new {{spec.title | caseUcfirst}}Exception(json.message, json.code, json.type, json); - } + if (response.status >= 400) throw new {{spec.title | caseUcfirst}}Exception(json.message, json.code, json.type, json); return json; } else { const text = await response.text(); - if (response.status >= 400) { - throw new {{spec.title | caseUcfirst}}Exception(text, response.status, "", text); - } + if (response.status >= 400) throw new {{spec.title | caseUcfirst}}Exception(text, response.status, "", text); return text; } } catch (error) { - if (!("response" in error) || error.response === undefined) { - throw new {{spec.title | caseUcfirst}}Exception(error.message); - } + if (!(error instanceof {{spec.title | caseUcfirst}}Exception)) throw new {{spec.title | caseUcfirst}}Exception(error.message); throw error; } } From e2e7dd2aff68fcdbd3d462be9f031e325a42583e Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Mon, 18 Dec 2023 14:10:23 +0000 Subject: [PATCH 24/29] fix: tests --- templates/node/lib/client.js.twig | 22 +++++++++++++-------- templates/node/lib/services/service.js.twig | 3 +++ 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/templates/node/lib/client.js.twig b/templates/node/lib/client.js.twig index a77c32e28..f880687cb 100644 --- a/templates/node/lib/client.js.twig +++ b/templates/node/lib/client.js.twig @@ -122,15 +122,21 @@ class Client { }), }); - if (response.headers.get("content-type") === "application/json") { - const json = await response.json(); - if (response.status >= 400) throw new {{spec.title | caseUcfirst}}Exception(json.message, json.code, json.type, json); - return json; - } else { - const text = await response.text(); - if (response.status >= 400) throw new {{spec.title | caseUcfirst}}Exception(text, response.status, "", text); - return text; + const text = await response.text(); + let json = undefined; + try { + json = JSON.parse(text); + } catch (error) { + if (response.status >= 400) { + throw new {{spec.title | caseUcfirst}}Exception(text, response.status, "", text); + } + return response; + } + if (response.status >= 400) { + throw new {{spec.title | caseUcfirst}}Exception(json.message, json.code, json.type, json); } + return json; + } catch (error) { if (!(error instanceof {{spec.title | caseUcfirst}}Exception)) throw new {{spec.title | caseUcfirst}}Exception(error.message); throw error; diff --git a/templates/node/lib/services/service.js.twig b/templates/node/lib/services/service.js.twig index 71da69a55..e6f849c96 100644 --- a/templates/node/lib/services/service.js.twig +++ b/templates/node/lib/services/service.js.twig @@ -26,6 +26,9 @@ class {{ service.name | caseUcfirst }} extends Service { {% for parameter in method.parameters.all %} * @param {{ '{' }}{{ parameter | typeName }}{{ '}' }} {{ parameter.name | caseCamel | escapeKeyword }} {% endfor %} +{% if 'multipart/form-data' in method.consumes %} + * @param {CallableFunction} onProgress +{% endif %} * @throws {{ '{' }}{{ spec.title | caseUcfirst}}Exception} * @returns {Promise} */ From 5a3305442a86f7d1828cce02158f46d4cf7cca3d Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Tue, 26 Dec 2023 13:08:17 +0000 Subject: [PATCH 25/29] chore: remove 'arraybuffer' param --- templates/deno/src/services/service.ts.twig | 2 +- templates/node/base/requests/api.twig | 2 +- templates/node/base/requests/file.twig | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/templates/deno/src/services/service.ts.twig b/templates/deno/src/services/service.ts.twig index 9e3b6da70..f989df67c 100644 --- a/templates/deno/src/services/service.ts.twig +++ b/templates/deno/src/services/service.ts.twig @@ -171,7 +171,7 @@ export class {{ service.name | caseUcfirst }} extends Service { payload['{{ parameter.name }}'] = { type: 'file', file: new File([uploadableChunkTrimmed], {{ parameter.name | caseCamel | escapeKeyword }}.filename), filename: {{ parameter.name | caseCamel | escapeKeyword }}.filename }; - response = await this.client.call('{{ method.method | caseLower }}', apiPath, apiHeaders, payload{% if method.type == 'location' %}, 'arraybuffer'{% endif %}); + response = await this.client.call('{{ method.method | caseLower }}', apiPath, apiHeaders, payload); if (!id) { id = response['$id']; diff --git a/templates/node/base/requests/api.twig b/templates/node/base/requests/api.twig index 4c578a889..a327c609a 100644 --- a/templates/node/base/requests/api.twig +++ b/templates/node/base/requests/api.twig @@ -5,4 +5,4 @@ {% for key, header in method.headers %} '{{ key }}': '{{ header }}', {% endfor %} - }, payload{% if method.type == 'location' %}, 'arraybuffer'{% endif %}); \ No newline at end of file + }, payload); \ No newline at end of file diff --git a/templates/node/base/requests/file.twig b/templates/node/base/requests/file.twig index 76a20f0d7..74aae23ac 100644 --- a/templates/node/base/requests/file.twig +++ b/templates/node/base/requests/file.twig @@ -63,7 +63,7 @@ payload['{{ parameter.name }}'] = { type: 'file', file: new File([uploadableChunkTrimmed], {{ parameter.name | caseCamel | escapeKeyword }}.filename), filename: {{ parameter.name | caseCamel | escapeKeyword }}.filename }; - response = await this.client.call('{{ method.method | caseLower }}', apiPath, apiHeaders, payload{% if method.type == 'location' %}, 'arraybuffer'{% endif %}); + response = await this.client.call('{{ method.method | caseLower }}', apiPath, apiHeaders, payload); if (!id) { id = response['$id']; From 67a67dc5944ecbaa220761fef73f933257a71ece Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Tue, 26 Dec 2023 14:51:33 +0000 Subject: [PATCH 26/29] Revert "chore: remove 'arraybuffer' param" This reverts commit 5a3305442a86f7d1828cce02158f46d4cf7cca3d. --- templates/deno/src/services/service.ts.twig | 2 +- templates/node/base/requests/api.twig | 2 +- templates/node/base/requests/file.twig | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/templates/deno/src/services/service.ts.twig b/templates/deno/src/services/service.ts.twig index f989df67c..9e3b6da70 100644 --- a/templates/deno/src/services/service.ts.twig +++ b/templates/deno/src/services/service.ts.twig @@ -171,7 +171,7 @@ export class {{ service.name | caseUcfirst }} extends Service { payload['{{ parameter.name }}'] = { type: 'file', file: new File([uploadableChunkTrimmed], {{ parameter.name | caseCamel | escapeKeyword }}.filename), filename: {{ parameter.name | caseCamel | escapeKeyword }}.filename }; - response = await this.client.call('{{ method.method | caseLower }}', apiPath, apiHeaders, payload); + response = await this.client.call('{{ method.method | caseLower }}', apiPath, apiHeaders, payload{% if method.type == 'location' %}, 'arraybuffer'{% endif %}); if (!id) { id = response['$id']; diff --git a/templates/node/base/requests/api.twig b/templates/node/base/requests/api.twig index a327c609a..4c578a889 100644 --- a/templates/node/base/requests/api.twig +++ b/templates/node/base/requests/api.twig @@ -5,4 +5,4 @@ {% for key, header in method.headers %} '{{ key }}': '{{ header }}', {% endfor %} - }, payload); \ No newline at end of file + }, payload{% if method.type == 'location' %}, 'arraybuffer'{% endif %}); \ No newline at end of file diff --git a/templates/node/base/requests/file.twig b/templates/node/base/requests/file.twig index 74aae23ac..76a20f0d7 100644 --- a/templates/node/base/requests/file.twig +++ b/templates/node/base/requests/file.twig @@ -63,7 +63,7 @@ payload['{{ parameter.name }}'] = { type: 'file', file: new File([uploadableChunkTrimmed], {{ parameter.name | caseCamel | escapeKeyword }}.filename), filename: {{ parameter.name | caseCamel | escapeKeyword }}.filename }; - response = await this.client.call('{{ method.method | caseLower }}', apiPath, apiHeaders, payload); + response = await this.client.call('{{ method.method | caseLower }}', apiPath, apiHeaders, payload{% if method.type == 'location' %}, 'arraybuffer'{% endif %}); if (!id) { id = response['$id']; From 5a3a672fb76317de298f76d52788eb4c550aba83 Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Tue, 26 Dec 2023 15:20:01 +0000 Subject: [PATCH 27/29] fix: arraybuffer responses --- templates/deno/src/client.ts.twig | 58 +++++++++++++------------- templates/node/lib/client.js.twig | 67 ++++++++++++++++++------------- 2 files changed, 67 insertions(+), 58 deletions(-) diff --git a/templates/deno/src/client.ts.twig b/templates/deno/src/client.ts.twig index 3f044a063..365d1941f 100644 --- a/templates/deno/src/client.ts.twig +++ b/templates/deno/src/client.ts.twig @@ -61,7 +61,7 @@ export class Client { return this; } - async call(method: string, path: string = "", headers: Payload = {}, params: Payload = {}) { + async call(method: string, path: string = "", headers: Payload = {}, params: Payload = {}, responseType: string = "json") { headers = {...this.headers, ...headers}; const url = new URL(this.endpoint + path); @@ -88,41 +88,41 @@ export class Client { body = JSON.stringify(params); } + let response = undefined; try { - const response = await fetch(url.toString(), { - method, + response = await fetch(url.toString(), { + method: method.toUpperCase(), headers, - body, + body }); - const contentType = response.headers.get("content-type"); - - if (contentType && contentType.includes("application/json")) { - if (response.status >= 400) { - const json = await response.json(); - throw new {{ spec.title | caseUcfirst}}Exception( - json.message, - json.status, - json.type ?? "", - json - ); - } + } catch (error) { + throw new {{spec.title | caseUcfirst}}Exception(error.message); + } - return response.json(); - } else { - if (response.status >= 400) { - const text = await response.text(); - throw new {{ spec.title | caseUcfirst}}Exception(text, response.status, "", null); - } - return response; + if (response.status >= 400) { + const text = await response.text(); + let json = undefined; + try { + json = JSON.parse(text); + } catch (error) { + throw new {{spec.title | caseUcfirst}}Exception(text, response.status, "", text); } + throw new {{spec.title | caseUcfirst}}Exception(json.message, json.code, json.type, json); + } + + if (responseType === "arraybuffer") { + const data = await response.arrayBuffer(); + return data; + } + + const text = await response.text(); + let json = undefined; + try { + json = JSON.parse(text); } catch (error) { - throw new {{ spec.title | caseUcfirst}}Exception( - error?.response?.message || error.message, - error?.response?.code, - error?.response?.type, - error.response - ); + return text; } + return json; } static flatten(data: Payload, prefix = '') { diff --git a/templates/node/lib/client.js.twig b/templates/node/lib/client.js.twig index f880687cb..3becdc66e 100644 --- a/templates/node/lib/client.js.twig +++ b/templates/node/lib/client.js.twig @@ -83,7 +83,7 @@ class Client { return this; } - async call(method, path = "", headers = {}, params = {}) { + async call(method, path = "", headers = {}, params = {}, responseType = "json") { headers = {...this.headers, ...headers}; const url = new URL(this.endpoint + path); @@ -110,37 +110,46 @@ class Client { body = JSON.stringify(params); } + let response = undefined; + try { + response = await fetch(url.toString(), { + method: method.toUpperCase(), + headers, + body, + dispatcher: new Agent({ + connect: { + rejectUnauthorized: !this.selfSigned, + }, + }), + }); + } catch (error) { + throw new {{spec.title | caseUcfirst}}Exception(error.message); + } + + if (response.status >= 400) { + const text = await response.text(); + let json = undefined; try { - const response = await fetch(url.toString(), { - method: method.toUpperCase(), - headers, - body, - dispatcher: new Agent({ - connect: { - rejectUnauthorized: !this.selfSigned, - }, - }), - }); - - const text = await response.text(); - let json = undefined; - try { - json = JSON.parse(text); - } catch (error) { - if (response.status >= 400) { - throw new {{spec.title | caseUcfirst}}Exception(text, response.status, "", text); - } - return response; - } - if (response.status >= 400) { - throw new {{spec.title | caseUcfirst}}Exception(json.message, json.code, json.type, json); - } - return json; - + json = JSON.parse(text); } catch (error) { - if (!(error instanceof {{spec.title | caseUcfirst}}Exception)) throw new {{spec.title | caseUcfirst}}Exception(error.message); - throw error; + throw new {{spec.title | caseUcfirst}}Exception(text, response.status, "", text); } + throw new {{spec.title | caseUcfirst}}Exception(json.message, json.code, json.type, json); + } + + if (responseType === "arraybuffer") { + const data = await response.arrayBuffer(); + return data; + } + + const text = await response.text(); + let json = undefined; + try { + json = JSON.parse(text); + } catch (error) { + return text; + } + return json; } static flatten(data, prefix = '') { From 379ff46723bc38ee09bf4fba6f220763fbf03fe0 Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Tue, 26 Dec 2023 22:00:54 +0000 Subject: [PATCH 28/29] feat: docstrings for InputFile class --- templates/node/index.d.ts.twig | 2 +- templates/node/lib/inputFile.js.twig | 81 ++++++++++++++++++++++------ 2 files changed, 67 insertions(+), 16 deletions(-) diff --git a/templates/node/index.d.ts.twig b/templates/node/index.d.ts.twig index 721eae574..eb6680162 100644 --- a/templates/node/index.d.ts.twig +++ b/templates/node/index.d.ts.twig @@ -250,7 +250,7 @@ declare module "{{ language.params.npmPackage|caseDash }}" { * @throws {{ '{' }}{{ spec.title | caseUcfirst}}Exception} * @returns {Promise} */ - {{ method.name | caseCamel }}{% if generics %}<{{generics}}>{% endif %}({% for parameter in method.parameters.all %}{{ parameter.name | caseCamel | escapeKeyword }}{% if not parameter.required or parameter.nullable %}?{% endif %}: {{ parameter | typeName }}{% if not loop.last %}, {% endif %}{% endfor %}): Promise<{% if method.type == 'location' %}Buffer{% else %}{% if method.responseModel and method.responseModel != 'any' %}{% if not spec.definitions[method.responseModel].additionalProperties %}Models.{% endif %}{{method.responseModel | caseUcfirst}}{% if generics_return %}<{{generics_return}}>{% endif %}{% else %}{% if method.method == 'delete' %}string{% else %}any{% endif %}{% endif %}{% endif %}>; + {{ method.name | caseCamel }}{% if generics %}<{{generics}}>{% endif %}({% for parameter in method.parameters.all %}{{ parameter.name | caseCamel | escapeKeyword }}{% if not parameter.required or parameter.nullable %}?{% endif %}: {{ parameter | typeName }}{% if not loop.last %}, {% endif %}{% endfor %}): Promise<{% if method.type == 'location' %}ArrayBuffer{% else %}{% if method.responseModel and method.responseModel != 'any' %}{% if not spec.definitions[method.responseModel].additionalProperties %}Models.{% endif %}{{method.responseModel | caseUcfirst}}{% if generics_return %}<{{generics_return}}>{% endif %}{% else %}{% if method.method == 'delete' %}string{% else %}any{% endif %}{% endif %}{% endif %}>; {% endfor %} } {% endfor %} diff --git a/templates/node/lib/inputFile.js.twig b/templates/node/lib/inputFile.js.twig index b2d71d774..d5af0cca8 100644 --- a/templates/node/lib/inputFile.js.twig +++ b/templates/node/lib/inputFile.js.twig @@ -1,40 +1,91 @@ -const { readFileSync } = require("fs"); +const fs = require("fs"); const { ReadableStream } = require("stream/web"); -const _bufferToString = (buffer) => { +/** + * @param {fs.ReadStream} readStream + * @returns {ReadableStream} + */ +function convertReadStreamToReadableStream(readStream) { + return new ReadableStream({ + start(controller) { + readStream.on("data", (chunk) => { + controller.enqueue(chunk); + }); + readStream.on("end", () => { + controller.close(); + }); + readStream.on("error", (err) => { + controller.error(err); + }); + }, + cancel() { + readStream.destroy(); + }, + }); +} + +/** + * @param {Buffer} buffer + * @returns {ReadableStream} + */ +function bufferToReadableStream(buffer) { return new ReadableStream({ start(controller) { controller.enqueue(buffer); controller.close(); }, }); -}; +} class InputFile { - stream; // Content of file as a stream - size; // Total final size of the file content - filename; // File name + /** @type {ReadableStream} Content of file as a stream */ + stream; + + /** @type {number} Total final size of the file content */ + size; + /** @type {string} File name */ + filename; + + /** + * @param {string} filePath + * @param {string} filename + * @returns {InputFile} + */ static fromPath = (filePath, filename) => { - const data = readFileSync(filePath); - const stream = _bufferToString(data); - const size = data.byteLength; + const nodeStream = fs.createReadStream(filePath); + const stream = convertReadStreamToReadableStream(nodeStream); + const size = fs.statSync(filePath).size; return new InputFile(stream, filename, size); }; + /** + * @param {Buffer} buffer + * @param {string} filename + * @returns {InputFile} + */ static fromBuffer = (buffer, filename) => { - const stream = _bufferToString(buffer); + const stream = bufferToReadableStream(buffer); const size = buffer.byteLength; return new InputFile(stream, filename, size); }; + /** + * @param {string} content + * @param {string} filename + * @returns {InputFile} + */ static fromPlainText = (content, filename) => { - const buffer = new TextEncoder().encode(content); - const stream = _bufferToString(buffer); - const size = buffer.byteLength; - return new InputFile(stream, filename, size); + const array = new TextEncoder().encode(content); + const buffer = Buffer.from(array); + return InputFile.fromBuffer(buffer, filename); }; + /** + * @param {ReadableStream} stream + * @param {string} filename + * @param {number} size + */ constructor(stream, filename, size) { this.stream = stream; this.filename = filename; @@ -42,4 +93,4 @@ class InputFile { } } -module.exports = InputFile; \ No newline at end of file +module.exports = InputFile; From b9340ede798fceebacfdf0ee01663dc79dc7ef1a Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Tue, 26 Dec 2023 22:10:00 +0000 Subject: [PATCH 29/29] fix: inputFile types --- templates/node/lib/inputFile.js.twig | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/templates/node/lib/inputFile.js.twig b/templates/node/lib/inputFile.js.twig index d5af0cca8..5790b8b41 100644 --- a/templates/node/lib/inputFile.js.twig +++ b/templates/node/lib/inputFile.js.twig @@ -81,6 +81,27 @@ class InputFile { return InputFile.fromBuffer(buffer, filename); }; + /** + * @param {ReadableStream} stream + * @param {string} filename + * @param {number} size + * @returns {InputFile} + */ + static fromStream = (stream, filename, size) => { + return new InputFile(stream, filename, size); + }; + + /** + * @param {Blob} blob + * @param {string} filename + * @returns {InputFile} + */ + static fromBlob = (blob, filename) => { + const stream = blob.stream(); + const size = blob.size; + return new InputFile(stream, filename, size); + }; + /** * @param {ReadableStream} stream * @param {string} filename