fix: block if already ongoing or waiting

This commit is contained in:
Charles GTE
2026-07-05 14:06:40 +02:00
parent 503d5469d4
commit ff65590c97
3 changed files with 46 additions and 10 deletions
@@ -1,6 +1,7 @@
"use server"; "use server";
import {z} from "zod"; import {z} from "zod";
import {and, inArray, isNull} from "drizzle-orm";
import {db} from "@/db"; import {db} from "@/db";
import * as drizzleDb from "@/db"; import * as drizzleDb from "@/db";
import {ServerActionResult} from "@/types/action-type"; import {ServerActionResult} from "@/types/action-type";
@@ -14,17 +15,35 @@ const bulkSchema = z.object({
export const bulkBackupAction = userAction export const bulkBackupAction = userAction
.schema(bulkSchema) .schema(bulkSchema)
.action(async ({parsedInput}): Promise<ServerActionResult<{queued: number}>> => { .action(async ({parsedInput}): Promise<ServerActionResult<{queued: number; skipped: {databaseId: string; reason: string}[]}>> => {
try { try {
const {projectId, databaseIds} = parsedInput; const {projectId, databaseIds} = parsedInput;
await assertDatabasesInOrgProject(projectId, databaseIds); await assertDatabasesInOrgProject(projectId, databaseIds);
await db
.insert(drizzleDb.schemas.backup) const activeBackups = await db.query.backup.findMany({
.values(databaseIds.map((databaseId) => ({databaseId, status: "waiting" as const}))); where: and(
inArray(drizzleDb.schemas.backup.databaseId, databaseIds),
inArray(drizzleDb.schemas.backup.status, ["waiting", "ongoing"]),
isNull(drizzleDb.schemas.backup.deletedAt),
),
columns: {databaseId: true},
});
const busy = new Set(activeBackups.map((b) => b.databaseId));
const toQueue = databaseIds.filter((id) => !busy.has(id));
const skipped = [...busy].map((databaseId) => ({databaseId, reason: "backup already in progress"}));
if (toQueue.length > 0) {
await db
.insert(drizzleDb.schemas.backup)
.values(toQueue.map((databaseId) => ({databaseId, status: "waiting" as const})));
}
return { return {
success: true, success: true,
value: {queued: databaseIds.length}, value: {queued: toQueue.length, skipped},
actionSuccess: {message: `Queued ${databaseIds.length} backup(s).`}, actionSuccess: {
message: `Queued ${toQueue.length} backup(s)${skipped.length ? `, ${skipped.length} skipped` : ""}.`,
},
}; };
} catch (error) { } catch (error) {
return { return {
@@ -1,7 +1,7 @@
"use server"; "use server";
import {z} from "zod"; import {z} from "zod";
import {and, desc, eq, isNull} from "drizzle-orm"; import {and, desc, eq, inArray, isNull} from "drizzle-orm";
import {db} from "@/db"; import {db} from "@/db";
import * as drizzleDb from "@/db"; import * as drizzleDb from "@/db";
import {ServerActionResult} from "@/types/action-type"; import {ServerActionResult} from "@/types/action-type";
@@ -34,6 +34,7 @@ export type RestorePreviewRow = {
backupStorageId?: string; backupStorageId?: string;
backupDate?: string; backupDate?: string;
restorable: boolean; restorable: boolean;
reason?: string;
}; };
async function resolveLatestRestorable(projectId: string, databaseIds: string[]): Promise<RestorePreviewRow[]> { async function resolveLatestRestorable(projectId: string, databaseIds: string[]): Promise<RestorePreviewRow[]> {
@@ -45,6 +46,22 @@ async function resolveLatestRestorable(projectId: string, databaseIds: string[])
const rows: RestorePreviewRow[] = []; const rows: RestorePreviewRow[] = [];
for (const databaseId of databaseIds) { for (const databaseId of databaseIds) {
const activeRestore = await db.query.restoration.findFirst({
where: and(
eq(drizzleDb.schemas.restoration.databaseId, databaseId),
inArray(drizzleDb.schemas.restoration.status, ["waiting", "ongoing"]),
),
});
if (activeRestore) {
rows.push({
databaseId,
name: nameById.get(databaseId) ?? databaseId,
restorable: false,
reason: "restore already in progress",
});
continue;
}
const backups = await db.query.backup.findMany({ const backups = await db.query.backup.findMany({
where: and( where: and(
eq(drizzleDb.schemas.backup.databaseId, databaseId), eq(drizzleDb.schemas.backup.databaseId, databaseId),
@@ -76,7 +93,7 @@ async function resolveLatestRestorable(projectId: string, databaseIds: string[])
} }
} }
rows.push(picked ?? {databaseId, name: nameById.get(databaseId) ?? databaseId, restorable: false}); rows.push(picked ?? {databaseId, name: nameById.get(databaseId) ?? databaseId, restorable: false, reason: "no successful backup"});
} }
return rows; return rows;
} }
@@ -117,7 +134,7 @@ export const bulkRestoreLatestAction = userAction
const restorable = rows.filter((r) => r.restorable); const restorable = rows.filter((r) => r.restorable);
const skipped = rows const skipped = rows
.filter((r) => !r.restorable) .filter((r) => !r.restorable)
.map((r) => ({databaseId: r.databaseId, reason: "no successful backup"})); .map((r) => ({databaseId: r.databaseId, reason: r.reason ?? "no successful backup"}));
if (restorable.length > 0) { if (restorable.length > 0) {
await db.insert(drizzleDb.schemas.restoration).values( await db.insert(drizzleDb.schemas.restoration).values(
@@ -55,7 +55,7 @@ export const BulkRestoreModal = (props: BulkRestoreModalProps) => {
<td className="p-2"> <td className="p-2">
{r.restorable {r.restorable
? <span className="text-green-600">{r.backupDate ? new Date(r.backupDate).toLocaleString() : "latest backup"}</span> ? <span className="text-green-600">{r.backupDate ? new Date(r.backupDate).toLocaleString() : "latest backup"}</span>
: <span className="text-amber-500">no successful backup skipped</span>} : <span className="text-amber-500">{r.reason ?? "no successful backup"} skipped</span>}
</td> </td>
</tr> </tr>
))} ))}