Skip to content

Commit ea5f1b7

Browse files
committed
feat(ui): add files api
Signed-off-by: Petr Bulánek <[email protected]>
1 parent c295201 commit ea5f1b7

File tree

10 files changed

+122
-60
lines changed

10 files changed

+122
-60
lines changed

apps/agentstack-sdk-ts/src/client/api/files/api.ts

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,32 @@
55

66
import type { CallApi } from '../../client/types';
77
import { ApiMethod } from '../../client/types';
8-
import { createFileResponseSchema, readFileResponseSchema } from './schemas';
9-
import type { CreateFileRequest, ReadFileRequest } from './types';
8+
import {
9+
createFileResponseSchema,
10+
deleteFileResponseSchema,
11+
readFileContentResponseSchema,
12+
readFileResponseSchema,
13+
} from './schemas';
14+
import type { CreateFileRequest, DeleteFileRequest, ReadFileContentRequest, ReadFileRequest } from './types';
1015

1116
export function createFilesApi(callApi: CallApi) {
12-
const createFile = ({ context_id, ...body }: CreateFileRequest) =>
13-
callApi({
17+
const createFile = ({ context_id, file }: CreateFileRequest) => {
18+
const body = new FormData();
19+
20+
if (file instanceof File) {
21+
body.append('file', file);
22+
} else {
23+
body.append('file', file.blob, file.filename);
24+
}
25+
26+
return callApi({
1427
method: ApiMethod.Post,
1528
path: '/api/v1/files',
1629
schema: createFileResponseSchema,
1730
query: { context_id },
1831
body,
1932
});
33+
};
2034

2135
const readFile = ({ context_id, file_id }: ReadFileRequest) =>
2236
callApi({
@@ -26,8 +40,26 @@ export function createFilesApi(callApi: CallApi) {
2640
query: { context_id },
2741
});
2842

43+
const deleteFile = ({ context_id, file_id }: DeleteFileRequest) =>
44+
callApi({
45+
method: ApiMethod.Delete,
46+
path: `/api/v1/files/${file_id}`,
47+
schema: deleteFileResponseSchema,
48+
query: { context_id },
49+
});
50+
51+
const readFileContent = ({ context_id, file_id }: ReadFileContentRequest) =>
52+
callApi({
53+
method: ApiMethod.Get,
54+
path: `/api/v1/files/${file_id}/content`,
55+
schema: readFileContentResponseSchema,
56+
query: { context_id },
57+
});
58+
2959
return {
3060
createFile,
3161
readFile,
62+
deleteFile,
63+
readFileContent,
3264
};
3365
}

apps/agentstack-sdk-ts/src/client/api/files/schemas.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,13 @@ export const fileSchema = z.object({
2323

2424
export const createFileRequestSchema = z.object({
2525
context_id: z.string().nullable(),
26-
file: z.string(),
26+
file: z.union([
27+
z.file(),
28+
z.object({
29+
blob: z.instanceof(Blob),
30+
filename: z.string(),
31+
}),
32+
]),
2733
});
2834

2935
export const createFileResponseSchema = fileSchema;
@@ -34,3 +40,17 @@ export const readFileRequestSchema = z.object({
3440
});
3541

3642
export const readFileResponseSchema = fileSchema;
43+
44+
export const deleteFileRequestSchema = z.object({
45+
context_id: z.string().nullable(),
46+
file_id: z.string(),
47+
});
48+
49+
export const deleteFileResponseSchema = z.null();
50+
51+
export const readFileContentRequestSchema = z.object({
52+
context_id: z.string().nullable(),
53+
file_id: z.string(),
54+
});
55+
56+
export const readFileContentResponseSchema = z.unknown();

apps/agentstack-sdk-ts/src/client/api/files/types.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@ import type z from 'zod';
88
import type {
99
createFileRequestSchema,
1010
createFileResponseSchema,
11+
deleteFileRequestSchema,
12+
deleteFileResponseSchema,
1113
fileSchema,
14+
readFileContentRequestSchema,
15+
readFileContentResponseSchema,
1216
readFileRequestSchema,
1317
readFileResponseSchema,
1418
} from './schemas';
@@ -25,3 +29,9 @@ export type CreateFileResponse = z.infer<typeof createFileResponseSchema>;
2529

2630
export type ReadFileRequest = z.infer<typeof readFileRequestSchema>;
2731
export type ReadFileResponse = z.infer<typeof readFileResponseSchema>;
32+
33+
export type DeleteFileRequest = z.infer<typeof deleteFileRequestSchema>;
34+
export type DeleteFileResponse = z.infer<typeof deleteFileResponseSchema>;
35+
36+
export type ReadFileContentRequest = z.infer<typeof readFileContentRequestSchema>;
37+
export type ReadFileContentResponse = z.infer<typeof readFileContentResponseSchema>;

apps/agentstack-sdk-ts/src/client/client/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ function createCallApi({ baseUrl, fetch: fetchFn }: { baseUrl: string; fetch: ty
3131
const rawResponse = await fetchFn(requestUrl, requestInit);
3232
const bodyText = await safeReadText(rawResponse);
3333

34-
const { ok, status, statusText } = rawResponse;
34+
const { ok, status, statusText, headers } = rawResponse;
3535
const response = { status, statusText, bodyText };
3636

3737
if (!ok) {
@@ -46,7 +46,7 @@ function createCallApi({ baseUrl, fetch: fetchFn }: { baseUrl: string; fetch: ty
4646
} satisfies ApiFailure;
4747
}
4848

49-
const { data: parsedBody, error: parseError } = parseBodyText(bodyText);
49+
const { data: parsedBody, error: parseError } = parseBodyText(bodyText, headers);
5050

5151
if (parseError) {
5252
return {

apps/agentstack-sdk-ts/src/client/client/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export enum ApiMethod {
1717
}
1818

1919
export type ApiQueryParams = Record<string, unknown>;
20-
export type ApiRequestBody = Record<string, unknown>;
20+
export type ApiRequestBody = Record<string, unknown> | FormData;
2121

2222
export interface ApiParams<T> {
2323
method: ApiMethod;

apps/agentstack-sdk-ts/src/client/client/utils.ts

Lines changed: 33 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55

6-
import type { ApiQueryParams, ApiRequestBody } from './types';
7-
import type { ApiMethod } from './types';
6+
import type { ApiMethod, ApiQueryParams, ApiRequestBody } from './types';
87

98
export function buildRequestUrl({ baseUrl, path, query }: { baseUrl: string; path: string; query?: ApiQueryParams }) {
109
const url = `${baseUrl.replace(/\/+$/, '')}${path}`;
@@ -27,21 +26,24 @@ export function buildRequestUrl({ baseUrl, path, query }: { baseUrl: string; pat
2726
}
2827

2928
export function buildRequestInit({ method, body }: { method: ApiMethod; body?: ApiRequestBody }) {
30-
const headers = new Headers({ Accept: 'application/json' });
29+
const headers = new Headers();
30+
31+
let requestBody: FormData | string | undefined;
3132

3233
if (body) {
33-
headers.set('Content-Type', 'application/json');
34+
if (body instanceof FormData) {
35+
requestBody = body;
36+
} else {
37+
headers.set('Content-Type', 'application/json');
3438

35-
return {
36-
method,
37-
headers,
38-
body: JSON.stringify(body),
39-
};
39+
requestBody = JSON.stringify(body);
40+
}
4041
}
4142

4243
return {
4344
method,
4445
headers,
46+
body: requestBody,
4547
};
4648
}
4749

@@ -53,7 +55,10 @@ export async function safeReadText(response: Response) {
5355
}
5456
}
5557

56-
export function parseBodyText(bodyText: string | null): {
58+
export function parseBodyText(
59+
bodyText: string | null,
60+
headers: Headers,
61+
): {
5762
data: unknown;
5863
error?: Error;
5964
} {
@@ -63,14 +68,23 @@ export function parseBodyText(bodyText: string | null): {
6368
};
6469
}
6570

66-
try {
67-
return {
68-
data: JSON.parse(bodyText),
69-
};
70-
} catch (error) {
71-
return {
72-
data: null,
73-
error: error instanceof Error ? error : new Error('Failed to parse body text.'),
74-
};
71+
const contentType = headers.get('content-type') ?? '';
72+
const isJsonResponse = contentType.includes('application/json');
73+
74+
if (isJsonResponse) {
75+
try {
76+
return {
77+
data: JSON.parse(bodyText),
78+
};
79+
} catch (error) {
80+
return {
81+
data: null,
82+
error: error instanceof Error ? error : new Error('Failed to parse body text.'),
83+
};
84+
}
7585
}
86+
87+
return {
88+
data: bodyText,
89+
};
7690
}

apps/agentstack-ui/src/modules/files/api/index.ts

Lines changed: 12 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,31 +3,23 @@
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55

6-
import { api } from '#api/index.ts';
7-
import { ensureData } from '#api/utils.ts';
6+
import type { DeleteFileRequest } from 'agentstack-sdk';
7+
import { unwrapResult } from 'agentstack-sdk';
88

9-
import type { DeleteFileParams, UploadFileParams, UploadFileRequest } from './types';
9+
import { agentStackClient } from '#api/agentstack-client.ts';
1010

11-
export async function uploadFile({ context_id, file }: UploadFileParams) {
12-
const response = await api.POST('/api/v1/files', {
13-
params: { query: { context_id } },
14-
body: { file: file.originalFile } as unknown as UploadFileRequest,
15-
bodySerializer: (body) => {
16-
const formData = new FormData();
11+
import type { UploadFileParams } from './types';
1712

18-
formData.append('file', body.file);
13+
export async function uploadFile({ file, ...request }: UploadFileParams) {
14+
const response = await agentStackClient.createFile({ ...request, file: file.originalFile });
15+
const result = unwrapResult(response);
1916

20-
return formData;
21-
},
22-
});
23-
24-
return ensureData(response);
17+
return result;
2518
}
2619

27-
export async function deleteFile({ file_id, context_id }: DeleteFileParams) {
28-
const response = await api.DELETE('/api/v1/files/{file_id}', {
29-
params: { path: { file_id }, query: { context_id } },
30-
});
20+
export async function deleteFile(request: DeleteFileRequest) {
21+
const response = await agentStackClient.deleteFile(request);
22+
const result = unwrapResult(response);
3123

32-
return ensureData(response);
24+
return result;
3325
}

apps/agentstack-ui/src/modules/files/api/mutations/useUploadFile.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,14 @@
44
*/
55

66
import { useMutation } from '@tanstack/react-query';
7+
import type { CreateFileResponse } from 'agentstack-sdk';
78

89
import { uploadFile } from '..';
9-
import type { UploadFileParams, UploadFileResponse } from '../types';
10+
import type { UploadFileParams } from '../types';
1011

1112
interface Props {
1213
onMutate?: (variables: UploadFileParams) => void;
13-
onSuccess?: (data: UploadFileResponse | undefined, variables: UploadFileParams) => void;
14+
onSuccess?: (data: CreateFileResponse, variables: UploadFileParams) => void;
1415
onError?: (error: Error, variables: UploadFileParams) => void;
1516
}
1617

apps/agentstack-ui/src/modules/files/api/types.ts

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,8 @@
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55

6-
import type { ApiPath, ApiQuery, ApiRequest, ApiResponse } from '#@types/utils.ts';
6+
import type { CreateFileRequest } from 'agentstack-sdk';
77

88
import type { FileEntity } from '../types';
99

10-
export type UploadFileQuery = ApiQuery<'/api/v1/files', 'post'>;
11-
export type UploadFileRequest = ApiRequest<'/api/v1/files', 'post', 'multipart/form-data'>;
12-
export type UploadFileParams = UploadFileQuery & Omit<UploadFileRequest, 'file'> & { file: FileEntity };
13-
export type UploadFileResponse = ApiResponse<'/api/v1/files', 'post', 'application/json', 201>;
14-
15-
export type DeleteFileQuery = ApiQuery<'/api/v1/files/{file_id}', 'delete'>;
16-
export type DeleteFilePath = ApiPath<'/api/v1/files/{file_id}', 'delete'>;
17-
export type DeleteFileParams = DeleteFilePath & DeleteFileQuery;
10+
export type UploadFileParams = Omit<CreateFileRequest, 'file'> & { file: FileEntity };

apps/agentstack-ui/src/modules/files/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55

6-
import type { UploadFileResponse } from './api/types';
6+
import type { CreateFileResponse } from 'agentstack-sdk';
77

88
export interface FileEntity {
99
id: string;
1010
originalFile: File;
1111
status: FileStatus;
12-
uploadFile?: UploadFileResponse;
12+
uploadFile?: CreateFileResponse;
1313
error?: string;
1414
}
1515

0 commit comments

Comments
 (0)