Implement metadata handling for database and compose backups. Update backup schemas to include metadata fields for various database types. Enhance backup creation and update processes to accommodate new metadata requirements. Modify UI components to support metadata input for different database types during backup operations.

This commit is contained in:
Mauricio Siu
2025-04-27 22:14:06 -06:00
parent 2ea2605ab1
commit 7c2eb63625
15 changed files with 6010 additions and 77 deletions

View File

@@ -58,7 +58,36 @@ import { z } from "zod";
type CacheType = "cache" | "fetch";
const AddPostgresBackup1Schema = z.object({
const getMetadataSchema = (
backupType: "database" | "compose",
databaseType: Props["databaseType"],
) => {
if (backupType !== "compose") return z.object({}).optional();
const schemas = {
postgres: z.object({
databaseUser: z.string().min(1, "Database user is required"),
}),
mariadb: z.object({
databaseUser: z.string().min(1, "Database user is required"),
databasePassword: z.string().min(1, "Database password is required"),
}),
mongo: z.object({
databaseUser: z.string().min(1, "Database user is required"),
databasePassword: z.string().min(1, "Database password is required"),
}),
mysql: z.object({
databaseRootPassword: z.string().min(1, "Root password is required"),
}),
"web-server": z.object({}),
};
return z.object({
[databaseType]: schemas[databaseType],
});
};
const Schema = z.object({
destinationId: z.string().min(1, "Destination required"),
schedule: z.string().min(1, "Schedule (Cron) required"),
prefix: z.string().min(1, "Prefix required"),
@@ -68,7 +97,7 @@ const AddPostgresBackup1Schema = z.object({
serviceName: z.string().nullable(),
});
type AddPostgresBackup = z.infer<typeof AddPostgresBackup1Schema>;
type Schema = z.infer<typeof Schema>;
interface Props {
id: string;
@@ -89,7 +118,11 @@ export const AddBackup = ({
const { mutateAsync: createBackup, isLoading: isCreatingPostgresBackup } =
api.backup.create.useMutation();
const form = useForm<AddPostgresBackup>({
const schema = Schema.extend({
metadata: getMetadataSchema(backupType, databaseType),
});
const form = useForm<z.infer<typeof schema>>({
defaultValues: {
database: "",
destinationId: "",
@@ -98,8 +131,9 @@ export const AddBackup = ({
schedule: "",
keepLatestCount: undefined,
serviceName: null,
metadata: {},
},
resolver: zodResolver(AddPostgresBackup1Schema),
resolver: zodResolver(schema),
});
const {
@@ -128,10 +162,11 @@ export const AddBackup = ({
schedule: "",
keepLatestCount: undefined,
serviceName: null,
metadata: {},
});
}, [form, form.reset, form.formState.isSubmitSuccessful, databaseType]);
const onSubmit = async (data: AddPostgresBackup) => {
const onSubmit = async (data: Schema) => {
if (backupType === "compose" && !data.serviceName) {
form.setError("serviceName", {
type: "manual",
@@ -489,6 +524,115 @@ export const AddBackup = ({
</FormItem>
)}
/>
{backupType === "compose" && (
<>
{databaseType === "postgres" && (
<FormField
control={form.control}
name="metadata.postgres.databaseUser"
render={({ field }) => (
<FormItem>
<FormLabel>Database User</FormLabel>
<FormControl>
<Input placeholder="postgres" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
{databaseType === "mariadb" && (
<>
<FormField
control={form.control}
name="metadata.mariadb.databaseUser"
render={({ field }) => (
<FormItem>
<FormLabel>Database User</FormLabel>
<FormControl>
<Input placeholder="mariadb" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="metadata.mariadb.databasePassword"
render={({ field }) => (
<FormItem>
<FormLabel>Database Password</FormLabel>
<FormControl>
<Input
type="password"
placeholder="••••••••"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</>
)}
{databaseType === "mongo" && (
<>
<FormField
control={form.control}
name="metadata.mongo.databaseUser"
render={({ field }) => (
<FormItem>
<FormLabel>Database User</FormLabel>
<FormControl>
<Input placeholder="mongo" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="metadata.mongo.databasePassword"
render={({ field }) => (
<FormItem>
<FormLabel>Database Password</FormLabel>
<FormControl>
<Input
type="password"
placeholder="••••••••"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</>
)}
{databaseType === "mysql" && (
<FormField
control={form.control}
name="metadata.mysql.databaseRootPassword"
render={({ field }) => (
<FormItem>
<FormLabel>Root Password</FormLabel>
<FormControl>
<Input
type="password"
placeholder="••••••••"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
</>
)}
</div>
<DialogFooter>
<Button

View File

@@ -61,21 +61,24 @@ export const ShowBackups = ({
? query()
: api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id });
console.log(postgres);
const mutationMap =
backupType === "database"
? {
postgres: api.backup.manualBackupPostgres.useMutation(),
mysql: api.backup.manualBackupMySql.useMutation(),
mariadb: api.backup.manualBackupMariadb.useMutation(),
mongo: api.backup.manualBackupMongo.useMutation(),
"web-server": api.backup.manualBackupWebServer.useMutation(),
}
: {
compose: api.backup.manualBackupCompose.useMutation(),
};
const mutationMap = {
postgres: () => api.backup.manualBackupPostgres.useMutation(),
mysql: () => api.backup.manualBackupMySql.useMutation(),
mariadb: () => api.backup.manualBackupMariadb.useMutation(),
mongo: () => api.backup.manualBackupMongo.useMutation(),
"web-server": () => api.backup.manualBackupWebServer.useMutation(),
compose: () => api.backup.manualBackupCompose.useMutation(),
};
const key2 = backupType === "database" ? databaseType : "compose";
const mutation = mutationMap[key2 as keyof typeof mutationMap];
const { mutateAsync: manualBackup, isLoading: isManualBackup } = mutationMap[
databaseType
]
? mutationMap[databaseType]()
const { mutateAsync: manualBackup, isLoading: isManualBackup } = mutation
? mutation
: api.backup.manualBackupMongo.useMutation();
const { mutateAsync: deleteBackup, isLoading: isRemoving } =

View File

@@ -63,7 +63,36 @@ import { z } from "zod";
type CacheType = "cache" | "fetch";
const UpdateBackupSchema = z.object({
const getMetadataSchema = (
backupType: "database" | "compose",
databaseType: string,
) => {
if (backupType !== "compose") return z.object({}).optional();
const schemas = {
postgres: z.object({
databaseUser: z.string().min(1, "Database user is required"),
}),
mariadb: z.object({
databaseUser: z.string().min(1, "Database user is required"),
databasePassword: z.string().min(1, "Database password is required"),
}),
mongo: z.object({
databaseUser: z.string().min(1, "Database user is required"),
databasePassword: z.string().min(1, "Database password is required"),
}),
mysql: z.object({
databaseRootPassword: z.string().min(1, "Root password is required"),
}),
"web-server": z.object({}),
};
return z.object({
[databaseType]: schemas[databaseType as keyof typeof schemas],
});
};
const Schema = z.object({
destinationId: z.string().min(1, "Destination required"),
schedule: z.string().min(1, "Schedule (Cron) required"),
prefix: z.string().min(1, "Prefix required"),
@@ -71,10 +100,9 @@ const UpdateBackupSchema = z.object({
database: z.string().min(1, "Database required"),
keepLatestCount: z.coerce.number().optional(),
serviceName: z.string().nullable(),
metadata: z.object({}).optional(),
});
type UpdateBackup = z.infer<typeof UpdateBackupSchema>;
interface Props {
backupId: string;
refetch: () => void;
@@ -114,7 +142,13 @@ export const UpdateBackup = ({ backupId, refetch }: Props) => {
const { mutateAsync, isLoading: isLoadingUpdate } =
api.backup.update.useMutation();
const form = useForm<UpdateBackup>({
const schema = backup
? Schema.extend({
metadata: getMetadataSchema(backup.backupType, backup.databaseType),
})
: Schema;
const form = useForm<z.infer<typeof schema>>({
defaultValues: {
database: "",
destinationId: "",
@@ -123,8 +157,9 @@ export const UpdateBackup = ({ backupId, refetch }: Props) => {
schedule: "",
keepLatestCount: undefined,
serviceName: null,
metadata: {},
},
resolver: zodResolver(UpdateBackupSchema),
resolver: zodResolver(schema),
});
useEffect(() => {
@@ -139,11 +174,12 @@ export const UpdateBackup = ({ backupId, refetch }: Props) => {
keepLatestCount: backup.keepLatestCount
? Number(backup.keepLatestCount)
: undefined,
metadata: backup.metadata || {},
});
}
}, [form, form.reset, backup]);
const onSubmit = async (data: UpdateBackup) => {
const onSubmit = async (data: z.infer<typeof schema>) => {
if (backup?.backupType === "compose" && !data.serviceName) {
form.setError("serviceName", {
type: "manual",
@@ -161,6 +197,7 @@ export const UpdateBackup = ({ backupId, refetch }: Props) => {
database: data.database,
serviceName: data.serviceName,
keepLatestCount: data.keepLatestCount as number | null,
metadata: data.metadata || {},
})
.then(async () => {
toast.success("Backup Updated");
@@ -473,6 +510,115 @@ export const UpdateBackup = ({ backupId, refetch }: Props) => {
</FormItem>
)}
/>
{backup?.backupType === "compose" && (
<>
{backup.databaseType === "postgres" && (
<FormField
control={form.control}
name="metadata.postgres.databaseUser"
render={({ field }) => (
<FormItem>
<FormLabel>Database User</FormLabel>
<FormControl>
<Input placeholder="postgres" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
{backup.databaseType === "mariadb" && (
<>
<FormField
control={form.control}
name="metadata.mariadb.databaseUser"
render={({ field }) => (
<FormItem>
<FormLabel>Database User</FormLabel>
<FormControl>
<Input placeholder="mariadb" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="metadata.mariadb.databasePassword"
render={({ field }) => (
<FormItem>
<FormLabel>Database Password</FormLabel>
<FormControl>
<Input
type="password"
placeholder="••••••••"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</>
)}
{backup.databaseType === "mongo" && (
<>
<FormField
control={form.control}
name="metadata.mongo.databaseUser"
render={({ field }) => (
<FormItem>
<FormLabel>Database User</FormLabel>
<FormControl>
<Input placeholder="mongo" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="metadata.mongo.databasePassword"
render={({ field }) => (
<FormItem>
<FormLabel>Database Password</FormLabel>
<FormControl>
<Input
type="password"
placeholder="••••••••"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</>
)}
{backup.databaseType === "mysql" && (
<FormField
control={form.control}
name="metadata.mysql.databaseRootPassword"
render={({ field }) => (
<FormItem>
<FormLabel>Root Password</FormLabel>
<FormControl>
<Input
type="password"
placeholder="••••••••"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
</>
)}
</div>
<DialogFooter>
<Button

View File

@@ -0,0 +1 @@
ALTER TABLE "backup" ADD COLUMN "metadata" jsonb;

File diff suppressed because it is too large Load Diff

View File

@@ -624,6 +624,13 @@
"when": 1745801614194,
"tag": "0088_same_ezekiel",
"breakpoints": true
},
{
"idx": 89,
"version": "7",
"when": 1745812150155,
"tag": "0089_dazzling_marrow",
"breakpoints": true
}
]
}

View File

@@ -10,6 +10,7 @@ import {
IS_CLOUD,
createBackup,
findBackupById,
findComposeByBackupId,
findMariadbByBackupId,
findMariadbById,
findMongoByBackupId,
@@ -31,6 +32,7 @@ import {
} from "@dokploy/server";
import { findDestinationById } from "@dokploy/server/services/destination";
import { runComposeBackup } from "@dokploy/server/utils/backups/compose";
import {
getS3Credentials,
normalizeS3Path,
@@ -240,9 +242,18 @@ export const backupRouter = createTRPCRouter({
manualBackupCompose: protectedProcedure
.input(apiFindOneBackup)
.mutation(async ({ input }) => {
// const backup = await findBackupById(input.backupId);
// await runComposeBackup(backup);
return true;
try {
const backup = await findBackupById(input.backupId);
const compose = await findComposeByBackupId(backup.backupId);
await runComposeBackup(compose, backup);
return true;
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error running manual Compose backup ",
cause: error,
});
}
}),
manualBackupMongo: protectedProcedure
.input(apiFindOneBackup)

View File

@@ -27,7 +27,6 @@ import {
createMount,
deleteMount,
findComposeById,
findDomainsByComposeId,
findProjectById,
findServerById,
findUserById,
@@ -268,8 +267,7 @@ export const composeRouter = createTRPCRouter({
message: "You are not authorized to get this compose",
});
}
const domains = await findDomainsByComposeId(input.composeId);
const composeFile = await addDomainToCompose(compose, domains);
const composeFile = await addDomainToCompose(compose);
return dump(composeFile, {
lineWidth: 1000,
});
@@ -723,18 +721,4 @@ export const composeRouter = createTRPCRouter({
});
}
}),
manualBackup: protectedProcedure
.input(z.object({ composeId: z.string() }))
.mutation(async ({ input, ctx }) => {
const compose = await findComposeById(input.composeId);
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to backup this compose",
});
}
await createBackup({
composeId: compose.composeId,
});
}),
});