Skip to content

Commit

Permalink
fix: ligther code
Browse files Browse the repository at this point in the history
  • Loading branch information
ivan-dalmet committed Feb 17, 2025
1 parent 8ddf759 commit 8ce490c
Show file tree
Hide file tree
Showing 9 changed files with 131 additions and 163 deletions.
21 changes: 11 additions & 10 deletions src/features/account/AccountProfileForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
FormFieldsAccountProfile,
zFormFieldsAccountProfile,
} from '@/features/account/schemas';
import { useAvatarFetch, useAvatarUpload } from '@/features/account/service';
import { useFetchFile, useUploadFileMutation } from '@/files/client';
import {
AVAILABLE_LANGUAGES,
DEFAULT_LANGUAGE_KEY,
Expand All @@ -32,8 +32,9 @@ export const AccountProfileForm = () => {
staleTime: Infinity,
});

const accountAvatar = useAvatarFetch(account.data?.image ?? '');
const uploadFile = useAvatarUpload();
const accountAvatar = useFetchFile(account.data?.image);

const uploadAvatar = useUploadFileMutation();

const updateAccount = trpc.account.update.useMutation({
onSuccess: async () => {
Expand Down Expand Up @@ -65,13 +66,13 @@ export const AccountProfileForm = () => {
image,
...values
}) => {
let fileUrl = account.data?.image;
try {
if (image?.file) {
const uploadResponse = await uploadFile.mutateAsync(image?.file);
fileUrl = uploadResponse.fileUrl;
}
updateAccount.mutate({ ...values, image: fileUrl });
updateAccount.mutate({
...values,
image: image?.file
? await uploadAvatar.mutateAsync(image.file)
: account.data?.image,
});
} catch {
form.setError('image', {
message: t('account:profile.feedbacks.uploadError.title'),
Expand Down Expand Up @@ -126,7 +127,7 @@ export const AccountProfileForm = () => {
<Button
type="submit"
variant="@primary"
isLoading={updateAccount.isLoading}
isLoading={updateAccount.isLoading || uploadAvatar.isLoading}
>
{t('account:profile.actions.update')}
</Button>
Expand Down
8 changes: 3 additions & 5 deletions src/features/account/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,6 @@ export const zFormFieldsAccountProfile = () =>
name: true,
language: true,
})
.merge(
z.object({
image: zFieldUploadValue(['image']).optional(),
})
);
.extend({
image: zFieldUploadValue(['image']).optional(),
});
20 changes: 0 additions & 20 deletions src/features/account/service.ts

This file was deleted.

82 changes: 82 additions & 0 deletions src/files/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { useMutation, useQuery } from '@tanstack/react-query';

import { trpc } from '@/lib/trpc/client';

/**
* Fetches a file from the specified URL and returns file information.
* Designed to be used as a `queryFn` in a `useQuery`.
*
* @param url The URL from which the file should be fetched.
* @param [metadata] The metadata of the file you want to retrieve.
* @returns A Promise that resolves to an object containing information about the file.
*
* @example
* // Usage with Tanstack Query's useQuery:
* const fileQuery = useQuery({
queryKey: ['fileKey', url],
queryFn: () => fetchFile(url, ['name']),
enabled: !!url,
});
*/
export const fetchFile = async (url: string, metadata?: string[]) => {
const fileResponse = await fetch(url, {
cache: 'no-cache',
});
if (!fileResponse.ok) {
throw new Error('Could not fetch the file');
}

const lastModifiedDateHeader = fileResponse.headers.get('Last-Modified');
const defaultFileData = {
fileUrl: url,
size: fileResponse.headers.get('Content-Length') ?? undefined,
type: fileResponse.headers.get('Content-Type') ?? undefined,
lastModifiedDate: lastModifiedDateHeader
? new Date(lastModifiedDateHeader)
: new Date(),
};

if (!metadata) {
return defaultFileData;
}

return metadata.reduce((file, metadataKey) => {
return {
...file,
[metadataKey]: fileResponse.headers.get(`x-amz-meta-${metadataKey}`),
};
}, defaultFileData);
};

export const useUploadFileMutation = (
params: {
getMetadata?: (file: File) => Record<string, string>;
} = {}
) => {
const uploadPresignedUrl = trpc.files.uploadPresignedUrl.useMutation();
return useMutation({
mutationFn: async (file: File) => {
const presignedUrlOutput = await uploadPresignedUrl.mutateAsync({
metadata: {
name: file.name,
...params.getMetadata?.(file),
},
});
await fetch(presignedUrlOutput.signedUrl, {
method: 'PUT',
headers: { 'Content-Type': file.type },
body: file,
});
// TODO ERRORS
return presignedUrlOutput.futureFileUrl;
},
});
};

export const useFetchFile = (url?: string | null) => {
return useQuery({
queryKey: ['file', url],
queryFn: () => (url ? fetchFile(url, ['name']) : undefined),
enabled: !!url,
});
};
9 changes: 0 additions & 9 deletions src/files/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,6 @@ export const zFieldUploadValue = (acceptedTypes?: UploadFileType[]) =>
}),
}
);

export type UploadSignedUrlInput = z.infer<
ReturnType<typeof zUploadSignedUrlInput>
>;
export const zUploadSignedUrlInput = () =>
z.object({
metadata: z.string().optional(),
});

export type UploadSignedUrlOutput = z.infer<
ReturnType<typeof zUploadSignedUrlOutput>
>;
Expand Down
97 changes: 0 additions & 97 deletions src/files/utils.ts
Original file line number Diff line number Diff line change
@@ -1,102 +1,5 @@
import { UseMutateAsyncFunction } from '@tanstack/react-query';
import { stringify } from 'superjson';

import { env } from '@/env.mjs';

import { UploadSignedUrlInput } from './schemas';

/**
* Fetches a file from the specified URL and returns file information.
* Designed to be used as a `queryFn` in a `useQuery`.
*
* @param url The URL from which the file should be fetched.
* @param [metadata] The metadata of the file you want to retrieve.
* @returns A Promise that resolves to an object containing information about the file.
*
* @example
* // Usage with Tanstack Query's useQuery:
* const fileQuery = useQuery({
queryKey: ['fileKey', url],
queryFn: () => fetchFile(url, ['name']),
enabled: !!url,
});
*/
export const fetchFile = async (url: string, metadata?: string[]) => {
const fileResponse = await fetch(url, {
cache: 'no-cache',
});
if (!fileResponse.ok) {
throw new Error('Could not fetch the file');
}

const lastModifiedDateHeader = fileResponse.headers.get('Last-Modified');
const defaultFileData = {
fileUrl: url,
size: fileResponse.headers.get('Content-Length') ?? undefined,
type: fileResponse.headers.get('Content-Type') ?? undefined,
lastModifiedDate: lastModifiedDateHeader
? new Date(lastModifiedDateHeader)
: new Date(),
};

if (!metadata) {
return defaultFileData;
}

return metadata.reduce((file, metadataKey) => {
return {
...file,
[metadataKey]: fileResponse.headers.get(`x-amz-meta-${metadataKey}`),
};
}, defaultFileData);
};

/**
* Asynchronously uploads a file to a server using a presigned URL.
* Designed to be used as a `mutationFn` in a `useMutation`.
*
* @param getPresignedUrl
* - An asyncMutation that is used to obtain the presigned URL and the future URL where the file will be accessible.
*
* @param file - The file object to upload.
* @param metadata - Optional metadata for the file, which will be sent to the server when generating the presigned URL.
*
* @returns A promise that resolves to an object containing the URL of the uploaded file,
*
* @example
* // Usage with Tanstack Query's useMutation:
* const getPresignedUrl = trpc.routeToGetPresignedUrl.useMutation();
const fileUpload = useMutation({
mutationFn: (file: File) => uploadFile(getPresignedUrl.mutateAsync, file),
});
*/
export const uploadFile = async (
getPresignedUrl: UseMutateAsyncFunction<
{ signedUrl: string; futureFileUrl: string },
unknown,
UploadSignedUrlInput
>,
file: File,
metadata: Record<string, string> = {}
) => {
const { signedUrl, futureFileUrl } = await getPresignedUrl({
metadata: stringify({
name: file.name,
...metadata,
}),
});

await fetch(signedUrl, {
method: 'PUT',
headers: { 'Content-Type': file.type },
body: file,
});

return {
fileUrl: futureFileUrl,
} as const;
};

export const isFileUrlValidBucket = async (url: string) => {
return url.startsWith(env.S3_BUCKET_PUBLIC_URL);
};
2 changes: 2 additions & 0 deletions src/server/router.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createTRPCRouter } from '@/server/config/trpc';
import { accountRouter } from '@/server/routers/account';
import { authRouter } from '@/server/routers/auth';
import { filesRouter } from '@/server/routers/files';
import { oauthRouter } from '@/server/routers/oauth';
import { repositoriesRouter } from '@/server/routers/repositories';
import { usersRouter } from '@/server/routers/users';
Expand All @@ -16,6 +17,7 @@ export const appRouter = createTRPCRouter({
oauth: oauthRouter,
repositories: repositoriesRouter,
users: usersRouter,
files: filesRouter,
});

// export type definition of API
Expand Down
22 changes: 0 additions & 22 deletions src/server/routers/account.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,17 @@
import { TRPCError } from '@trpc/server';
import dayjs from 'dayjs';
import { randomUUID } from 'node:crypto';
import { parse } from 'superjson';
import { z } from 'zod';

import EmailDeleteAccountCode from '@/emails/templates/delete-account-code';
import EmailUpdateAlreadyUsed from '@/emails/templates/email-update-already-used';
import EmailUpdateCode from '@/emails/templates/email-update-code';
import { env } from '@/env.mjs';
import {
zUserAccount,
zUserAccountWithEmail,
} from '@/features/account/schemas';
import { zVerificationCodeValidate } from '@/features/auth/schemas';
import { VALIDATION_TOKEN_EXPIRATION_IN_MINUTES } from '@/features/auth/utils';
import { zUploadSignedUrlInput, zUploadSignedUrlOutput } from '@/files/schemas';
import { isFileUrlValidBucket } from '@/files/utils';
import i18n from '@/lib/i18n/server';
import {
Expand All @@ -24,7 +21,6 @@ import {
} from '@/server/config/auth';
import { sendEmail } from '@/server/config/email';
import { ExtendedTRPCError } from '@/server/config/errors';
import { getS3UploadSignedUrl } from '@/server/config/s3';
import { createTRPCRouter, protectedProcedure } from '@/server/config/trpc';

export const accountRouter = createTRPCRouter({
Expand Down Expand Up @@ -306,22 +302,4 @@ export const accountRouter = createTRPCRouter({

return user;
}),
uploadAvatarPresignedUrl: protectedProcedure()
.meta({
openapi: {
method: 'GET',
path: '/accounts/avatar-upload-presigned-url',
tags: ['accounts', 'files'],
protect: true,
},
})
.input(zUploadSignedUrlInput())
.output(zUploadSignedUrlOutput())
.mutation(async ({ ctx, input }) => {
return await getS3UploadSignedUrl({
key: ctx.user.id,
host: env.S3_BUCKET_PUBLIC_URL,
metadata: parse(input?.metadata ?? ''),
});
}),
});
33 changes: 33 additions & 0 deletions src/server/routers/files.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { z } from 'zod';

import { env } from '@/env.mjs';
import { zUploadSignedUrlOutput } from '@/files/schemas';
import { getS3UploadSignedUrl } from '@/server/config/s3';
import { createTRPCRouter, protectedProcedure } from '@/server/config/trpc';

export const filesRouter = createTRPCRouter({
uploadPresignedUrl: protectedProcedure()
.meta({
openapi: {
method: 'GET',
path: '/files/upload-presigned-url',
tags: ['files'],
protect: true,
},
})
.input(
z
.object({
metadata: z.record(z.string(), z.string()),
})
.optional()
)
.output(zUploadSignedUrlOutput())
.mutation(async ({ input, ctx }) => {
return await getS3UploadSignedUrl({
key: ctx.user.id, // FIX ME
host: env.S3_BUCKET_PUBLIC_URL,
metadata: input?.metadata,
});
}),
});

0 comments on commit 8ce490c

Please sign in to comment.