Skip to content

Commit

Permalink
Feat: PR(1) added centralized helper function for minio-upload using …
Browse files Browse the repository at this point in the history
…pre-signed url (#3762)

* fixed conflict

* fixed conflict
  • Loading branch information
iamanishx authored Mar 1, 2025
1 parent 8688f5b commit 9eeb840
Show file tree
Hide file tree
Showing 6 changed files with 268 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[Admin Docs](/)

***

# Variable: PRESIGNED\_URL

> `const` **PRESIGNED\_URL**: `DocumentNode`
Defined in: [src/GraphQl/Mutations/mutations.ts:786](https://github.com/PalisadoesFoundation/talawa-admin/blob/main/src/GraphQl/Mutations/mutations.ts#L786)
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
Defined in: [src/components/LeftDrawerOrg/LeftDrawerOrg.tsx:34](https://github.com/PalisadoesFoundation/talawa-admin/blob/main/src/components/LeftDrawerOrg/LeftDrawerOrg.tsx#L34)

LeftDrawerOrg component for displaying organization details and navigation options.
LeftDrawerOrg component for displaying organization details and options.

## Parameters

Expand Down
13 changes: 13 additions & 0 deletions docs/docs/auto-docs/utils/MinioUpload/functions/useMinioUpload.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[Admin Docs](/)

***

# Function: useMinioUpload()

> **useMinioUpload**(): `InterfaceMinioUpload`
Defined in: [src/utils/MinioUpload.ts:11](https://github.com/PalisadoesFoundation/talawa-admin/blob/main/src/utils/MinioUpload.ts#L11)

## Returns

`InterfaceMinioUpload`
10 changes: 10 additions & 0 deletions src/GraphQl/Mutations/mutations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -782,3 +782,13 @@ export {
DELETE_VENUE_MUTATION,
UPDATE_VENUE_MUTATION,
} from './VenueMutations';

export const PRESIGNED_URL = gql`
mutation createPresignedUrl($input: MutationCreatePresignedUrlInput!) {
createPresignedUrl(input: $input) {
fileUrl
presignedUrl
objectName
}
}
`;
185 changes: 185 additions & 0 deletions src/utils/MinioUpload.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
import React from 'react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { MockedProvider } from '@apollo/client/testing';
import { PRESIGNED_URL } from 'GraphQl/Mutations/mutations';
import { useMinioUpload } from './MinioUpload';
import { vi } from 'vitest';

const TestComponent = ({
onUploadComplete,
}: {
onUploadComplete: (result: { fileUrl: string; objectName: string }) => void;
}): JSX.Element => {
const { uploadFileToMinio } = useMinioUpload();
const [status, setStatus] = React.useState('idle');

const handleFileChange = async (
e: React.ChangeEvent<HTMLInputElement>,
): Promise<void> => {
const files = e.target.files;
if (!files || !files[0]) return;
const file = files[0];

setStatus('uploading');
try {
const result = await uploadFileToMinio(file, 'test-org-id');
setStatus('success');
onUploadComplete(result);
} catch (error) {
setStatus('error');
console.error(error);
}
};

return (
<div>
<input type="file" data-testid="file-input" onChange={handleFileChange} />
<div data-testid="status">{status}</div>
</div>
);
};

describe('Minio Upload Integration', (): void => {
const successMocks = [
{
request: {
query: PRESIGNED_URL,
variables: {
input: {
fileName: 'test.png',
fileType: 'image/png',
organizationId: 'test-org-id',
},
},
},
result: {
data: {
createPresignedUrl: {
presignedUrl: 'https://minio-test.com/upload/url',
fileUrl: 'https://minio-test.com/file/url',
objectName: 'test-object-name',
},
},
},
},
];

beforeEach((): void => {
global.fetch = vi.fn(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve({}),
} as Response),
);
});

afterEach(() => {
vi.resetAllMocks();
});

it('should upload a file and call onUploadComplete with the expected result', async (): Promise<void> => {
const handleComplete = vi.fn();

render(
<MockedProvider mocks={successMocks} addTypename={false}>
<TestComponent onUploadComplete={handleComplete} />
</MockedProvider>,
);

const file = new File(['dummy content'], 'test.png', { type: 'image/png' });
const input = screen.getByTestId('file-input') as HTMLInputElement;

Object.defineProperty(input, 'files', {
value: [file],
writable: false,
});

fireEvent.change(input);

// Expect initial status to be "uploading"
expect(screen.getByTestId('status').textContent).toBe('uploading');

await waitFor(() => {
expect(screen.getByTestId('status').textContent).toBe('success');
});

expect(handleComplete).toHaveBeenCalledWith({
fileUrl: 'https://minio-test.com/file/url',
objectName: 'test-object-name',
});
});

it('should set status to error if mutation returns no data or missing createPresignedUrl', async () => {
const errorMock = [
{
request: {
query: PRESIGNED_URL,
variables: {
input: {
fileName: 'test.png',
fileType: 'image/png',
organizationId: 'test-org-id',
},
},
},
result: {
data: {
createPresignedUrl: null,
},
},
},
];

const handleComplete = vi.fn();

render(
<MockedProvider mocks={errorMock} addTypename={false}>
<TestComponent onUploadComplete={handleComplete} />
</MockedProvider>,
);

const file = new File(['dummy content'], 'test.png', { type: 'image/png' });
const input = screen.getByTestId('file-input') as HTMLInputElement;
Object.defineProperty(input, 'files', {
value: [file],
writable: false,
});

fireEvent.change(input);

await waitFor(() => {
expect(screen.getByTestId('status').textContent).toBe('error');
});

expect(handleComplete).not.toHaveBeenCalled();
});

it('should set status to error when file upload fails', async () => {
(
global.fetch as unknown as {
mockImplementationOnce: (fn: () => Promise<Response>) => void;
}
).mockImplementationOnce(() => Promise.resolve({ ok: false } as Response));
const handleComplete = vi.fn();

render(
<MockedProvider mocks={successMocks} addTypename={false}>
<TestComponent onUploadComplete={handleComplete} />
</MockedProvider>,
);

const file = new File(['dummy content'], 'test.png', { type: 'image/png' });
const input = screen.getByTestId('file-input') as HTMLInputElement;
Object.defineProperty(input, 'files', {
value: [file],
writable: false,
});
fireEvent.change(input);

await waitFor(() => {
expect(screen.getByTestId('status').textContent).toBe('error');
});

expect(handleComplete).not.toHaveBeenCalled();
});
});
50 changes: 50 additions & 0 deletions src/utils/MinioUpload.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { PRESIGNED_URL } from 'GraphQl/Mutations/mutations';
import { useMutation } from '@apollo/client';

interface InterfaceMinioUpload {
uploadFileToMinio: (
file: File,
organizationId: string,
) => Promise<{ fileUrl: string; objectName: string }>;
}

export const useMinioUpload = (): InterfaceMinioUpload => {
const [generatePresignedUrl] = useMutation(PRESIGNED_URL);

const uploadFileToMinio = async (
file: File,
organizationId: string,
): Promise<{ fileUrl: string; objectName: string }> => {
// 1. Call the mutation to get presignedUrl & fileUrl
const { data } = await generatePresignedUrl({
variables: {
input: {
fileName: file.name,
fileType: file.type,
organizationId: organizationId,
},
},
});
if (!data || !data.createPresignedUrl) {
throw new Error('Failed to get presigned URL');
}
const { presignedUrl, fileUrl, objectName } = data.createPresignedUrl;

// 2. Upload the file directly to MinIO using the presigned URL
const response = await fetch(presignedUrl, {
method: 'PUT',
body: file,
headers: {
'Content-Type': file.type,
},
});

if (!response.ok) {
throw new Error('File upload failed');
}

return { fileUrl, objectName };
};

return { uploadFileToMinio };
};

0 comments on commit 9eeb840

Please sign in to comment.