@scality/data-browser-library 1.1.5 → 1.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,203 @@
1
+ import { act } from "@testing-library/react";
2
+ import { renderHookWithWrapper } from "../../test/testUtils.js";
3
+ import { useS3Client } from "../useS3Client.js";
4
+ import { useDeleteFolder } from "../useDeleteFolder.js";
5
+ jest.mock('../useS3Client');
6
+ const mockSend = jest.fn();
7
+ const mockUseS3Client = jest.mocked(useS3Client);
8
+ describe('useDeleteFolder', ()=>{
9
+ beforeEach(()=>{
10
+ jest.clearAllMocks();
11
+ mockUseS3Client.mockReturnValue({
12
+ send: mockSend
13
+ });
14
+ });
15
+ it('deletes an empty folder with a single version', async ()=>{
16
+ mockSend.mockResolvedValueOnce({
17
+ Versions: [
18
+ {
19
+ Key: 'folder/',
20
+ VersionId: 'v1'
21
+ }
22
+ ],
23
+ DeleteMarkers: [],
24
+ CommonPrefixes: [],
25
+ IsTruncated: false
26
+ });
27
+ mockSend.mockResolvedValueOnce({
28
+ Deleted: [
29
+ {
30
+ Key: 'folder/'
31
+ }
32
+ ],
33
+ Errors: []
34
+ });
35
+ const { result } = renderHookWithWrapper(()=>useDeleteFolder());
36
+ await act(()=>result.current.mutateAsync({
37
+ Bucket: 'my-bucket',
38
+ FolderKey: 'folder/'
39
+ }));
40
+ expect(mockSend).toHaveBeenCalledTimes(2);
41
+ const deleteCommand = mockSend.mock.calls[1][0];
42
+ expect(deleteCommand.input).toEqual({
43
+ Bucket: 'my-bucket',
44
+ Delete: {
45
+ Objects: [
46
+ {
47
+ Key: 'folder/',
48
+ VersionId: 'v1'
49
+ }
50
+ ]
51
+ }
52
+ });
53
+ });
54
+ it('deletes folder with multiple versions and delete markers', async ()=>{
55
+ mockSend.mockResolvedValueOnce({
56
+ Versions: [
57
+ {
58
+ Key: 'folder/',
59
+ VersionId: 'v1'
60
+ },
61
+ {
62
+ Key: 'folder/',
63
+ VersionId: 'v2'
64
+ }
65
+ ],
66
+ DeleteMarkers: [
67
+ {
68
+ Key: 'folder/',
69
+ VersionId: 'dm1'
70
+ }
71
+ ],
72
+ CommonPrefixes: [],
73
+ IsTruncated: false
74
+ });
75
+ mockSend.mockResolvedValueOnce({
76
+ Deleted: [],
77
+ Errors: []
78
+ });
79
+ const { result } = renderHookWithWrapper(()=>useDeleteFolder());
80
+ await act(()=>result.current.mutateAsync({
81
+ Bucket: 'my-bucket',
82
+ FolderKey: 'folder/'
83
+ }));
84
+ const deleteCommand = mockSend.mock.calls[1][0];
85
+ expect(deleteCommand.input.Delete.Objects).toEqual([
86
+ {
87
+ Key: 'folder/',
88
+ VersionId: 'v1'
89
+ },
90
+ {
91
+ Key: 'folder/',
92
+ VersionId: 'v2'
93
+ },
94
+ {
95
+ Key: 'folder/',
96
+ VersionId: 'dm1'
97
+ }
98
+ ]);
99
+ });
100
+ it('throws when folder has sub-folders (CommonPrefixes)', async ()=>{
101
+ mockSend.mockResolvedValueOnce({
102
+ Versions: [
103
+ {
104
+ Key: 'folder/',
105
+ VersionId: 'v1'
106
+ }
107
+ ],
108
+ DeleteMarkers: [],
109
+ CommonPrefixes: [
110
+ {
111
+ Prefix: 'folder/sub/'
112
+ }
113
+ ],
114
+ IsTruncated: false
115
+ });
116
+ const { result } = renderHookWithWrapper(()=>useDeleteFolder());
117
+ await expect(act(()=>result.current.mutateAsync({
118
+ Bucket: 'my-bucket',
119
+ FolderKey: 'folder/'
120
+ }))).rejects.toThrow('Cannot delete folder: The folder is not empty');
121
+ });
122
+ it('throws when folder contains other objects', async ()=>{
123
+ mockSend.mockResolvedValueOnce({
124
+ Versions: [
125
+ {
126
+ Key: 'folder/',
127
+ VersionId: 'v1'
128
+ },
129
+ {
130
+ Key: 'folder/file.txt',
131
+ VersionId: 'v2'
132
+ }
133
+ ],
134
+ DeleteMarkers: [],
135
+ CommonPrefixes: [],
136
+ IsTruncated: false
137
+ });
138
+ const { result } = renderHookWithWrapper(()=>useDeleteFolder());
139
+ await expect(act(()=>result.current.mutateAsync({
140
+ Bucket: 'my-bucket',
141
+ FolderKey: 'folder/'
142
+ }))).rejects.toThrow('Cannot delete folder: The folder is not empty');
143
+ });
144
+ it('throws when ListObjectVersions response is truncated', async ()=>{
145
+ mockSend.mockResolvedValueOnce({
146
+ Versions: [
147
+ {
148
+ Key: 'folder/',
149
+ VersionId: 'v1'
150
+ }
151
+ ],
152
+ DeleteMarkers: [],
153
+ CommonPrefixes: [],
154
+ IsTruncated: true
155
+ });
156
+ const { result } = renderHookWithWrapper(()=>useDeleteFolder());
157
+ await expect(act(()=>result.current.mutateAsync({
158
+ Bucket: 'my-bucket',
159
+ FolderKey: 'folder/'
160
+ }))).rejects.toThrow('Cannot delete folder: The folder is not empty');
161
+ });
162
+ it('throws when DeleteObjects returns Errors', async ()=>{
163
+ mockSend.mockResolvedValueOnce({
164
+ Versions: [
165
+ {
166
+ Key: 'folder/',
167
+ VersionId: 'v1'
168
+ }
169
+ ],
170
+ DeleteMarkers: [],
171
+ CommonPrefixes: [],
172
+ IsTruncated: false
173
+ });
174
+ mockSend.mockResolvedValueOnce({
175
+ Errors: [
176
+ {
177
+ Key: 'folder/',
178
+ Code: 'AccessDenied',
179
+ Message: 'Access Denied because object protected by object lock.'
180
+ }
181
+ ]
182
+ });
183
+ const { result } = renderHookWithWrapper(()=>useDeleteFolder());
184
+ await expect(act(()=>result.current.mutateAsync({
185
+ Bucket: 'my-bucket',
186
+ FolderKey: 'folder/'
187
+ }))).rejects.toThrow('Access Denied because object protected by object lock.');
188
+ });
189
+ it('does not call DeleteObjects when folder has no versions', async ()=>{
190
+ mockSend.mockResolvedValueOnce({
191
+ Versions: [],
192
+ DeleteMarkers: [],
193
+ CommonPrefixes: [],
194
+ IsTruncated: false
195
+ });
196
+ const { result } = renderHookWithWrapper(()=>useDeleteFolder());
197
+ await act(()=>result.current.mutateAsync({
198
+ Bucket: 'my-bucket',
199
+ FolderKey: 'folder/'
200
+ }));
201
+ expect(mockSend).toHaveBeenCalledTimes(1);
202
+ });
203
+ });
@@ -9,6 +9,7 @@ export { useBatchObjectLegalHold } from './useBatchObjectLegalHold';
9
9
  export { type BucketConfigEditorConfig, type BucketConfigEditorResult, useBucketConfigEditor } from './useBucketConfigEditor';
10
10
  export { useBucketLocations } from './useBucketLocations';
11
11
  export { useDeleteBucketConfigRule } from './useDeleteBucketConfigRule';
12
+ export { useDeleteFolder } from './useDeleteFolder';
12
13
  export { useEmptyBucket } from './useEmptyBucket';
13
14
  export { useFeatures } from './useFeatures';
14
15
  export { useISVBucketStatus } from './useISVBucketDetection';
@@ -8,6 +8,7 @@ import { useBatchObjectLegalHold } from "./useBatchObjectLegalHold.js";
8
8
  import { useBucketConfigEditor } from "./useBucketConfigEditor.js";
9
9
  import { useBucketLocations } from "./useBucketLocations.js";
10
10
  import { useDeleteBucketConfigRule } from "./useDeleteBucketConfigRule.js";
11
+ import { useDeleteFolder } from "./useDeleteFolder.js";
11
12
  import { useEmptyBucket } from "./useEmptyBucket.js";
12
13
  import { useFeatures } from "./useFeatures.js";
13
14
  import { useISVBucketStatus } from "./useISVBucketDetection.js";
@@ -17,4 +18,4 @@ import { useS3Client } from "./useS3Client.js";
17
18
  import { useS3ConfigSwitch } from "./useS3ConfigSwitch.js";
18
19
  import { useSupportedNotificationEvents } from "./useSupportedNotificationEvents.js";
19
20
  import { useTableRowSelection } from "./useTableRowSelection.js";
20
- export { getAccessibleBucketsStorageKey, getLimitedAccessFlagKey, setLimitedAccessFlag, useAccessibleBuckets, useBatchObjectLegalHold, useBucketConfigEditor, useBucketLocations, useBuckets, useCopyObject, useCreateBucket, useCreateFolder, useDeleteBucket, useDeleteBucketConfigRule, useDeleteBucketCors, useDeleteBucketLifecycle, useDeleteBucketPolicy, useDeleteBucketReplication, useDeleteBucketTagging, useDeleteObject, useDeleteObjectTagging, useDeleteObjects, useDeletePublicAccessBlock, useEmptyBucket, useFeatures, useGetBucketAcl, useGetBucketCors, useGetBucketEncryption, useGetBucketLifecycle, useGetBucketLocation, useGetBucketNotification, useGetBucketObjectLockConfiguration, useGetBucketPolicy, useGetBucketReplication, useGetBucketTagging, useGetBucketVersioning, useGetObject, useGetObjectAttributes, useGetObjectTorrent, useGetPresignedDownload, useGetPresignedPost, useGetPresignedUpload, useGetPublicAccessBlock, useISVBucketStatus, useIsBucketEmpty, useLimitedAccessFlow, useListMultipartUploads, useListObjectVersions, useListObjects, useLoginMutation, useObjectAcl, useObjectLegalHold, useObjectMetadata, useObjectRetention, useObjectTagging, usePutObject, usePutPublicAccessBlock, useRestoreObject, useS3Client, useS3ConfigSwitch, useSearchObjects, useSearchObjectsVersions, useSelectObjectContent, useSetBucketAcl, useSetBucketCors, useSetBucketEncryption, useSetBucketLifecycle, useSetBucketNotification, useSetBucketObjectLockConfiguration, useSetBucketPolicy, useSetBucketReplication, useSetBucketTagging, useSetBucketVersioning, useSetObjectAcl, useSetObjectLegalHold, useSetObjectRetention, useSetObjectTagging, useSupportedNotificationEvents, useTableRowSelection, useUploadObjects, useValidateBucketAccess };
21
+ export { getAccessibleBucketsStorageKey, getLimitedAccessFlagKey, setLimitedAccessFlag, useAccessibleBuckets, useBatchObjectLegalHold, useBucketConfigEditor, useBucketLocations, useBuckets, useCopyObject, useCreateBucket, useCreateFolder, useDeleteBucket, useDeleteBucketConfigRule, useDeleteBucketCors, useDeleteBucketLifecycle, useDeleteBucketPolicy, useDeleteBucketReplication, useDeleteBucketTagging, useDeleteFolder, useDeleteObject, useDeleteObjectTagging, useDeleteObjects, useDeletePublicAccessBlock, useEmptyBucket, useFeatures, useGetBucketAcl, useGetBucketCors, useGetBucketEncryption, useGetBucketLifecycle, useGetBucketLocation, useGetBucketNotification, useGetBucketObjectLockConfiguration, useGetBucketPolicy, useGetBucketReplication, useGetBucketTagging, useGetBucketVersioning, useGetObject, useGetObjectAttributes, useGetObjectTorrent, useGetPresignedDownload, useGetPresignedPost, useGetPresignedUpload, useGetPublicAccessBlock, useISVBucketStatus, useIsBucketEmpty, useLimitedAccessFlow, useListMultipartUploads, useListObjectVersions, useListObjects, useLoginMutation, useObjectAcl, useObjectLegalHold, useObjectMetadata, useObjectRetention, useObjectTagging, usePutObject, usePutPublicAccessBlock, useRestoreObject, useS3Client, useS3ConfigSwitch, useSearchObjects, useSearchObjectsVersions, useSelectObjectContent, useSetBucketAcl, useSetBucketCors, useSetBucketEncryption, useSetBucketLifecycle, useSetBucketNotification, useSetBucketObjectLockConfiguration, useSetBucketPolicy, useSetBucketReplication, useSetBucketTagging, useSetBucketVersioning, useSetObjectAcl, useSetObjectLegalHold, useSetObjectRetention, useSetObjectTagging, useSupportedNotificationEvents, useTableRowSelection, useUploadObjects, useValidateBucketAccess };
@@ -0,0 +1,6 @@
1
+ interface DeleteFolderInput {
2
+ Bucket: string;
3
+ FolderKey: string;
4
+ }
5
+ export declare const useDeleteFolder: (options?: Omit<import("@tanstack/react-query").UseMutationOptions<void, import("..").EnhancedS3Error, DeleteFolderInput, unknown>, "mutationFn"> | undefined) => import("@tanstack/react-query").UseMutationResult<void, import("..").EnhancedS3Error, DeleteFolderInput>;
6
+ export {};
@@ -0,0 +1,40 @@
1
+ import { DeleteObjectsCommand, ListObjectVersionsCommand } from "@aws-sdk/client-s3";
2
+ import { useCreateS3FunctionMutationHook } from "./factories/useCreateS3MutationHook.js";
3
+ async function deleteFolder(s3Client, input) {
4
+ const { Bucket, FolderKey } = input;
5
+ const listResponse = await s3Client.send(new ListObjectVersionsCommand({
6
+ Bucket,
7
+ Prefix: FolderKey,
8
+ Delimiter: '/'
9
+ }));
10
+ const versions = listResponse.Versions ?? [];
11
+ const deleteMarkers = listResponse.DeleteMarkers ?? [];
12
+ const commonPrefixes = listResponse.CommonPrefixes ?? [];
13
+ const filteredVersions = versions.filter((v)=>v.Key === FolderKey);
14
+ const filteredDeleteMarkers = deleteMarkers.filter((v)=>v.Key === FolderKey);
15
+ if (listResponse.IsTruncated || commonPrefixes.length > 0 || filteredVersions.length + filteredDeleteMarkers.length < versions.length + deleteMarkers.length) throw new Error('Cannot delete folder: The folder is not empty');
16
+ const objects = [
17
+ ...filteredVersions.map((v)=>({
18
+ Key: v.Key,
19
+ VersionId: v.VersionId
20
+ })),
21
+ ...filteredDeleteMarkers.map((v)=>({
22
+ Key: v.Key,
23
+ VersionId: v.VersionId
24
+ }))
25
+ ];
26
+ if (objects.length > 0) {
27
+ const deleteResponse = await s3Client.send(new DeleteObjectsCommand({
28
+ Bucket,
29
+ Delete: {
30
+ Objects: objects
31
+ }
32
+ }));
33
+ if (deleteResponse.Errors && deleteResponse.Errors.length > 0) {
34
+ const firstError = deleteResponse.Errors[0];
35
+ throw new Error(firstError.Message ?? 'Failed to delete folder');
36
+ }
37
+ }
38
+ }
39
+ const useDeleteFolder = useCreateS3FunctionMutationHook(deleteFolder);
40
+ export { useDeleteFolder };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scality/data-browser-library",
3
- "version": "1.1.5",
3
+ "version": "1.1.7",
4
4
  "description": "A modular React component library for browsing S3 buckets and objects",
5
5
  "type": "module",
6
6
  "types": "./dist/index.d.ts",