Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 31 additions & 1 deletion src/SDK/Language/Node.php
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ public function getTypeName(array $parameter, array $method = []): string
}
return 'any[]';
case self::TYPE_FILE:
return "File";
return "File | InputFile";
case self::TYPE_OBJECT:
if (empty($method)) {
return $parameter['type'];
Expand Down Expand Up @@ -186,6 +186,36 @@ public function getParamExample(array $param, string $lang = ''): string
};
}

/**
* Check if service has any file parameters
*
* @param array $service
* @return bool
*/
public function hasFileParam(array $service): bool
{
foreach ($service['methods'] as $method) {
foreach ($method['parameters']['all'] as $parameter) {
if ($parameter['type'] === self::TYPE_FILE) {
return true;
}
}
}
return false;
}

/**
* @return array
*/
public function getFilters(): array
{
return \array_merge(parent::getFilters(), [
new \Twig\TwigFilter('hasFileParam', function ($service) {
return $this->hasFileParam($service);
}),
]);
}

/**
* @return array
*/
Expand Down
51 changes: 50 additions & 1 deletion templates/node/src/client.ts.twig
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { fetch, FormData, File } from 'node-fetch-native-with-agent';
import { createAgent } from 'node-fetch-native-with-agent/agent';
import { Models } from './models';
import { InputFile } from './inputFile';

type Payload = {
[key: string]: any;
Expand Down Expand Up @@ -205,12 +206,60 @@ class Client {
}

async chunkedUpload(method: string, url: URL, headers: Headers = {}, originalPayload: Payload = {}, onProgress: (progress: UploadProgress) => void) {
const [fileParam, file] = Object.entries(originalPayload).find(([_, value]) => value instanceof File) ?? [];
const [fileParam, file] = Object.entries(originalPayload).find(
([_, value]) => value instanceof File || value instanceof InputFile
) ?? [];

if (!file || !fileParam) {
throw new Error('File not found in payload');
}

if (file instanceof InputFile) {
const size = await file.size();

if (size <= Client.CHUNK_SIZE) {
const payload = { ...originalPayload };
payload[fileParam] = await file.toFile();
return await this.call(method, url, headers, payload);
}

let start = 0;
let response = null;

while (start < size) {
let end = start + Client.CHUNK_SIZE;
if (end >= size) {
end = size;
}

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

const payload = { ...originalPayload };
payload[fileParam] = new File([chunk], file.filename);

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

if (onProgress && typeof onProgress === 'function') {
onProgress({
$id: response.$id,
progress: Math.round((end / size) * 100),
sizeUploaded: end,
chunksTotal: Math.ceil(size / Client.CHUNK_SIZE),
chunksUploaded: Math.ceil(end / Client.CHUNK_SIZE)
});
}

if (response && response.$id) {
headers['x-{{spec.title | caseLower }}-id'] = response.$id;
}

start = end;
}

return response;
}

if (file.size <= Client.CHUNK_SIZE) {
return await this.call(method, url, headers, originalPayload);
}
Expand Down
113 changes: 100 additions & 13 deletions templates/node/src/inputFile.ts.twig
Original file line number Diff line number Diff line change
@@ -1,23 +1,110 @@
import { File } from "node-fetch-native-with-agent";
import { realpathSync, readFileSync } from "fs";
import type { BinaryLike } from "crypto";
import { basename } from "path";
import { realpathSync } from "fs";
import { promises as fs } from "fs";
Comment on lines +2 to +4
Copy link
Contributor

@coderabbitai coderabbitai bot Jan 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Find and examine the file in question
fd "inputFile.ts.twig" --type f

Repository: appwrite/sdk-generator

Length of output: 139


🏁 Script executed:

# Check the repository structure to understand the project
ls -la
cat README.md | head -100

Repository: appwrite/sdk-generator

Length of output: 5331


🏁 Script executed:

# Look for any edge runtime or environment-specific configurations
rg -i "edge|cloudflare|vercel|worker" --max-count 20

Repository: appwrite/sdk-generator

Length of output: 1812


🏁 Script executed:

# Check if there's package.json to understand the project
cat package.json | head -50

Repository: appwrite/sdk-generator

Length of output: 110


🏁 Script executed:

# Read the inputFile.ts.twig file to see realpathSync usage
cat -n templates/node/src/inputFile.ts.twig

Repository: appwrite/sdk-generator

Length of output: 4153


🏁 Script executed:

# Read the client.ts.twig to see how edge runtimes are handled
cat -n templates/node/src/client.ts.twig | head -150

Repository: appwrite/sdk-generator

Length of output: 5378


🏁 Script executed:

# Check if there's any documentation about edge runtime support or Node.js version compatibility
rg -i "edge.runtime|nodejs|node version|realpathSync" templates/node --type ts -A 3 -B 3

Repository: appwrite/sdk-generator

Length of output: 48


🏁 Script executed:

# Check the templates directory structure to understand the organization
find templates/node -type f -name "*.twig" | head -20

Repository: appwrite/sdk-generator

Length of output: 576


🏁 Script executed:

# Check the package.json template for export conditions or edge runtime support
cat -n templates/node/package.json.twig | head -60

Repository: appwrite/sdk-generator

Length of output: 1638


🏁 Script executed:

# Check the index.ts to see what's exported
cat -n templates/node/src/index.ts.twig

Repository: appwrite/sdk-generator

Length of output: 779


🏁 Script executed:

# Check README for edge runtime or browser support documentation
cat -n templates/node/README.md.twig | head -100

Repository: appwrite/sdk-generator

Length of output: 1944


🏁 Script executed:

# Look for any conditional imports or environment checks in the Node.js template
rg "import|require" templates/node/src --type ts -B 2 -A 2

Repository: appwrite/sdk-generator

Length of output: 48


🏁 Script executed:

# Check if there's any mention of edge runtime or browser support in Node SDK docs/code
rg -i "edge|browser|cloudflare|vercel" templates/node --type ts --type twig

Repository: appwrite/sdk-generator

Length of output: 543


🏁 Script executed:

# Check tsconfig to understand compilation targets
cat -n templates/node/tsconfig.json.twig

Repository: appwrite/sdk-generator

Length of output: 656


🏁 Script executed:

# Look for any tests or examples showing InputFile usage
find templates/node -name "*test*" -o -name "*example*" -o -name "*spec*" 2>/dev/null | head -10

Repository: appwrite/sdk-generator

Length of output: 101


🏁 Script executed:

# Check if node-fetch-native-with-agent documents edge runtime support
rg -i "edge|cloudflare|worker" . --max-count 5

Repository: appwrite/sdk-generator

Length of output: 1844


The entire inputFile.ts module is incompatible with edge runtimes due to fs module imports, not just realpathSync.

The issue extends beyond realpathSync blocking: the fs module import itself (lines 2-4) will fail in edge runtimes like Cloudflare Workers or Vercel Edge Functions that don't support Node.js filesystem APIs. This makes the entire InputFile class unavailable in these environments, even for methods like fromBuffer that don't require filesystem access.

Since client.ts.twig already includes edge runtime detection and warnings (lines 58-59, 119-122), the SDK is designed to support edge runtimes. Either add conditional imports for fs and realpathSync (gated by runtime detection) or provide fallback implementations for edge environments.

🤖 Prompt for AI Agents
In @templates/node/src/inputFile.ts.twig around lines 2 - 4, The module imports
Node fs APIs at top-level (basename, realpathSync, promises as fs) which breaks
edge runtimes; change to conditional/dynamic loading: remove top-level
fs/realpathSync imports and instead import or require them inside methods that
need filesystem access (e.g., in InputFile.fromPath, InputFile.fromFileSystem)
guarded by a runtime check/flag similar to client.ts.twig, and provide safe
fallbacks for edge environments (e.g., have fromBuffer remain usable without fs,
have realpathSync return the input path or undefined in edge, and throw a clear
error when file-system-only methods are invoked in an edge runtime). Ensure
references to basename/realpathSync/fs are only used after the runtime check or
dynamic import so edge environments never attempt to load the Node fs module.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot Based on this comment, make a PR that makes changes compatible with Edge. Goal is to prevent entire InputFile from breaking - instead, we just want exception at runtime if trying to use methods that require fs

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

type BlobLike = {
size: number;
slice: (start: number, end: number) => BlobLike;
arrayBuffer: () => Promise<ArrayBuffer>;
};

type InputFileSource =
| { type: 'path'; path: string }
| { type: 'buffer'; data: Buffer }
| { type: 'blob'; data: BlobLike };

export class InputFile {
static fromBuffer(
parts: Blob | BinaryLike,
name: string
): File {
return new File([parts], name);
private source: InputFileSource;
filename: string;

private constructor(source: InputFileSource, filename: string) {
this.source = source;
this.filename = filename;
}

static fromBuffer(parts: BlobLike | Buffer | Uint8Array | ArrayBuffer | string, name: string): InputFile {
if (parts && typeof (parts as BlobLike).arrayBuffer === 'function') {
return new InputFile({ type: 'blob', data: parts as BlobLike }, name);
}

if (Buffer.isBuffer(parts)) {
return new InputFile({ type: 'buffer', data: parts }, name);
}

if (typeof parts === 'string') {
return new InputFile({ type: 'buffer', data: Buffer.from(parts) }, name);
}

if (parts instanceof ArrayBuffer) {
return new InputFile({ type: 'buffer', data: Buffer.from(parts) }, name);
}

if (ArrayBuffer.isView(parts)) {
return new InputFile({
type: 'buffer',
data: Buffer.from(parts.buffer, parts.byteOffset, parts.byteLength),
}, name);
}

throw new Error('Unsupported input type for InputFile.fromBuffer');
}

static fromPath(path: string, name: string): File {
static fromPath(path: string, name?: string): InputFile {
const realPath = realpathSync(path);
const contents = readFileSync(realPath);
return this.fromBuffer(contents, name);
return new InputFile({ type: 'path', path: realPath }, name ?? basename(realPath));
}

static fromPlainText(content: string, name: string): InputFile {
return new InputFile({ type: 'buffer', data: Buffer.from(content) }, name);
}

async size(): Promise<number> {
switch (this.source.type) {
case 'path':
return (await fs.stat(this.source.path)).size;
case 'buffer':
return this.source.data.length;
case 'blob':
return this.source.data.size;
}
}

async slice(start: number, end: number): Promise<Buffer> {
const length = end - start;

switch (this.source.type) {
case 'path': {
const handle = await fs.open(this.source.path, 'r');
try {
const buffer = Buffer.alloc(length);
const result = await handle.read(buffer, 0, length, start);
return result.bytesRead === buffer.length ? buffer : buffer.subarray(0, result.bytesRead);
} finally {
await handle.close();
}
}
case 'buffer':
return this.source.data.subarray(start, end);
case 'blob': {
const arrayBuffer = await this.source.data.slice(start, end).arrayBuffer();
return Buffer.from(arrayBuffer);
}
}
}

async toFile(): Promise<File> {
const buffer = await this.toBuffer();
return new File([buffer], this.filename);
}

static fromPlainText(content: string, name: string): File {
const arrayBytes = new TextEncoder().encode(content);
return this.fromBuffer(arrayBytes, name);
private async toBuffer(): Promise<Buffer> {
switch (this.source.type) {
case 'path':
return await fs.readFile(this.source.path);
case 'buffer':
return this.source.data;
case 'blob':
return Buffer.from(await this.source.data.arrayBuffer());
}
}
}
4 changes: 4 additions & 0 deletions templates/node/src/services/template.ts.twig
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { {{ spec.title | caseUcfirst}}Exception, Client, type Payload, UploadProgress } from '../client';
import type { Models } from '../models';

{% if service | hasFileParam %}
import { InputFile } from '../inputFile';
{% endif %}

{% set added = [] %}
{% for method in service.methods %}
{% for parameter in method.parameters.all %}
Expand Down
5 changes: 4 additions & 1 deletion tests/Node16Test.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,10 @@ class Node16Test extends Base
...Base::BAR_RESPONSES, // Object params
...Base::GENERAL_RESPONSES,
...Base::UPLOAD_RESPONSES,
...Base::UPLOAD_RESPONSES, // Object params
...Base::LARGE_FILE_RESPONSES,
...Base::LARGE_FILE_RESPONSES,
...Base::LARGE_FILE_RESPONSES,
...Base::LARGE_FILE_RESPONSES, // Large file uploads
...Base::ENUM_RESPONSES,
...Base::MODEL_RESPONSES,
...Base::EXCEPTION_RESPONSES,
Expand Down
5 changes: 4 additions & 1 deletion tests/Node18Test.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,10 @@ class Node18Test extends Base
...Base::BAR_RESPONSES, // Object params
...Base::GENERAL_RESPONSES,
...Base::UPLOAD_RESPONSES,
...Base::UPLOAD_RESPONSES, // Object params
...Base::LARGE_FILE_RESPONSES,
...Base::LARGE_FILE_RESPONSES,
...Base::LARGE_FILE_RESPONSES,
...Base::LARGE_FILE_RESPONSES, // Large file uploads
...Base::ENUM_RESPONSES,
...Base::MODEL_RESPONSES,
...Base::EXCEPTION_RESPONSES,
Expand Down
5 changes: 4 additions & 1 deletion tests/Node20Test.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,10 @@ class Node20Test extends Base
...Base::BAR_RESPONSES, // Object params
...Base::GENERAL_RESPONSES,
...Base::UPLOAD_RESPONSES,
...Base::UPLOAD_RESPONSES, // Object params
...Base::LARGE_FILE_RESPONSES,
...Base::LARGE_FILE_RESPONSES,
...Base::LARGE_FILE_RESPONSES,
...Base::LARGE_FILE_RESPONSES, // Large file uploads
...Base::ENUM_RESPONSES,
...Base::MODEL_RESPONSES,
...Base::EXCEPTION_RESPONSES,
Expand Down
Loading