Skip to content

Commit

Permalink
SDK binary support for executions
Browse files Browse the repository at this point in the history
  • Loading branch information
Meldiron committed Sep 18, 2024
1 parent 68639f9 commit b8bd37b
Show file tree
Hide file tree
Showing 21 changed files with 412 additions and 337 deletions.
2 changes: 1 addition & 1 deletion docs/examples/functions/create-deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const functions = new sdk.Functions(client);

const result = await functions.createDeployment(
'<FUNCTION_ID>', // functionId
InputFile.fromPath('/path/to/file', 'filename'), // code
Payload.fromBinary(fs.readFileSync('/path/to/file.png'), 'file.png'), // code
false, // activate
'<ENTRYPOINT>', // entrypoint (optional)
'<COMMANDS>' // commands (optional)
Expand Down
3 changes: 2 additions & 1 deletion docs/examples/functions/create-execution.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const sdk = require('node-appwrite');
const fs = require('fs');

const client = new sdk.Client()
.setEndpoint('https://cloud.appwrite.io/v1') // Your API Endpoint
Expand All @@ -9,7 +10,7 @@ const functions = new sdk.Functions(client);

const result = await functions.createExecution(
'<FUNCTION_ID>', // functionId
'<BODY>', // body (optional)
Payload.fromJson({ x: "y" }), // body (optional)
false, // async (optional)
'<PATH>', // path (optional)
sdk.ExecutionMethod.GET, // method (optional)
Expand Down
2 changes: 1 addition & 1 deletion docs/examples/storage/create-file.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@ const storage = new sdk.Storage(client);
const result = await storage.createFile(
'<BUCKET_ID>', // bucketId
'<FILE_ID>', // fileId
InputFile.fromPath('/path/to/file', 'filename'), // file
Payload.fromBinary(fs.readFileSync('/path/to/file.png'), 'file.png'), // file
["read("any")"] // permissions (optional)
);
15 changes: 3 additions & 12 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "node-appwrite",
"homepage": "https://appwrite.io/support",
"description": "Appwrite is an open-source self-hosted backend server that abstract and simplify complex and repetitive development tasks behind a very simple REST API",
"version": "14.1.0",
"version": "15.0.0-rc1",
"license": "BSD-3-Clause",
"main": "dist/index.js",
"type": "commonjs",
Expand All @@ -19,16 +19,6 @@
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
},
"./file": {
"import": {
"types": "./dist/inputFile.d.mts",
"default": "./dist/inputFile.mjs"
},
"require": {
"types": "./dist/inputFile.d.ts",
"default": "./dist/inputFile.js"
}
}
},
"files": [
Expand All @@ -48,6 +38,7 @@
"typescript": "5.4.2"
},
"dependencies": {
"node-fetch-native-with-agent": "1.7.2"
"node-fetch-native-with-agent": "1.7.2",
"parse-multipart-data": "^1.5.0"
}
}
78 changes: 62 additions & 16 deletions src/client.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { fetch, FormData, File } from 'node-fetch-native-with-agent';
import { fetch, FormData, Blob } from 'node-fetch-native-with-agent';
import { createAgent } from 'node-fetch-native-with-agent/agent';
import { Models } from './models';
import { Payload } from './payload';
import * as multipart from 'parse-multipart-data';

type Payload = {
type Params = {
[key: string]: any;
}

Expand Down Expand Up @@ -33,7 +35,7 @@ class AppwriteException extends Error {
}

function getUserAgent() {
let ua = 'AppwriteNodeJSSDK/14.1.0';
let ua = 'AppwriteNodeJSSDK/15.0.0-rc1';

// `process` is a global in Node.js, but not fully available in all runtimes.
const platform: string[] = [];
Expand Down Expand Up @@ -82,7 +84,7 @@ class Client {
'x-sdk-name': 'Node.js',
'x-sdk-platform': 'server',
'x-sdk-language': 'nodejs',
'x-sdk-version': '14.1.0',
'x-sdk-version': '15.0.0-rc1',
'user-agent' : getUserAgent(),
'X-Appwrite-Response-Format': '1.6.0',
};
Expand Down Expand Up @@ -217,7 +219,7 @@ class Client {
return this;
}

prepareRequest(method: string, url: URL, headers: Headers = {}, params: Payload = {}): { uri: string, options: RequestInit } {
prepareRequest(method: string, url: URL, headers: Headers = {}, params: Params = {}): { uri: string, options: RequestInit } {
method = method.toUpperCase();

headers = Object.assign({}, this.headers, headers);
Expand All @@ -242,8 +244,8 @@ class Client {
const formData = new FormData();

for (const [key, value] of Object.entries(params)) {
if (value instanceof File) {
formData.append(key, value, value.name);
if (value instanceof Payload) {
formData.append(key, new Blob([value.toBinary()]), value.filename);
} else if (Array.isArray(value)) {
for (const nestedValue of value) {
formData.append(`${key}[]`, nestedValue);
Expand All @@ -255,15 +257,26 @@ class Client {

options.body = formData;
delete headers['content-type'];
headers['accept'] = 'multipart/form-data';
break;
}
}

return { uri: url.toString(), options };
}

async chunkedUpload(method: string, url: URL, headers: Headers = {}, originalPayload: Payload = {}, onProgress: (progress: UploadProgress) => void) {
const file = Object.values(originalPayload).find((value) => value instanceof File);
async chunkedUpload(method: string, url: URL, headers: Headers = {}, originalPayload: Params = {}, onProgress: (progress: UploadProgress) => void) {
let file;
for (const value of Object.values(originalPayload)) {
if (value instanceof Payload) {
file = value;
break;
}
}

if (!file) {
throw new Error('No payload found in params');
}

if (file.size <= Client.CHUNK_SIZE) {
return await this.call(method, url, headers, originalPayload);
Expand All @@ -279,9 +292,9 @@ class Client {
}

headers['content-range'] = `bytes ${start}-${end-1}/${file.size}`;
const chunk = file.slice(start, end);
const chunk = file.toBinary(start, end - start);

let payload = { ...originalPayload, file: new File([chunk], file.name)};
let payload = { ...originalPayload, file: new Payload(Buffer.from(chunk), file.filename)};

response = await this.call(method, url, headers, payload);

Expand All @@ -305,7 +318,7 @@ class Client {
return response;
}

async redirect(method: string, url: URL, headers: Headers = {}, params: Payload = {}): Promise<string> {
async redirect(method: string, url: URL, headers: Headers = {}, params: Params = {}): Promise<string> {
const { uri, options } = this.prepareRequest(method, url, headers, params);

const response = await fetch(uri, {
Expand All @@ -320,7 +333,7 @@ class Client {
return response.headers.get('location') || '';
}

async call(method: string, url: URL, headers: Headers = {}, params: Payload = {}, responseType = 'json'): Promise<any> {
async call(method: string, url: URL, headers: Headers = {}, params: Params = {}, responseType = 'json'): Promise<any> {
const { uri, options } = this.prepareRequest(method, url, headers, params);

let data: any = null;
Expand All @@ -336,6 +349,39 @@ class Client {
data = await response.json();
} else if (responseType === 'arrayBuffer') {
data = await response.arrayBuffer();
} else if (response.headers.get('content-type')?.includes('multipart/form-data')) {
const chunks = [];
for await (const chunk of (response.body as AsyncIterable<any>)) {
chunks.push(chunk instanceof Buffer ? chunk : Buffer.from(chunk));
}
const body = Buffer.concat(chunks);
const boundary = multipart.getBoundary(
response.headers.get("content-type") || ""
);
const parts = multipart.parse(body, boundary);
const partsObject: { [key: string]: any } = {};

for (const part of parts) {
if (!part.name) {
continue;
}
if (part.name === "responseBody") {
partsObject[part.name] = Payload.fromBinary(part.data, part.filename);
} else if (part.name === "responseStatusCode") {
partsObject[part.name] = parseInt(part.data.toString());
} else if (part.name === "duration") {
partsObject[part.name] = parseFloat(part.data.toString());
} else if (part.type === 'application/json') {
try {
partsObject[part.name] = JSON.parse(part.data.toString());
} catch (e) {
throw new Error(`Error parsing JSON for part ${part.name}: ${e instanceof Error ? e.message : 'Unknown error'}`);
}
} else {
partsObject[part.name] = part.data.toString();
}
}
data = partsObject;
} else {
data = {
message: await response.text()
Expand All @@ -349,8 +395,8 @@ class Client {
return data;
}

static flatten(data: Payload, prefix = ''): Payload {
let output: Payload = {};
static flatten(data: Params, prefix = ''): Params {
let output: Params = {};

for (const [key, value] of Object.entries(data)) {
let finalKey = prefix ? prefix + '[' + key +']' : key;
Expand All @@ -367,5 +413,5 @@ class Client {

export { Client, AppwriteException };
export { Query } from './query';
export type { Models, Payload, UploadProgress };
export type { Models, Params, UploadProgress };
export type { QueryTypes, QueryTypesList } from './query';
3 changes: 3 additions & 0 deletions src/enums/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ export enum Runtime {
Python311 = 'python-3.11',
Python312 = 'python-3.12',
Pythonml311 = 'python-ml-3.11',
Deno121 = 'deno-1.21',
Deno124 = 'deno-1.24',
Deno135 = 'deno-1.35',
Deno140 = 'deno-1.40',
Dart215 = 'dart-2.15',
Dart216 = 'dart-2.16',
Expand Down
3 changes: 2 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@ export { Messaging } from './services/messaging';
export { Storage } from './services/storage';
export { Teams } from './services/teams';
export { Users } from './services/users';
export type { Models, Payload, UploadProgress } from './client';
export type { Models, Params, UploadProgress } from './client';
export type { QueryTypes, QueryTypesList } from './query';
export { Permission } from './permission';
export { Role } from './role';
export { ID } from './id';
export { Payload } from './payload';
export { AuthenticatorType } from './enums/authenticator-type';
export { AuthenticationFactor } from './enums/authentication-factor';
export { OAuthProvider } from './enums/o-auth-provider';
Expand Down
23 changes: 0 additions & 23 deletions src/inputFile.ts

This file was deleted.

2 changes: 2 additions & 0 deletions src/models.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { Payload } from './payload';

/**
* Appwrite Models
*/
Expand Down
43 changes: 43 additions & 0 deletions src/payload.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
export class Payload {
private data: Buffer;
public filename?: string;
public size: number;

constructor(data: Buffer, filename?: string) {
this.data = data;
this.filename = filename;
this.size = data.byteLength;
}

public toBinary(offset: number = 0, length?: number): Buffer {
if (offset === 0 && length === undefined) {
return this.data;
} else if (length === undefined) {
return this.data.subarray(offset);
} else {
return this.data.subarray(offset, offset + length);
}
}

public toJson<T = unknown>(): T {
return JSON.parse(this.toString());
}

public toString(): string {
return this.data.toString("utf-8");
}

public static fromBinary(bytes: Buffer, filename?: string): Payload {
return new Payload(bytes, filename);
}

public static fromJson(object: any, filename?: string): Payload {
const data = Buffer.from(JSON.stringify(object), "utf-8");
return new Payload(data, filename);
}

public static fromString(text: string, filename?: string): Payload {
const data = Buffer.from(text, "utf-8");
return new Payload(data, filename);
}
}
Loading

0 comments on commit b8bd37b

Please sign in to comment.