Skip to content

Commit ab469be

Browse files
amirai21asafgardin
authored andcommitted
feat: wip
1 parent 6ff4df7 commit ab469be

File tree

9 files changed

+61
-82
lines changed

9 files changed

+61
-82
lines changed

examples/studio/conversational-rag/rag-engine.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ async function uploadGetUpdateDelete(fileInput, label) {
1111
const uploadFileResponse: UploadFileResponse = await client.ragEngine.create(fileInput, {
1212
path: label,
1313
});
14-
14+
console.log(uploadFileResponse);
1515
let file: FileResponse = await client.ragEngine.get(uploadFileResponse.fileId);
1616
console.log(file);
1717
await sleep(1000); // Give it a sec to start process before updating

src/APIClient.ts

Lines changed: 41 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
HTTPMethod,
99
Headers,
1010
CrossPlatformResponse,
11+
UnifiedFormData,
1112
} from './types';
1213
import { AI21EnvConfig } from './EnvConfig';
1314
import { createFetchInstance, createFilesHandlerInstance } from './runtime';
@@ -25,6 +26,19 @@ const validatePositiveInteger = (name: string, n: unknown): number => {
2526
return n;
2627
};
2728

29+
30+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
31+
const appendBodyToFormData = (formData: UnifiedFormData, body: Record<string, any>): void => {
32+
for (const [key, value] of Object.entries(body)) {
33+
if (Array.isArray(value)) {
34+
value.forEach((item) => formData.append(key, item));
35+
} else {
36+
formData.append(key, value);
37+
}
38+
}
39+
};
40+
41+
2842
export abstract class APIClient {
2943
protected baseURL: string;
3044
protected maxRetries: number;
@@ -52,38 +66,38 @@ export abstract class APIClient {
5266
this.filesHandler = filesHandler;
5367
}
5468
get<Req, Rsp>(path: string, opts?: RequestOptions<Req>): Promise<Rsp> {
55-
return this.makeRequest('get', path, opts);
69+
return this.prepareAndExecuteRequest('get', path, opts);
5670
}
5771

5872
post<Req, Rsp>(path: string, opts?: RequestOptions<Req>): Promise<Rsp> {
59-
return this.makeRequest('post', path, opts);
73+
return this.prepareAndExecuteRequest('post', path, opts);
6074
}
6175

6276
put<Req, Rsp>(path: string, opts?: RequestOptions<Req>): Promise<Rsp> {
63-
return this.makeRequest('put', path, opts);
77+
return this.prepareAndExecuteRequest('put', path, opts);
6478
}
6579

6680
delete<Req, Rsp>(path: string, opts?: RequestOptions<Req>): Promise<Rsp> {
67-
return this.makeRequest('delete', path, opts);
81+
return this.prepareAndExecuteRequest('delete', path, opts);
6882
}
6983

7084
async upload<Req, Rsp>(path: string, file: FilePathOrFileObject, opts?: RequestOptions<Req>): Promise<Rsp> {
71-
const formData = await this.filesHandler.createFormData(file);
85+
const formDataRequest = await this.filesHandler.prepareFormDataRequest(file);
7286

7387
if (opts?.body) {
7488
// eslint-disable-next-line @typescript-eslint/no-explicit-any
75-
this.filesHandler.appendBodyToFormData(formData, opts.body as Record<string, any>);
89+
appendBodyToFormData(formDataRequest.formData, opts.body as Record<string, any>);
7690
}
7791

7892
const headers = {
7993
...opts?.headers,
80-
...this.filesHandler.getMultipartFormDataHeaders(formData),
94+
...formDataRequest.headers,
8195
};
8296

8397
const options: FinalRequestOptions = {
8498
method: 'post',
8599
path: path,
86-
body: formData,
100+
body: formDataRequest.formData,
87101
headers,
88102
};
89103

@@ -105,38 +119,42 @@ export abstract class APIClient {
105119
...this.authHeaders(opts),
106120
};
107121

108-
if (opts?.body instanceof FormData) {
109-
return defaultHeaders;
110-
}
111-
return { ...defaultHeaders, 'Content-Type': 'application/json' };
122+
return { ...defaultHeaders, ...opts.headers };
112123
}
113124

114125
// eslint-disable-next-line @typescript-eslint/no-unused-vars
115126
protected authHeaders(opts: FinalRequestOptions): Headers {
116127
return {};
117128
}
118129

119-
private makeRequest<Req, Rsp>(method: HTTPMethod, path: string, opts?: RequestOptions<Req>): Promise<Rsp> {
130+
private buildFullUrl(path: string, query?: Record<string, unknown>): string {
131+
let url = `${this.baseURL}${path}`;
132+
if (query) {
133+
const queryString = new URLSearchParams(query as Record<string, string>).toString();
134+
url += `?${queryString}`;
135+
}
136+
return url;
137+
}
138+
139+
private prepareAndExecuteRequest<Req, Rsp>(method: HTTPMethod, path: string, opts?: RequestOptions<Req>): Promise<Rsp> {
120140
const options = {
121141
method,
122142
path,
123-
124143
...opts,
125-
};
144+
} as FinalRequestOptions;
145+
146+
if (options?.body) {
147+
options.body = JSON.stringify(options.body);
148+
options.headers = { ...options.headers, 'Content-Type': 'application/json' };
149+
}
126150

127-
return this.performRequest(options as FinalRequestOptions).then(
151+
return this.performRequest(options).then(
128152
(response) => this.fetch.handleResponse<Rsp>(response) as Rsp,
129153
);
130154
}
131155

132156
private async performRequest(options: FinalRequestOptions): Promise<APIResponseProps> {
133-
let url = `${this.baseURL}${options.path}`;
134-
135-
if (options.query) {
136-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
137-
const queryString = new URLSearchParams(options.query as Record<string, any>).toString();
138-
url += `?${queryString}`;
139-
}
157+
const url = this.buildFullUrl(options.path, options.query as Record<string, unknown>);
140158

141159
const headers = {
142160
...this.defaultHeaders(options),

src/fetch/BrowserFetch.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,11 @@ import { Stream, BrowserSSEDecoder } from '../streaming';
55
export class BrowserFetch extends BaseFetch {
66
call(url: string, options: FinalRequestOptions): Promise<CrossPlatformResponse> {
77
const controller = new AbortController();
8-
const body = options.body instanceof FormData ? options.body : JSON.stringify(options.body);
98

109
return fetch(url, {
1110
method: options.method,
1211
headers: options?.headers ? (options.headers as HeadersInit) : undefined,
13-
body,
12+
body: options?.body ? (options.body as BodyInit) : undefined,
1413
signal: controller.signal,
1514
});
1615
}

src/fetch/NodeFetch.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,16 @@
11
import { FinalRequestOptions, CrossPlatformResponse } from 'types';
22
import { BaseFetch } from './BaseFetch';
33
import { Stream, NodeSSEDecoder } from '../streaming';
4-
import FormData from 'form-data';
54

65
export class NodeFetch extends BaseFetch {
76
async call(url: string, options: FinalRequestOptions): Promise<CrossPlatformResponse> {
87
const nodeFetchModule = await import('node-fetch');
98
const nodeFetch = nodeFetchModule.default;
109

11-
const body = options.body instanceof FormData ? options.body : JSON.stringify(options.body);
12-
1310
return nodeFetch(url, {
1411
method: options.method,
1512
headers: options?.headers ? (options.headers as Record<string, string>) : undefined,
16-
body,
13+
body: options?.body ? (options.body as import('form-data') | string) : undefined,
1714
});
1815
}
1916

src/files/BaseFilesHandler.ts

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,6 @@
1-
import { UnifiedFormData } from 'types';
1+
import { FormDataRequest } from 'types/API';
22
import { FilePathOrFileObject } from 'types/rag';
33

44
export abstract class BaseFilesHandler {
5-
abstract createFormData(file: FilePathOrFileObject): Promise<UnifiedFormData>;
6-
7-
abstract getMultipartFormDataHeaders(formData: UnifiedFormData): Record<string, string> | null;
8-
9-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
10-
appendBodyToFormData = (formData: UnifiedFormData, body: Record<string, any>): void => {
11-
for (const [key, value] of Object.entries(body)) {
12-
if (Array.isArray(value)) {
13-
value.forEach((item) => formData.append(key, item));
14-
} else {
15-
formData.append(key, value);
16-
}
17-
}
18-
};
5+
abstract prepareFormDataRequest(file: FilePathOrFileObject): Promise<FormDataRequest>;
196
}

src/files/BrowserFilesHandler.ts

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,12 @@
1-
import { UnifiedFormData } from 'types';
21
import { FilePathOrFileObject } from 'types/rag';
32
import { BaseFilesHandler } from './BaseFilesHandler';
3+
import { FormDataRequest } from 'types/API';
44

55
export class BrowserFilesHandler extends BaseFilesHandler {
6-
async createFormData(file: FilePathOrFileObject): Promise<UnifiedFormData> {
6+
async prepareFormDataRequest(file: FilePathOrFileObject): Promise<FormDataRequest> {
77
const formData = new FormData();
8-
9-
if (file instanceof window.File) {
10-
formData.append('file', file);
11-
} else {
12-
throw new Error('Unsupported file type in browser');
13-
}
14-
15-
return formData;
16-
}
17-
18-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
19-
getMultipartFormDataHeaders(formData: UnifiedFormData): Record<string, string> | null {
20-
/* In browser, we don't need to set any additional headers for multipart/form-data, as the browser will handle it */
21-
return {};
8+
formData.append('file', file);
9+
// Note that when uploading files in a browser, the browser handles the multipart/form-data headers
10+
return { formData, headers: {} };
2211
}
2312
}

src/files/NodeFilesHandler.ts

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { UnifiedFormData } from 'types';
21
import { FilePathOrFileObject } from 'types/rag';
32
import { BaseFilesHandler } from './BaseFilesHandler';
3+
import { FormDataRequest } from 'types/API';
44

55
export class NodeFilesHandler extends BaseFilesHandler {
6-
async convertReadableStream(readableStream: ReadableStream): Promise<NodeJS.ReadableStream> {
6+
private async convertReadableStream(readableStream: ReadableStream): Promise<NodeJS.ReadableStream> {
77
const { Readable } = await import('stream');
88
const reader = readableStream.getReader();
99

@@ -19,34 +19,22 @@ export class NodeFilesHandler extends BaseFilesHandler {
1919
});
2020
}
2121

22-
async createFormData(file: FilePathOrFileObject): Promise<UnifiedFormData> {
22+
async prepareFormDataRequest(file: FilePathOrFileObject): Promise<FormDataRequest> {
2323
const { default: FormDataNode } = await import('form-data');
2424
const formData = new FormDataNode();
2525

2626
if (typeof file === 'string') {
2727
const fs = (await import('fs')).default;
2828
formData.append('file', fs.createReadStream(file), { filename: file.split('/').pop() });
29-
} else if (Buffer.isBuffer(file)) {
30-
formData.append('file', file, { filename: 'TODO - add filename to buffer flow' });
3129
} else if (file instanceof File) {
3230
const nodeStream = await this.convertReadableStream(file.stream());
3331
formData.append('file', nodeStream, file.name);
3432
} else {
3533
throw new Error(`Unsupported file type for Node.js file upload flow: ${file}`);
3634
}
3735

38-
return formData;
39-
}
36+
const formDataHeaders = { 'Content-Type': `multipart/form-data; boundary=${formData.getBoundary()}` };
4037

41-
getMultipartFormDataHeaders(formData: UnifiedFormData): Record<string, string> | null {
42-
if (formData instanceof FormData) {
43-
throw new Error(
44-
'getMultipartFormDataHeaders invoked with native browser FormData instance instead of NodeJS form-data',
45-
);
46-
}
47-
const boundary = formData.getBoundary();
48-
return {
49-
'Content-Type': `multipart/form-data; boundary=${boundary}`,
50-
};
38+
return { formData, headers: formDataHeaders };
5139
}
5240
}

src/types/API.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export type RequestOptions<Req = unknown | Record<string, unknown> | ArrayBuffer
1010
method?: HTTPMethod;
1111
path?: string;
1212
query?: Req | undefined;
13-
body?: Req | FormData | null | undefined;
13+
body?: Req | UnifiedFormData | string | null | undefined;
1414
headers?: Headers | undefined;
1515

1616
maxRetries?: number;
@@ -30,5 +30,7 @@ export type Headers = Record<string, string | null | undefined>;
3030
export type CrossPlatformResponse = Response | import('node-fetch').Response;
3131
export type CrossPlatformReadableStream = ReadableStream<Uint8Array> | import('stream/web').ReadableStream;
3232

33-
export type FormDataNode = import('form-data');
34-
export type UnifiedFormData = FormData | FormDataNode;
33+
export type UnifiedFormData = FormData | import('form-data');
34+
35+
export type FormDataRequest = { formData: UnifiedFormData; headers: Headers };
36+

src/types/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ export {
3636
type Headers,
3737
type CrossPlatformResponse,
3838
type UnifiedFormData,
39-
type FormDataNode,
4039
} from './API';
4140

4241
export {

0 commit comments

Comments
 (0)