Skip to content

Commit 11a42cf

Browse files
authored
Merge pull request #2753 from appwrite/fix-dat-860
2 parents 683f49b + f8a04a2 commit 11a42cf

21 files changed

Lines changed: 379 additions & 204 deletions

File tree

src/lib/actions/analytics.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,7 @@ export enum Submit {
277277
DatabaseDelete = 'submit_database_delete',
278278
DatabaseUpdateName = 'submit_database_update_name',
279279
DatabaseImportCsv = 'submit_database_import_csv',
280+
DatabaseBackupDelete = 'submit_database_backup_delete',
280281

281282
ColumnCreate = 'submit_column_create',
282283
ColumnUpdate = 'submit_column_update',

src/lib/components/multiSelectTable.svelte

Lines changed: 93 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
<script lang="ts" module>
2-
export type DeleteOperationState = Error | void;
2+
export type DeleteOperationState = {
3+
error?: Error;
4+
deleted: string[];
5+
} | void;
6+
7+
export type DeleteOperation = (
8+
deleteFn: (id: string) => Promise<unknown>,
9+
batchSize?: number
10+
) => Promise<Exclude<DeleteOperationState, void>>;
311
</script>
412

513
<script lang="ts">
@@ -40,7 +48,14 @@
4048
children: Snippet<[root: TableRootProps]>;
4149
deleteContent?: Snippet<[count: number]>;
4250
deleteContentNotice?: Snippet;
43-
onDelete?: (selectedRows: string[]) => Promise<DeleteOperationState> | DeleteOperationState;
51+
onDelete?: (
52+
batchDelete: DeleteOperation,
53+
/**
54+
* this is useful when you have a custom deletion logic
55+
* and the default `batchDelete` helper doesn't fit the use-case!
56+
*/
57+
selectedRows: string[]
58+
) => Promise<DeleteOperationState> | DeleteOperationState;
4459
onCancel?: () => Promise<void> | void;
4560
} = $props();
4661
@@ -49,10 +64,8 @@
4964
let onDeleteError: string | null = $state(null);
5065
let showConfirmDeletion: boolean = $state(false);
5166
52-
function notifySuccess() {
67+
function notifySuccess(count: number) {
5368
if (!showSuccessNotification) return;
54-
55-
const count = selectedRows.length;
5669
if (count === 0) return;
5770
5871
const label = `${resource}${count > 1 ? 's' : ''}`;
@@ -71,6 +84,75 @@
7184
7285
return `${resource}s`;
7386
}
87+
88+
async function batchDelete(
89+
ids: string[],
90+
deleteFn: (id: string) => Promise<unknown>,
91+
batchSize: number | undefined = undefined
92+
): Promise<Exclude<DeleteOperationState, void>> {
93+
const deleted: string[] = [];
94+
let firstError: Error | undefined;
95+
96+
// prevent infinite loop
97+
if (batchSize !== undefined) {
98+
batchSize = Math.max(1, Math.floor(Math.abs(batchSize)));
99+
}
100+
101+
async function processBatch(batch: string[]) {
102+
// build promises
103+
const results = await Promise.allSettled(batch.map((id) => deleteFn(id)));
104+
105+
results.forEach((result, index) => {
106+
if (result.status === 'fulfilled') {
107+
// success, log it!
108+
deleted.push(batch[index]);
109+
} else if (!firstError) {
110+
// error
111+
firstError =
112+
result.reason instanceof Error
113+
? result.reason
114+
: new Error(String(result.reason));
115+
}
116+
});
117+
}
118+
119+
// batch when needed.
120+
// example: >= 100 items to delete!
121+
if (batchSize && batchSize < ids.length) {
122+
for (let i = 0; i < ids.length; i += batchSize) {
123+
const batch = ids.slice(i, i + batchSize);
124+
await processBatch(batch);
125+
}
126+
} else {
127+
await processBatch(ids);
128+
}
129+
130+
return {
131+
deleted,
132+
error: firstError
133+
};
134+
}
135+
136+
async function consumeDeleteOperation() {
137+
const state = await onDelete?.((deleteFn, batchSize) => {
138+
return batchDelete(selectedRows, deleteFn, batchSize);
139+
}, selectedRows);
140+
141+
if (!state) {
142+
return false;
143+
}
144+
145+
const deletedCount = state.deleted.length;
146+
selectedRows = selectedRows.filter((id) => !state.deleted.includes(id));
147+
148+
if (state.error) {
149+
onDeleteError = `Some ${getPluralResource()} were not deleted. Error: ${state.error.message}`;
150+
return false;
151+
}
152+
153+
notifySuccess(deletedCount);
154+
return true;
155+
}
74156
</script>
75157

76158
{#key computeKey}
@@ -104,13 +186,7 @@
104186
if (confirmDeletion) {
105187
showConfirmDeletion = true;
106188
} else {
107-
const state = await onDelete?.(selectedRows);
108-
if (state instanceof Error) {
109-
// user should handle error on their own!
110-
} else {
111-
notifySuccess();
112-
selectedRows = [];
113-
}
189+
await consumeDeleteOperation();
114190
}
115191
}}>Delete</Button>
116192
</svelte:fragment>
@@ -129,16 +205,13 @@
129205
disableModal = true;
130206
onDeleteError = null;
131207

132-
const state = await onDelete?.(selectedRows);
133-
if (state instanceof Error) {
134-
disableModal = false;
135-
onDeleteError = state.message || `Failed to delete ${resource}s`;
136-
} else {
137-
notifySuccess();
138-
selectedRows = [];
139-
disableModal = false;
208+
const allDeleted = await consumeDeleteOperation();
209+
210+
if (allDeleted) {
140211
showConfirmDeletion = false;
141212
}
213+
214+
disableModal = false;
142215
}}>
143216
<Typography.Text>
144217
{@const selectionCount = selectedRows.length}

src/routes/(console)/project-[region]-[project]/auth/+page.svelte

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import {
1010
AvatarInitials,
1111
Copy,
12+
type DeleteOperation,
1213
type DeleteOperationState,
1314
Empty,
1415
EmptySearch,
@@ -60,20 +61,22 @@
6061
);
6162
}
6263
63-
async function handleDelete(selectedRows: string[]): Promise<DeleteOperationState> {
64-
const promises = selectedRows.map((userId) => {
65-
return sdk.forProject(page.params.region, page.params.project).users.delete({ userId });
66-
});
64+
async function handleDelete(batchDelete: DeleteOperation): Promise<DeleteOperationState> {
65+
const result = await batchDelete((userId) =>
66+
sdk.forProject(page.params.region, page.params.project).users.delete({ userId })
67+
);
6768
6869
try {
69-
await Promise.all(promises);
70-
trackEvent(Submit.UserDelete, { total: selectedRows.length });
71-
} catch (error) {
72-
trackError(error, Submit.UserDelete);
73-
return error;
70+
if (result.error) {
71+
trackError(result.error, Submit.UserDelete);
72+
} else {
73+
trackEvent(Submit.UserDelete, { total: result.deleted.length });
74+
}
7475
} finally {
7576
await invalidate(Dependencies.USERS);
7677
}
78+
79+
return result;
7780
}
7881
</script>
7982

src/routes/(console)/project-[region]-[project]/auth/teams/+page.svelte

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
SearchQuery,
1313
PaginationWithLimit,
1414
type DeleteOperationState,
15+
type DeleteOperation,
1516
MultiSelectionTable
1617
} from '$lib/components';
1718
import Create from '../createTeam.svelte';
@@ -44,20 +45,22 @@
4445
);
4546
};
4647
47-
async function handleDelete(selectedRows: string[]): Promise<DeleteOperationState> {
48-
const promises = selectedRows.map((teamId) => {
49-
return sdk.forProject(page.params.region, page.params.project).teams.delete({ teamId });
50-
});
48+
async function handleDelete(batchDelete: DeleteOperation): Promise<DeleteOperationState> {
49+
const result = await batchDelete((teamId) =>
50+
sdk.forProject(page.params.region, page.params.project).teams.delete({ teamId })
51+
);
5152
5253
try {
53-
await Promise.all(promises);
54-
trackEvent(Submit.TeamDelete, { total: selectedRows.length });
55-
} catch (error) {
56-
trackError(error, Submit.TeamDelete);
57-
return error;
54+
if (result.error) {
55+
trackError(result.error, Submit.TeamDelete);
56+
} else {
57+
trackEvent(Submit.TeamDelete, { total: result.deleted.length });
58+
}
5859
} finally {
5960
await invalidate(Dependencies.TEAMS);
6061
}
62+
63+
return result;
6164
}
6265
</script>
6366

src/routes/(console)/project-[region]-[project]/auth/teams/team-[team]/members/+page.svelte

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
AvatarInitials,
77
PaginationWithLimit,
88
MultiSelectionTable,
9+
type DeleteOperation,
910
type DeleteOperationState
1011
} from '$lib/components';
1112
import { Button } from '$lib/elements/forms';
@@ -29,23 +30,25 @@
2930
let showDelete = $state(false);
3031
let selectedMembership: Models.Membership | null = $state(null);
3132
32-
async function handleBulkDelete(selectedRows: string[]): Promise<DeleteOperationState> {
33-
const promises = selectedRows.map((membershipId) => {
34-
return sdk.forProject(page.params.region, page.params.project).teams.deleteMembership({
33+
async function handleBulkDelete(batchDelete: DeleteOperation): Promise<DeleteOperationState> {
34+
const result = await batchDelete((membershipId) =>
35+
sdk.forProject(page.params.region, page.params.project).teams.deleteMembership({
3536
teamId: page.params.team,
3637
membershipId
37-
});
38-
});
38+
})
39+
);
3940
4041
try {
41-
await Promise.all(promises);
42-
trackEvent(Submit.MembershipUpdate, { total: selectedRows.length });
43-
} catch (error) {
44-
trackError(error, Submit.MembershipUpdate);
45-
return error;
42+
if (result.error) {
43+
trackError(result.error, Submit.MembershipUpdate);
44+
} else {
45+
trackEvent(Submit.MembershipUpdate, { total: result.deleted.length });
46+
}
4647
} finally {
4748
await invalidate(Dependencies.MEMBERSHIPS);
4849
}
50+
51+
return result;
4952
}
5053
</script>
5154

src/routes/(console)/project-[region]-[project]/auth/user-[user]/identities/table.svelte

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
<script lang="ts">
2-
import { type DeleteOperationState, Id, MultiSelectionTable } from '$lib/components';
2+
import {
3+
type DeleteOperationState,
4+
type DeleteOperation,
5+
Id,
6+
MultiSelectionTable
7+
} from '$lib/components';
38
import type { PageData } from './$types';
49
import DualTimeView from '$lib/components/dualTimeView.svelte';
510
import { sdk } from '$lib/stores/sdk';
@@ -21,22 +26,24 @@
2126
columns: Column[];
2227
} = $props();
2328
24-
async function handleDelete(selectedRows: string[]): Promise<DeleteOperationState> {
25-
const promises = selectedRows.map((id) => {
26-
return sdk
29+
async function handleDelete(batchDelete: DeleteOperation): Promise<DeleteOperationState> {
30+
const result = await batchDelete((id) =>
31+
sdk
2732
.forProject(page.params.region, page.params.project)
28-
.users.deleteIdentity({ identityId: id });
29-
});
33+
.users.deleteIdentity({ identityId: id })
34+
);
3035
3136
try {
32-
await Promise.all(promises);
33-
trackEvent(Submit.UserIdentityDelete, { total: selectedRows.length });
34-
} catch (error) {
35-
trackError(error, Submit.UserIdentityDelete);
36-
return error;
37+
if (result.error) {
38+
trackError(result.error, Submit.UserIdentityDelete);
39+
} else {
40+
trackEvent(Submit.UserIdentityDelete, { total: result.deleted.length });
41+
}
3742
} finally {
3843
await invalidate(Dependencies.USER_IDENTITIES);
3944
}
45+
46+
return result;
4047
}
4148
</script>
4249

src/routes/(console)/project-[region]-[project]/auth/user-[user]/memberships/+page.svelte

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import {
55
AvatarInitials,
66
type DeleteOperationState,
7+
type DeleteOperation,
78
MultiSelectionTable
89
} from '$lib/components';
910
import { Button } from '$lib/elements/forms';
@@ -23,29 +24,31 @@
2324
let showDelete = $state(false);
2425
let selectedMembership: Models.Membership | null = $state(null);
2526
26-
async function handleBulkDelete(selectedRows: string[]): Promise<DeleteOperationState> {
27+
async function handleBulkDelete(batchDelete: DeleteOperation): Promise<DeleteOperationState> {
2728
// Precompute a lookup map from membershipId to teamId for efficient access
2829
const membershipIdToTeamId: Record<string, string> = {};
2930
for (const membership of data.memberships.memberships) {
3031
membershipIdToTeamId[membership.$id] = membership.teamId;
3132
}
3233
33-
const promises = selectedRows.map((membershipId) =>
34+
const result = await batchDelete((membershipId) =>
3435
sdk.forProject(page.params.region, page.params.project).teams.deleteMembership({
3536
teamId: membershipIdToTeamId[membershipId] || '',
3637
membershipId
3738
})
3839
);
3940
4041
try {
41-
await Promise.all(promises);
42-
trackEvent(Submit.MembershipUpdate, { total: selectedRows.length });
43-
} catch (error) {
44-
trackError(error, Submit.MembershipUpdate);
45-
return error;
42+
if (result.error) {
43+
trackError(result.error, Submit.MembershipUpdate);
44+
} else {
45+
trackEvent(Submit.MembershipUpdate, { total: result.deleted.length });
46+
}
4647
} finally {
4748
await invalidate(Dependencies.MEMBERSHIPS);
4849
}
50+
51+
return result;
4952
}
5053
</script>
5154

0 commit comments

Comments
 (0)