Skip to content

Commit 4bece60

Browse files
authored
Merge pull request #725 from appwrite/feat-ssr
feat: ssr
2 parents 2e3d7b7 + b9340ed commit 4bece60

File tree

11 files changed

+256
-208
lines changed

11 files changed

+256
-208
lines changed

.github/workflows/tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ jobs:
1313
fail-fast: false
1414
matrix:
1515
php-version: ['8.1']
16-
sdk: [Android11Java8, Android11Java11, Android12Java8, Android12Java11, CLINode16, CLINode18, 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]
16+
sdk: [Android11Java8, Android11Java11, Android12Java8, Android12Java11, CLINode16, CLINode18, 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]
1717

1818
steps:
1919
- name: Checkout repository

templates/deno/src/client.ts.twig

Lines changed: 47 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -61,78 +61,79 @@ export class Client {
6161
return this;
6262
}
6363

64-
withoutHeader(key: string, headers: Payload): Payload {
65-
return Object.keys(headers).reduce((acc: Payload, cv: string) => {
66-
if (cv === 'content-type') return acc;
67-
acc[cv] = headers[cv];
68-
return acc;
69-
}, {})
70-
}
64+
async call(method: string, path: string = "", headers: Payload = {}, params: Payload = {}, responseType: string = "json") {
65+
headers = {...this.headers, ...headers};
66+
const url = new URL(this.endpoint + path);
7167

72-
async call(method: string, path: string = '', headers: Payload = {}, params: Payload = {}) {
73-
headers = Object.assign({}, this.headers, headers);
68+
let body: string | FormData | undefined = undefined;
7469

75-
let body;
76-
const url = new URL(this.endpoint + path);
77-
if (method.toUpperCase() === 'GET') {
78-
url.search = new URLSearchParams(this.flatten(params)).toString();
79-
body = null;
80-
} else if (headers['content-type'].toLowerCase().startsWith('multipart/form-data')) {
81-
headers = this.withoutHeader('content-type', headers);
70+
if (method.toUpperCase() === "GET") {
71+
url.search = new URLSearchParams(Client.flatten(params)).toString();
72+
} else if (headers["content-type"]?.toLowerCase().startsWith("multipart/form-data")) {
73+
delete headers["content-type"];
8274
const formData = new FormData();
83-
const flatParams = this.flatten(params);
84-
for (const key in flatParams) {
85-
const value = flatParams[key];
8675

87-
if(value && value.type && value.type === 'file') {
76+
const flatParams = Client.flatten(params);
77+
78+
for (const [key, value] of Object.entries(flatParams)) {
79+
if (value && value.type && value.type === "file") {
8880
formData.append(key, value.file, value.filename);
8981
} else {
90-
formData.append(key, flatParams[key]);
82+
formData.append(key, value);
9183
}
9284
}
85+
9386
body = formData;
9487
} else {
9588
body = JSON.stringify(params);
9689
}
9790

98-
const options = {
99-
method: method.toUpperCase(),
100-
headers: headers,
101-
body: body,
102-
};
103-
91+
let response = undefined;
10492
try {
105-
let response = await fetch(url.toString(), options);
106-
const contentType = response.headers.get('content-type');
107-
108-
if (contentType && contentType.includes('application/json')) {
109-
if (response.status >= 400) {
110-
let res = await response.json();
111-
throw new {{ spec.title | caseUcfirst}}Exception(res.message, res.status, res.type ?? "", res);
112-
}
93+
response = await fetch(url.toString(), {
94+
method: method.toUpperCase(),
95+
headers,
96+
body
97+
});
98+
} catch (error) {
99+
throw new {{spec.title | caseUcfirst}}Exception(error.message);
100+
}
113101

114-
return response.json();
115-
} else {
116-
if (response.status >= 400) {
117-
let res = await response.text();
118-
throw new {{ spec.title | caseUcfirst}}Exception(res, response.status, "", null);
119-
}
120-
return response;
102+
if (response.status >= 400) {
103+
const text = await response.text();
104+
let json = undefined;
105+
try {
106+
json = JSON.parse(text);
107+
} catch (error) {
108+
throw new {{spec.title | caseUcfirst}}Exception(text, response.status, "", text);
121109
}
122-
} catch(error) {
123-
throw new {{ spec.title | caseUcfirst}}Exception(error?.response?.message || error.message, error?.response?.code, error?.response?.type, error.response);
110+
throw new {{spec.title | caseUcfirst}}Exception(json.message, json.code, json.type, json);
111+
}
112+
113+
if (responseType === "arraybuffer") {
114+
const data = await response.arrayBuffer();
115+
return data;
116+
}
117+
118+
const text = await response.text();
119+
let json = undefined;
120+
try {
121+
json = JSON.parse(text);
122+
} catch (error) {
123+
return text;
124124
}
125+
return json;
125126
}
126127

127-
flatten(data: Payload, prefix = '') {
128+
static flatten(data: Payload, prefix = '') {
128129
let output: Payload = {};
129130

130131
for (const key in data) {
131132
let value = data[key];
132133
let finalKey = prefix ? prefix + '[' + key +']' : key;
133134

134135
if (Array.isArray(value)) {
135-
output = { ...output, ...this.flatten(value, finalKey) }; // @todo: handle name collision here if needed
136+
output = { ...output, ...Client.flatten(value, finalKey) }; // @todo: handle name collision here if needed
136137
}
137138
else {
138139
output[finalKey] = value;

templates/deno/src/inputFile.ts.twig

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,12 @@ export class InputFile {
1919
return new InputFile(stream, filename, size);
2020
};
2121

22+
static fromBlob = async (blob: Blob, filename: string) => {
23+
const arrayBuffer = await blob.arrayBuffer();
24+
const buffer = new Uint8Array(arrayBuffer);
25+
return InputFile.fromBuffer(buffer, filename);
26+
};
27+
2228
static fromBuffer = (buffer: Uint8Array, filename: string): InputFile => {
2329
const stream = _bufferToString(buffer);
2430
const size = buffer.byteLength;
Lines changed: 43 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
{% for parameter in method.parameters.all %}
22
{% if parameter.type == 'file' %}
3-
const size = {{ parameter.name | caseCamel | escapeKeyword }}.size;
43

4+
const size = {{ parameter.name | caseCamel | escapeKeyword }}.size;
5+
56
const apiHeaders = {
67
{% for parameter in method.parameters.header %}
78
'{{ parameter.name }}': ${{ parameter.name | caseCamel | escapeKeyword }},
@@ -28,117 +29,77 @@
2829
{% endif %}
2930
{% endfor %}
3031

31-
let currentChunk = Buffer.from('');
32-
let currentChunkSize = 0;
33-
let currentChunkStart = 0;
32+
let currentChunk = 1;
33+
let currentPosition = 0;
34+
let uploadableChunk = new Uint8Array(client.CHUNK_SIZE);
35+
3436

35-
const selfClient = this.client;
36-
37-
async function uploadChunk(lastUpload = false) {
38-
if(chunksUploaded - 1 >= currentChunkStart / client.CHUNK_SIZE) {
37+
const uploadChunk = async (lastUpload = false) => {
38+
if(currentChunk <= chunksUploaded) {
3939
return;
4040
}
41-
42-
const start = currentChunkStart;
43-
const end = currentChunkStart + currentChunkSize - 1;
4441

45-
if(!lastUpload || currentChunkStart !== 0) {
42+
const start = ((currentChunk - 1) * client.CHUNK_SIZE);
43+
let end = start + currentPosition - 1;
44+
45+
if(!lastUpload || currentChunk !== 1) {
4646
apiHeaders['content-range'] = 'bytes ' + start + '-' + end + '/' + size;
4747
}
4848

49+
let uploadableChunkTrimmed;
50+
51+
if(currentPosition + 1 >= client.CHUNK_SIZE) {
52+
uploadableChunkTrimmed = uploadableChunk;
53+
} else {
54+
uploadableChunkTrimmed = new Uint8Array(currentPosition);
55+
for(let i = 0; i <= currentPosition; i++) {
56+
uploadableChunkTrimmed[i] = uploadableChunk[i];
57+
}
58+
}
59+
4960
if (id) {
5061
apiHeaders['x-{{spec.title | caseLower }}-id'] = id;
5162
}
5263

53-
payload['{{ parameter.name }}'] = {
54-
type: 'file',
55-
file: currentChunk,
56-
filename: {{ parameter.name }}.filename,
57-
size: currentChunkSize
58-
};
64+
payload['{{ parameter.name }}'] = { type: 'file', file: new File([uploadableChunkTrimmed], {{ parameter.name | caseCamel | escapeKeyword }}.filename), filename: {{ parameter.name | caseCamel | escapeKeyword }}.filename };
5965

60-
response = await selfClient.call('{{ method.method | caseLower }}', apiPath, apiHeaders, payload{% if method.type == 'location' %}, 'arraybuffer'{% endif %});
66+
response = await this.client.call('{{ method.method | caseLower }}', apiPath, apiHeaders, payload{% if method.type == 'location' %}, 'arraybuffer'{% endif %});
6167

6268
if (!id) {
6369
id = response['$id'];
6470
}
65-
71+
6672
if (onProgress !== null) {
6773
onProgress({
6874
$id: response['$id'],
69-
progress: Math.min((start+client.CHUNK_SIZE) * client.CHUNK_SIZE, size) / size * 100,
75+
progress: Math.min((currentChunk) * client.CHUNK_SIZE, size) / size * 100,
7076
sizeUploaded: end+1,
7177
chunksTotal: response['chunksTotal'],
7278
chunksUploaded: response['chunksUploaded']
7379
});
7480
}
7581

76-
currentChunkStart += client.CHUNK_SIZE;
82+
uploadableChunk = new Uint8Array(client.CHUNK_SIZE);
83+
currentChunk++;
84+
currentPosition = 0;
7785
}
7886

79-
return await new Promise((resolve, reject) => {
80-
const writeStream = new Stream.Writable();
81-
writeStream._write = async (mainChunk, encoding, callback) => {
82-
try {
83-
// Segment incoming chunk into up to 5MB chunks
84-
const mainChunkSize = Buffer.byteLength(mainChunk);
85-
const chunksCount = Math.ceil(mainChunkSize / client.CHUNK_SIZE);
86-
const chunks = [];
87-
88-
for(let i = 0; i < chunksCount; i++) {
89-
const chunk = mainChunk.slice(i * client.CHUNK_SIZE, (i + 1) * client.CHUNK_SIZE);
90-
chunks.push(chunk);
91-
}
92-
93-
for (const chunk of chunks) {
94-
const chunkSize = Buffer.byteLength(chunk);
95-
96-
if(chunkSize + currentChunkSize == client.CHUNK_SIZE) {
97-
// Upload chunk
98-
currentChunk = Buffer.concat([currentChunk, chunk]);
99-
currentChunkSize = Buffer.byteLength(currentChunk);
100-
await uploadChunk();
101-
currentChunk = Buffer.from('');
102-
currentChunkSize = 0;
103-
} else if(chunkSize + currentChunkSize > client.CHUNK_SIZE) {
104-
// Upload chunk, put rest into next chunk
105-
const bytesToUpload = client.CHUNK_SIZE - currentChunkSize;
106-
const newChunkSection = chunk.slice(0, bytesToUpload);
107-
currentChunk = Buffer.concat([currentChunk, newChunkSection]);
108-
currentChunkSize = Buffer.byteLength(currentChunk);
109-
await uploadChunk();
110-
currentChunk = chunk.slice(bytesToUpload, undefined);
111-
currentChunkSize = chunkSize - bytesToUpload;
112-
} else {
113-
// Append into current chunk
114-
currentChunk = Buffer.concat([currentChunk, chunk]);
115-
currentChunkSize = chunkSize + currentChunkSize;
116-
}
117-
}
118-
119-
callback();
120-
} catch (e) {
121-
callback(e);
87+
for await (const chunk of {{ parameter.name | caseCamel | escapeKeyword }}.stream) {
88+
for(const b of chunk) {
89+
uploadableChunk[currentPosition] = b;
90+
91+
currentPosition++;
92+
if(currentPosition >= client.CHUNK_SIZE) {
93+
await uploadChunk();
94+
currentPosition = 0;
12295
}
12396
}
97+
}
12498

125-
writeStream.on("finish", async () => {
126-
if(currentChunkSize > 0) {
127-
try {
128-
await uploadChunk(true);
129-
} catch (e) {
130-
reject(e);
131-
}
132-
}
133-
134-
resolve(response);
135-
});
99+
if (currentPosition > 0) { // Check if there's any remaining data for the last chunk
100+
await uploadChunk(true);
101+
}
136102

137-
writeStream.on("error", (err) => {
138-
reject(err);
139-
});
140-
141-
{{ parameter.name | caseCamel | escapeKeyword }}.stream.pipe(writeStream);
142-
});
103+
return response;
143104
{% endif %}
144105
{% endfor %}

templates/node/index.d.ts.twig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,7 @@ declare module "{{ language.params.npmPackage|caseDash }}" {
250250
* @throws {{ '{' }}{{ spec.title | caseUcfirst}}Exception}
251251
* @returns {Promise}
252252
*/
253-
{{ 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 %}>;
253+
{{ 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 %}>;
254254
{% endfor %}
255255
}
256256
{% endfor %}

0 commit comments

Comments
 (0)