Add support for generating and uploading files via presigned URLs
Some checks failed
Build and Push Docker Images / changes (push) Successful in 4s
Build and Push Docker Images / build-backend (push) Has been skipped
Build and Push Docker Images / build-frontend (push) Failing after 48s

Introduces schemas, types, and API endpoints for managing file uploads using presigned URLs. Includes request and response models, React Query hooks, and client SDK functionality for generating and consuming presigned URLs.
This commit is contained in:
2025-03-13 07:34:53 +01:00
parent 03f643895d
commit 6397cfae49
4 changed files with 327 additions and 2 deletions

View File

@@ -22,6 +22,8 @@ import {
getEvent,
updateEvent,
getEventBySlug,
generatePresignedUrlApiV1UploadsPresignedUrlPost,
uploadFileApiV1UploadsTokenPost,
} from "../sdk.gen";
import { queryOptions, type UseMutationOptions } from "@tanstack/react-query";
import type {
@@ -65,6 +67,11 @@ import type {
UpdateEventError,
UpdateEventResponse,
GetEventBySlugData,
GeneratePresignedUrlApiV1UploadsPresignedUrlPostData,
GeneratePresignedUrlApiV1UploadsPresignedUrlPostError,
GeneratePresignedUrlApiV1UploadsPresignedUrlPostResponse,
UploadFileApiV1UploadsTokenPostData,
UploadFileApiV1UploadsTokenPostError,
} from "../types.gen";
import type { AxiosError } from "axios";
import { client as _heyApiClient } from "../client.gen";
@@ -621,3 +628,88 @@ export const getEventBySlugOptions = (options: Options<GetEventBySlugData>) => {
queryKey: getEventBySlugQueryKey(options),
});
};
export const generatePresignedUrlApiV1UploadsPresignedUrlPostQueryKey = (
options: Options<GeneratePresignedUrlApiV1UploadsPresignedUrlPostData>,
) =>
createQueryKey("generatePresignedUrlApiV1UploadsPresignedUrlPost", options);
export const generatePresignedUrlApiV1UploadsPresignedUrlPostOptions = (
options: Options<GeneratePresignedUrlApiV1UploadsPresignedUrlPostData>,
) => {
return queryOptions({
queryFn: async ({ queryKey, signal }) => {
const { data } = await generatePresignedUrlApiV1UploadsPresignedUrlPost({
...options,
...queryKey[0],
signal,
throwOnError: true,
});
return data;
},
queryKey: generatePresignedUrlApiV1UploadsPresignedUrlPostQueryKey(options),
});
};
export const generatePresignedUrlApiV1UploadsPresignedUrlPostMutation = (
options?: Partial<
Options<GeneratePresignedUrlApiV1UploadsPresignedUrlPostData>
>,
) => {
const mutationOptions: UseMutationOptions<
GeneratePresignedUrlApiV1UploadsPresignedUrlPostResponse,
AxiosError<GeneratePresignedUrlApiV1UploadsPresignedUrlPostError>,
Options<GeneratePresignedUrlApiV1UploadsPresignedUrlPostData>
> = {
mutationFn: async (localOptions) => {
const { data } = await generatePresignedUrlApiV1UploadsPresignedUrlPost({
...options,
...localOptions,
throwOnError: true,
});
return data;
},
};
return mutationOptions;
};
export const uploadFileApiV1UploadsTokenPostQueryKey = (
options: Options<UploadFileApiV1UploadsTokenPostData>,
) => createQueryKey("uploadFileApiV1UploadsTokenPost", options);
export const uploadFileApiV1UploadsTokenPostOptions = (
options: Options<UploadFileApiV1UploadsTokenPostData>,
) => {
return queryOptions({
queryFn: async ({ queryKey, signal }) => {
const { data } = await uploadFileApiV1UploadsTokenPost({
...options,
...queryKey[0],
signal,
throwOnError: true,
});
return data;
},
queryKey: uploadFileApiV1UploadsTokenPostQueryKey(options),
});
};
export const uploadFileApiV1UploadsTokenPostMutation = (
options?: Partial<Options<UploadFileApiV1UploadsTokenPostData>>,
) => {
const mutationOptions: UseMutationOptions<
unknown,
AxiosError<UploadFileApiV1UploadsTokenPostError>,
Options<UploadFileApiV1UploadsTokenPostData>
> = {
mutationFn: async (localOptions) => {
const { data } = await uploadFileApiV1UploadsTokenPost({
...options,
...localOptions,
throwOnError: true,
});
return data;
},
};
return mutationOptions;
};

View File

@@ -71,6 +71,19 @@ export const Body_login_oauthSchema = {
title: "Body_login_oauth",
} as const;
export const Body_upload_file_api_v1_uploads__token__postSchema = {
properties: {
file: {
type: "string",
format: "binary",
title: "File",
},
},
type: "object",
required: ["file"],
title: "Body_upload_file_api_v1_uploads__token__post",
} as const;
export const EventCreateSchema = {
properties: {
title: {
@@ -1118,6 +1131,55 @@ export const PaginatedResponse_EventResponse_Schema = {
title: "PaginatedResponse[EventResponse]",
} as const;
export const PresignedUrlRequestSchema = {
properties: {
filename: {
type: "string",
title: "Filename",
description: "Original filename of the image",
},
content_type: {
type: "string",
title: "Content Type",
description: "Content type of the file (e.g., image/jpeg)",
},
folder: {
type: "string",
title: "Folder",
description: "Folder to store the file in",
default: "images",
},
},
type: "object",
required: ["filename", "content_type"],
title: "PresignedUrlRequest",
description: "Request model for generating presigned URLs.",
} as const;
export const PresignedUrlResponseSchema = {
properties: {
upload_url: {
type: "string",
title: "Upload Url",
description: "URL to upload the file to",
},
file_url: {
type: "string",
title: "File Url",
description: "URL where the file will be accessible after upload",
},
expires_in: {
type: "integer",
title: "Expires In",
description: "Time in seconds until the upload URL expires",
},
},
type: "object",
required: ["upload_url", "file_url", "expires_in"],
title: "PresignedUrlResponse",
description: "Response model for presigned URL generation.",
} as const;
export const RefreshTokenRequestSchema = {
properties: {
refresh_token: {

View File

@@ -5,6 +5,7 @@ import {
type TDataShape,
type Client,
urlSearchParamsBodySerializer,
formDataBodySerializer,
} from "@hey-api/client-axios";
import type {
RootGetData,
@@ -63,6 +64,11 @@ import type {
GetEventBySlugData,
GetEventBySlugResponse,
GetEventBySlugError,
GeneratePresignedUrlApiV1UploadsPresignedUrlPostData,
GeneratePresignedUrlApiV1UploadsPresignedUrlPostResponse,
GeneratePresignedUrlApiV1UploadsPresignedUrlPostError,
UploadFileApiV1UploadsTokenPostData,
UploadFileApiV1UploadsTokenPostError,
} from "./types.gen";
import { client as _heyApiClient } from "./client.gen";
@@ -270,7 +276,6 @@ export const listEventThemes = <ThrowOnError extends boolean = false>(
/**
* Create Theme
* Create new event theme.
*/
export const createEventTheme = <ThrowOnError extends boolean = false>(
options: Options<CreateEventThemeData, ThrowOnError>,
@@ -280,6 +285,12 @@ export const createEventTheme = <ThrowOnError extends boolean = false>(
CreateEventThemeError,
ThrowOnError
>({
security: [
{
scheme: "bearer",
type: "http",
},
],
url: "/api/v1/event_themes/",
...options,
headers: {
@@ -331,7 +342,6 @@ export const getEventTheme = <ThrowOnError extends boolean = false>(
/**
* Update Theme
* Update specific theme by ID.
*/
export const updateEventTheme = <ThrowOnError extends boolean = false>(
options: Options<UpdateEventThemeData, ThrowOnError>,
@@ -341,6 +351,12 @@ export const updateEventTheme = <ThrowOnError extends boolean = false>(
UpdateEventThemeError,
ThrowOnError
>({
security: [
{
scheme: "bearer",
type: "http",
},
],
url: "/api/v1/event_themes/{theme_id}",
...options,
headers: {
@@ -535,3 +551,65 @@ export const getEventBySlug = <ThrowOnError extends boolean = false>(
...options,
});
};
/**
* Generate Presigned Url
* Generate a presigned URL for uploading a file.
*
* This endpoint creates a secure token that allows direct upload to the storage system.
* After successful upload, the file will be accessible at the returned file_url.
*/
export const generatePresignedUrlApiV1UploadsPresignedUrlPost = <
ThrowOnError extends boolean = false,
>(
options: Options<
GeneratePresignedUrlApiV1UploadsPresignedUrlPostData,
ThrowOnError
>,
) => {
return (options.client ?? _heyApiClient).post<
GeneratePresignedUrlApiV1UploadsPresignedUrlPostResponse,
GeneratePresignedUrlApiV1UploadsPresignedUrlPostError,
ThrowOnError
>({
security: [
{
scheme: "bearer",
type: "http",
},
],
url: "/api/v1/uploads/presigned-url",
...options,
headers: {
"Content-Type": "application/json",
...options?.headers,
},
});
};
/**
* Upload File
* Upload a file using a presigned URL token.
*
* This endpoint handles the actual file upload after a presigned URL is generated.
* The token validates the upload permissions and destination.
*/
export const uploadFileApiV1UploadsTokenPost = <
ThrowOnError extends boolean = false,
>(
options: Options<UploadFileApiV1UploadsTokenPostData, ThrowOnError>,
) => {
return (options.client ?? _heyApiClient).post<
unknown,
UploadFileApiV1UploadsTokenPostError,
ThrowOnError
>({
...formDataBodySerializer,
url: "/api/v1/uploads/{token}",
...options,
headers: {
"Content-Type": null,
...options?.headers,
},
});
};

View File

@@ -14,6 +14,10 @@ export type BodyLoginOauth = {
client_secret?: string | null;
};
export type BodyUploadFileApiV1UploadsTokenPost = {
file: Blob | File;
};
export type EventCreate = {
title: string;
description?: string | null;
@@ -179,6 +183,42 @@ export type PaginatedResponseEventResponse = {
size: number;
};
/**
* Request model for generating presigned URLs.
*/
export type PresignedUrlRequest = {
/**
* Original filename of the image
*/
filename: string;
/**
* Content type of the file (e.g., image/jpeg)
*/
content_type: string;
/**
* Folder to store the file in
*/
folder?: string;
};
/**
* Response model for presigned URL generation.
*/
export type PresignedUrlResponse = {
/**
* URL to upload the file to
*/
upload_url: string;
/**
* URL where the file will be accessible after upload
*/
file_url: string;
/**
* Time in seconds until the upload URL expires
*/
expires_in: number;
};
export type RefreshTokenRequest = {
refresh_token: string;
};
@@ -756,6 +796,59 @@ export type GetEventBySlugResponses = {
export type GetEventBySlugResponse =
GetEventBySlugResponses[keyof GetEventBySlugResponses];
export type GeneratePresignedUrlApiV1UploadsPresignedUrlPostData = {
body: PresignedUrlRequest;
path?: never;
query?: never;
url: "/api/v1/uploads/presigned-url";
};
export type GeneratePresignedUrlApiV1UploadsPresignedUrlPostErrors = {
/**
* Validation Error
*/
422: HttpValidationError;
};
export type GeneratePresignedUrlApiV1UploadsPresignedUrlPostError =
GeneratePresignedUrlApiV1UploadsPresignedUrlPostErrors[keyof GeneratePresignedUrlApiV1UploadsPresignedUrlPostErrors];
export type GeneratePresignedUrlApiV1UploadsPresignedUrlPostResponses = {
/**
* Successful Response
*/
200: PresignedUrlResponse;
};
export type GeneratePresignedUrlApiV1UploadsPresignedUrlPostResponse =
GeneratePresignedUrlApiV1UploadsPresignedUrlPostResponses[keyof GeneratePresignedUrlApiV1UploadsPresignedUrlPostResponses];
export type UploadFileApiV1UploadsTokenPostData = {
body: BodyUploadFileApiV1UploadsTokenPost;
path: {
token: string;
};
query?: never;
url: "/api/v1/uploads/{token}";
};
export type UploadFileApiV1UploadsTokenPostErrors = {
/**
* Validation Error
*/
422: HttpValidationError;
};
export type UploadFileApiV1UploadsTokenPostError =
UploadFileApiV1UploadsTokenPostErrors[keyof UploadFileApiV1UploadsTokenPostErrors];
export type UploadFileApiV1UploadsTokenPostResponses = {
/**
* Successful Response
*/
200: unknown;
};
export type ClientOptions = {
baseURL: "http://localhost:8000" | (string & {});
};