mirror of
https://github.com/biersoeckli/QuickStack.git
synced 2026-05-14 14:03:42 -05:00
feat/add volume backup model and related services for edit of backups schedules in ui
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "VolumeBackup" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"volumeId" TEXT NOT NULL,
|
||||
"targetId" TEXT NOT NULL,
|
||||
"cron" TEXT NOT NULL,
|
||||
"retention" INTEGER NOT NULL,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL,
|
||||
CONSTRAINT "VolumeBackup_volumeId_fkey" FOREIGN KEY ("volumeId") REFERENCES "AppVolume" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT "VolumeBackup_targetId_fkey" FOREIGN KEY ("targetId") REFERENCES "S3Target" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
+25
-10
@@ -186,12 +186,13 @@ model AppDomain {
|
||||
}
|
||||
|
||||
model AppVolume {
|
||||
id String @id @default(uuid())
|
||||
id String @id @default(uuid())
|
||||
containerMountPath String
|
||||
size Int
|
||||
accessMode String @default("rwo")
|
||||
accessMode String @default("rwo")
|
||||
appId String
|
||||
app App @relation(fields: [appId], references: [id], onDelete: Cascade)
|
||||
app App @relation(fields: [appId], references: [id], onDelete: Cascade)
|
||||
volumeBackups VolumeBackup[]
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
@@ -221,13 +222,27 @@ model Parameter {
|
||||
}
|
||||
|
||||
model S3Target {
|
||||
id String @id @default(uuid())
|
||||
name String
|
||||
bucketName String
|
||||
endpoint String
|
||||
region String
|
||||
accessKeyId String
|
||||
secretKey String
|
||||
id String @id @default(uuid())
|
||||
name String
|
||||
bucketName String
|
||||
endpoint String
|
||||
region String
|
||||
accessKeyId String
|
||||
secretKey String
|
||||
volumeBackups VolumeBackup[]
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model VolumeBackup {
|
||||
id String @id @default(uuid())
|
||||
volumeId String
|
||||
volume AppVolume @relation(fields: [volumeId], references: [id], onDelete: Cascade)
|
||||
targetId String
|
||||
target S3Target @relation(fields: [targetId], references: [id], onDelete: Cascade)
|
||||
cron String
|
||||
retention Int
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@ -5,28 +5,30 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
import GeneralAppRateLimits from "./general/app-rate-limits";
|
||||
import GeneralAppSource from "./general/app-source";
|
||||
import EnvEdit from "./environment/env-edit";
|
||||
import { App } from "@prisma/client";
|
||||
import { S3Target } from "@prisma/client";
|
||||
import DomainsList from "./domains/domains";
|
||||
import StorageList from "./volumes/storages";
|
||||
import { AppExtendedModel } from "@/shared/model/app-extended.model";
|
||||
import { BuildJobModel } from "@/shared/model/build-job";
|
||||
import BuildsTab from "./overview/deployments";
|
||||
import Logs from "./overview/logs";
|
||||
import MonitoringTab from "./overview/monitoring-app";
|
||||
import InternalHostnames from "./domains/ports-and-internal-hostnames";
|
||||
import TerminalStreamed from "./overview/terminal-streamed";
|
||||
import { useEffect } from "react";
|
||||
import { useBreadcrumbs } from "@/frontend/states/zustand.states";
|
||||
import FileMount from "./volumes/file-mount";
|
||||
import WebhookDeploymentInfo from "./overview/webhook-deployment";
|
||||
import DbCredentials from "./credentials/db-crendentials";
|
||||
import VolumeBackupList from "./volumes/volume-backup";
|
||||
import { VolumeBackupExtendedModel } from "@/shared/model/volume-backup-extended.model";
|
||||
|
||||
export default function AppTabs({
|
||||
app,
|
||||
tabName
|
||||
tabName,
|
||||
s3Targets,
|
||||
volumeBackups
|
||||
}: {
|
||||
app: AppExtendedModel;
|
||||
tabName: string;
|
||||
s3Targets: S3Target[],
|
||||
volumeBackups: VolumeBackupExtendedModel[]
|
||||
}) {
|
||||
const router = useRouter();
|
||||
|
||||
@@ -67,6 +69,10 @@ export default function AppTabs({
|
||||
<TabsContent value="storage" className="space-y-4">
|
||||
<StorageList app={app} />
|
||||
<FileMount app={app} />
|
||||
<VolumeBackupList
|
||||
app={app}
|
||||
s3Targets={s3Targets}
|
||||
volumeBackups={volumeBackups} />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
)
|
||||
|
||||
@@ -2,6 +2,8 @@ import { getAuthUserSession } from "@/server/utils/action-wrapper.utils";
|
||||
import appService from "@/server/services/app.service";
|
||||
import AppTabs from "./app-tabs";
|
||||
import AppBreadcrumbs from "./app-breadcrumbs";
|
||||
import s3TargetService from "@/server/services/s3-target.service";
|
||||
import volumeBackupService from "@/server/services/volume-backup.service";
|
||||
|
||||
export default async function AppPage({
|
||||
searchParams,
|
||||
@@ -15,10 +17,18 @@ export default async function AppPage({
|
||||
if (!appId) {
|
||||
return <p>Could not find app with id {appId}</p>
|
||||
}
|
||||
const app = await appService.getExtendedById(appId);
|
||||
const [app, s3Targets, volumeBackups] = await Promise.all([
|
||||
appService.getExtendedById(appId),
|
||||
s3TargetService.getAll(),
|
||||
volumeBackupService.getForApp(appId)
|
||||
]);
|
||||
|
||||
return (<>
|
||||
<AppTabs app={app} tabName={searchParams?.tabName ?? 'overview'} />
|
||||
<AppTabs
|
||||
volumeBackups={volumeBackups}
|
||||
s3Targets={s3Targets}
|
||||
app={app}
|
||||
tabName={searchParams?.tabName ?? 'overview'} />
|
||||
<AppBreadcrumbs app={app} />
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -8,6 +8,8 @@ import { z } from "zod";
|
||||
import { ServiceException } from "@/shared/model/service.exception.model";
|
||||
import pvcService from "@/server/services/pvc.service";
|
||||
import { fileMountEditZodModel } from "@/shared/model/file-mount-edit.model";
|
||||
import { VolumeBackupEditModel, volumeBackupEditZodModel } from "@/shared/model/backup-volume-edit.model";
|
||||
import volumeBackupService from "@/server/services/volume-backup.service";
|
||||
|
||||
const actionAppVolumeEditZodModel = appVolumeEditZodModel.merge(z.object({
|
||||
appId: z.string(),
|
||||
@@ -60,7 +62,6 @@ const actionAppFileMountEditZodModel = fileMountEditZodModel.merge(z.object({
|
||||
export const saveFileMount = async (prevState: any, inputData: z.infer<typeof actionAppFileMountEditZodModel>) =>
|
||||
saveFormAction(inputData, actionAppFileMountEditZodModel, async (validatedData) => {
|
||||
await getAuthUserSession();
|
||||
const existingApp = await appService.getExtendedById(validatedData.appId);
|
||||
await appService.saveFileMount({
|
||||
...validatedData,
|
||||
id: validatedData.id ?? undefined,
|
||||
@@ -72,4 +73,21 @@ export const deleteFileMount = async (fileMountId: string) =>
|
||||
await getAuthUserSession();
|
||||
await appService.deleteFileMountById(fileMountId);
|
||||
return new SuccessActionResult(undefined, 'Successfully deleted volume');
|
||||
});
|
||||
|
||||
export const saveBackupVolume = async (prevState: any, inputData: VolumeBackupEditModel) =>
|
||||
saveFormAction(inputData, volumeBackupEditZodModel, async (validatedData) => {
|
||||
await getAuthUserSession();
|
||||
await volumeBackupService.save({
|
||||
...validatedData,
|
||||
id: validatedData.id ?? undefined,
|
||||
});
|
||||
return new SuccessActionResult();
|
||||
});
|
||||
|
||||
export const deleteBackupVolume = async (backupVolumeId: string) =>
|
||||
simpleAction(async () => {
|
||||
await getAuthUserSession();
|
||||
await volumeBackupService.deleteById(backupVolumeId);
|
||||
return new SuccessActionResult(undefined, 'Successfully deleted backup schedule');
|
||||
});
|
||||
@@ -0,0 +1,156 @@
|
||||
'use client'
|
||||
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { useFormState } from 'react-dom'
|
||||
import { useEffect, useState } from "react";
|
||||
import { FormUtils } from "@/frontend/utils/form.utilts";
|
||||
import { SubmitButton } from "@/components/custom/submit-button";
|
||||
import { AppVolume, S3Target, VolumeBackup } from "@prisma/client"
|
||||
import { ServerActionResult } from "@/shared/model/server-action-error-return.model"
|
||||
import { saveBackupVolume } from "./actions"
|
||||
import { toast } from "sonner"
|
||||
import { VolumeBackupEditModel, volumeBackupEditZodModel } from "@/shared/model/backup-volume-edit.model"
|
||||
import SelectFormField from "@/components/custom/select-form-field"
|
||||
import Link from "next/link"
|
||||
|
||||
export default function VolumeBackupEditDialog({
|
||||
children,
|
||||
volumeBackup,
|
||||
s3Targets,
|
||||
volumes
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
volumeBackup?: VolumeBackup;
|
||||
s3Targets: S3Target[];
|
||||
volumes: AppVolume[];
|
||||
}) {
|
||||
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
|
||||
const form = useForm<VolumeBackupEditModel>({
|
||||
resolver: zodResolver(volumeBackupEditZodModel),
|
||||
defaultValues: {
|
||||
...volumeBackup,
|
||||
retention: volumeBackup?.retention || 5,
|
||||
targetId: volumeBackup?.targetId || (s3Targets.length === 1 ? s3Targets[0].id : undefined),
|
||||
volumeId: volumeBackup?.volumeId || (volumes.length === 1 ? volumes[0].id : undefined),
|
||||
}
|
||||
});
|
||||
|
||||
const [state, formAction] = useFormState((state: ServerActionResult<any, any>,
|
||||
payload: VolumeBackupEditModel) =>
|
||||
saveBackupVolume(state, {
|
||||
...payload
|
||||
}), FormUtils.getInitialFormState<typeof volumeBackupEditZodModel>());
|
||||
|
||||
useEffect(() => {
|
||||
if (state.status === 'success') {
|
||||
form.reset();
|
||||
toast.success('Volume Backup saved successfully', {
|
||||
description: "From now on the volume will be backed up according to the new settings.",
|
||||
});
|
||||
setIsOpen(false);
|
||||
}
|
||||
FormUtils.mapValidationErrorsToForm<typeof volumeBackupEditZodModel>(state, form);
|
||||
}, [state]);
|
||||
|
||||
useEffect(() => {
|
||||
form.reset(volumeBackup);
|
||||
}, [volumeBackup]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div onClick={() => setIsOpen(true)}>
|
||||
{children}
|
||||
</div>
|
||||
<Dialog open={!!isOpen} onOpenChange={(isOpened) => setIsOpen(false)}>
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Edit Backup Configuration</DialogTitle>
|
||||
<DialogDescription>
|
||||
Configure your custom volume for this container.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Form {...form}>
|
||||
<form action={(e) => form.handleSubmit((data) => {
|
||||
return formAction(data);
|
||||
})()}>
|
||||
<div className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="cron"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Cron expression</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="5 4 * * *" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
To learn more about cron expressions, visit <a href="https://crontab.guru/" target="_blank" className="underline">crontab.guru</a>.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="retention"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Retention</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="number" placeholder="5" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
The number of backups to keep.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<SelectFormField
|
||||
form={form}
|
||||
name="volumeId"
|
||||
label="Volume to backup"
|
||||
values={volumes.map((volume) =>
|
||||
[volume.id, `${volume.containerMountPath}`])}
|
||||
/>
|
||||
|
||||
<SelectFormField
|
||||
form={form}
|
||||
name="targetId"
|
||||
label="Backup Location"
|
||||
formDescription={<>
|
||||
S3 Storage Locations can be configured <span className="underline"><Link href="/settings/s3-targets">here</Link></span>.
|
||||
</>}
|
||||
values={s3Targets.map((target) =>
|
||||
[target.id, `${target.name}`])}
|
||||
/>
|
||||
|
||||
<p className="text-red-500">{state.message}</p>
|
||||
<SubmitButton>Save</SubmitButton>
|
||||
</div>
|
||||
</form>
|
||||
</Form >
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
)
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
'use client';
|
||||
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { AppExtendedModel } from "@/shared/model/app-extended.model";
|
||||
import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { EditIcon, TrashIcon } from "lucide-react";
|
||||
import { Toast } from "@/frontend/utils/toast.utils";
|
||||
import { deleteBackupVolume, deleteVolume } from "./actions";
|
||||
import { useConfirmDialog } from "@/frontend/states/zustand.states";
|
||||
import { S3Target, VolumeBackup } from "@prisma/client";
|
||||
import React from "react";
|
||||
import { formatDateTime } from "@/frontend/utils/format.utils";
|
||||
import VolumeBackupEditDialog from "./volume-backup-edit-overlay";
|
||||
import { VolumeBackupExtendedModel } from "@/shared/model/volume-backup-extended.model";
|
||||
|
||||
export default function VolumeBackupList({
|
||||
app,
|
||||
volumeBackups,
|
||||
s3Targets
|
||||
}: {
|
||||
app: AppExtendedModel,
|
||||
s3Targets: S3Target[],
|
||||
volumeBackups: VolumeBackupExtendedModel[]
|
||||
}) {
|
||||
|
||||
const { openConfirmDialog: openDialog } = useConfirmDialog();
|
||||
|
||||
const asyncDeleteBackupVolume = async (volumeId: string) => {
|
||||
const confirm = await openDialog({
|
||||
title: "Delete Backup Schedule",
|
||||
description: "Are you sure you want to remove this Backup Schdeule? All backups created by this schedule will still be available.",
|
||||
okButton: "Delete Backup Schedule"
|
||||
});
|
||||
if (confirm) {
|
||||
await Toast.fromAction(() => deleteBackupVolume(volumeId));
|
||||
}
|
||||
};
|
||||
|
||||
return <>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Backup Schedules</CardTitle>
|
||||
<CardDescription>Configure backup schedules for your volumes. Backups can be stored in a S3 bucket.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Table>
|
||||
<TableCaption>{volumeBackups.length} Backup Rules</TableCaption>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Cron Expression</TableHead>
|
||||
<TableHead>Retention</TableHead>
|
||||
<TableHead>Backup Location</TableHead>
|
||||
<TableHead>Created At</TableHead>
|
||||
<TableHead className="w-[100px]">Action</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{volumeBackups.map(volumeBackup => (
|
||||
<TableRow key={volumeBackup.id}>
|
||||
<TableCell className="font-medium">{volumeBackup.cron}</TableCell>
|
||||
<TableCell className="font-medium">{volumeBackup.retention}</TableCell>
|
||||
<TableCell className="font-medium">{volumeBackup.target.name}</TableCell>
|
||||
<TableCell className="font-medium">{formatDateTime(volumeBackup.createdAt)}</TableCell>
|
||||
<TableCell className="font-medium flex gap-2">
|
||||
<VolumeBackupEditDialog volumeBackup={volumeBackup}
|
||||
s3Targets={s3Targets} volumes={app.appVolumes}>
|
||||
<Button variant="ghost"><EditIcon /></Button>
|
||||
</VolumeBackupEditDialog>
|
||||
<Button variant="ghost" onClick={() => asyncDeleteBackupVolume(volumeBackup.id)}>
|
||||
<TrashIcon />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<VolumeBackupEditDialog s3Targets={s3Targets} volumes={app.appVolumes}>
|
||||
<Button>Add Backup Schedule</Button>
|
||||
</VolumeBackupEditDialog>
|
||||
</CardFooter>
|
||||
</Card >
|
||||
</>;
|
||||
}
|
||||
@@ -49,7 +49,7 @@ export default function QuickStackRegistrySettings({
|
||||
<CardTitle>Registry Storage Location</CardTitle>
|
||||
<CardDescription>
|
||||
After a build the Docker Image is stored on the server by default. This can take up a lot of disk space.
|
||||
If your want to store all Docker Images from the registry in a external S3 Storage you can configure it here.
|
||||
If your want to store all Docker Images from the registry in an external S3 Storage you can configure it here.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<Form {...form}>
|
||||
|
||||
@@ -88,6 +88,7 @@ class AppService {
|
||||
appPorts: true,
|
||||
appFileMounts: true,
|
||||
};
|
||||
|
||||
if (cached) {
|
||||
return await unstable_cache(async (id: string) => await dataAccess.client.app.findFirstOrThrow({
|
||||
where: {
|
||||
|
||||
@@ -56,7 +56,6 @@ class S3TargetService {
|
||||
data: item as Prisma.S3TargetUncheckedCreateInput
|
||||
});
|
||||
}
|
||||
await namespaceService.createNamespaceIfNotExists(savedItem.id);
|
||||
} finally {
|
||||
revalidateTag(Tags.s3Targets());
|
||||
}
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import { revalidateTag, unstable_cache } from "next/cache";
|
||||
import dataAccess from "../adapter/db.client";
|
||||
import { Tags } from "../utils/cache-tag-generator.utils";
|
||||
import { Prisma, VolumeBackup } from "@prisma/client";
|
||||
import { VolumeBackupExtendedModel } from "@/shared/model/volume-backup-extended.model";
|
||||
|
||||
class VolumeBackupService {
|
||||
|
||||
async getAll(): Promise<VolumeBackupExtendedModel[]> {
|
||||
return await unstable_cache(() => dataAccess.client.volumeBackup.findMany({
|
||||
orderBy: {
|
||||
cron: 'asc'
|
||||
},
|
||||
include: {
|
||||
target: true
|
||||
}
|
||||
}),
|
||||
[Tags.volumeBackups()], {
|
||||
tags: [Tags.volumeBackups()]
|
||||
})();
|
||||
}
|
||||
|
||||
async getForApp(appId: string): Promise<VolumeBackupExtendedModel[]> {
|
||||
return await unstable_cache(() => dataAccess.client.volumeBackup.findMany({
|
||||
where: {
|
||||
volume: {
|
||||
appId
|
||||
}
|
||||
},
|
||||
include: {
|
||||
target: true
|
||||
},
|
||||
orderBy: {
|
||||
cron: 'asc'
|
||||
}
|
||||
}),
|
||||
[Tags.volumeBackups()], {
|
||||
tags: [Tags.volumeBackups()]
|
||||
})();
|
||||
}
|
||||
|
||||
async getById(id: string) {
|
||||
return dataAccess.client.volumeBackup.findFirstOrThrow({
|
||||
where: {
|
||||
id
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async save(item: Prisma.VolumeBackupUncheckedCreateInput | Prisma.VolumeBackupUncheckedUpdateInput) {
|
||||
let savedItem: VolumeBackup;
|
||||
try {
|
||||
if (item.id) {
|
||||
savedItem = await dataAccess.client.volumeBackup.update({
|
||||
where: {
|
||||
id: item.id as string
|
||||
},
|
||||
data: item,
|
||||
});
|
||||
} else {
|
||||
savedItem = await dataAccess.client.volumeBackup.create({
|
||||
data: item as Prisma.VolumeBackupUncheckedCreateInput,
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
revalidateTag(Tags.volumeBackups());
|
||||
}
|
||||
return savedItem;
|
||||
}
|
||||
|
||||
async deleteById(id: string) {
|
||||
const existingItem = await this.getById(id);
|
||||
if (!existingItem) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await dataAccess.client.volumeBackup.delete({
|
||||
where: {
|
||||
id
|
||||
}
|
||||
});
|
||||
} finally {
|
||||
revalidateTag(Tags.volumeBackups());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const volumeBackupService = new VolumeBackupService();
|
||||
export default volumeBackupService;
|
||||
@@ -12,6 +12,10 @@ export class Tags {
|
||||
return `targets`;
|
||||
}
|
||||
|
||||
static volumeBackups() {
|
||||
return `volume-backups`;
|
||||
}
|
||||
|
||||
static apps(projectId: string) {
|
||||
return `apps-${projectId}`;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { z } from "zod";
|
||||
import { AppDomainModel, AppFileMountModel, AppModel, AppPortModel, AppVolumeModel, ProjectModel, RelatedAppModel } from "./generated-zod";
|
||||
|
||||
import { AppDomainModel, AppFileMountModel, AppModel, AppPortModel, AppVolumeModel, ProjectModel, VolumeBackupModel } from "./generated-zod";
|
||||
|
||||
export const AppExtendedZodModel= z.lazy(() => AppModel.extend({
|
||||
project: ProjectModel,
|
||||
appDomains: AppDomainModel.array(),
|
||||
appVolumes: AppVolumeModel.array(),
|
||||
appPorts: AppPortModel.array(),
|
||||
appFileMounts: AppFileMountModel.array(),
|
||||
appVolumes: AppVolumeModel.array(),
|
||||
}))
|
||||
|
||||
export type AppExtendedModel = z.infer<typeof AppExtendedZodModel>;
|
||||
export type AppExtendedModel = z.infer<typeof AppExtendedZodModel>;
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { stringToNumber } from "@/shared/utils/zod.utils";
|
||||
import { z } from "zod";
|
||||
|
||||
export const volumeBackupEditZodModel = z.object({
|
||||
id: z.string().nullable(),
|
||||
volumeId: z.string(),
|
||||
targetId: z.string(),
|
||||
cron: z.string().trim().regex(/^ *(\*|[0-5]?\d) *(\*|[01]?\d) *(\*|[0-2]?\d) *(\*|[0-6]?\d) *(\*|[0-6]?\d) *$/),
|
||||
//cron: z.string().trim().min(1),
|
||||
retention: stringToNumber,
|
||||
});
|
||||
|
||||
export type VolumeBackupEditModel = z.infer<typeof volumeBackupEditZodModel>;
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as z from "zod"
|
||||
|
||||
import { CompleteApp, RelatedAppModel } from "./index"
|
||||
import { CompleteApp, RelatedAppModel, CompleteVolumeBackup, RelatedVolumeBackupModel } from "./index"
|
||||
|
||||
export const AppVolumeModel = z.object({
|
||||
id: z.string(),
|
||||
@@ -14,6 +14,7 @@ export const AppVolumeModel = z.object({
|
||||
|
||||
export interface CompleteAppVolume extends z.infer<typeof AppVolumeModel> {
|
||||
app: CompleteApp
|
||||
volumeBackups: CompleteVolumeBackup[]
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -23,4 +24,5 @@ export interface CompleteAppVolume extends z.infer<typeof AppVolumeModel> {
|
||||
*/
|
||||
export const RelatedAppVolumeModel: z.ZodSchema<CompleteAppVolume> = z.lazy(() => AppVolumeModel.extend({
|
||||
app: RelatedAppModel,
|
||||
volumeBackups: RelatedVolumeBackupModel.array(),
|
||||
}))
|
||||
|
||||
@@ -11,3 +11,4 @@ export * from "./appvolume"
|
||||
export * from "./appfilemount"
|
||||
export * from "./parameter"
|
||||
export * from "./s3target"
|
||||
export * from "./volumebackup"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as z from "zod"
|
||||
|
||||
import { CompleteVolumeBackup, RelatedVolumeBackupModel } from "./index"
|
||||
|
||||
export const S3TargetModel = z.object({
|
||||
id: z.string(),
|
||||
@@ -12,3 +13,16 @@ export const S3TargetModel = z.object({
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
})
|
||||
|
||||
export interface CompleteS3Target extends z.infer<typeof S3TargetModel> {
|
||||
volumeBackups: CompleteVolumeBackup[]
|
||||
}
|
||||
|
||||
/**
|
||||
* RelatedS3TargetModel contains all relations on your model in addition to the scalars
|
||||
*
|
||||
* NOTE: Lazy required in case of potential circular dependencies within schema
|
||||
*/
|
||||
export const RelatedS3TargetModel: z.ZodSchema<CompleteS3Target> = z.lazy(() => S3TargetModel.extend({
|
||||
volumeBackups: RelatedVolumeBackupModel.array(),
|
||||
}))
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import * as z from "zod"
|
||||
|
||||
import { CompleteAppVolume, RelatedAppVolumeModel, CompleteS3Target, RelatedS3TargetModel } from "./index"
|
||||
|
||||
export const VolumeBackupModel = z.object({
|
||||
id: z.string(),
|
||||
volumeId: z.string(),
|
||||
targetId: z.string(),
|
||||
cron: z.string(),
|
||||
retention: z.number().int(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
})
|
||||
|
||||
export interface CompleteVolumeBackup extends z.infer<typeof VolumeBackupModel> {
|
||||
volume: CompleteAppVolume
|
||||
target: CompleteS3Target
|
||||
}
|
||||
|
||||
/**
|
||||
* RelatedVolumeBackupModel contains all relations on your model in addition to the scalars
|
||||
*
|
||||
* NOTE: Lazy required in case of potential circular dependencies within schema
|
||||
*/
|
||||
export const RelatedVolumeBackupModel: z.ZodSchema<CompleteVolumeBackup> = z.lazy(() => VolumeBackupModel.extend({
|
||||
volume: RelatedAppVolumeModel,
|
||||
target: RelatedS3TargetModel,
|
||||
}))
|
||||
@@ -0,0 +1,8 @@
|
||||
import { z } from "zod";
|
||||
import { S3TargetModel, VolumeBackupModel } from "./generated-zod";
|
||||
|
||||
export const volumeBackupExtendedZodModel = z.lazy(() => VolumeBackupModel.extend({
|
||||
target: S3TargetModel
|
||||
}))
|
||||
|
||||
export type VolumeBackupExtendedModel = z.infer<typeof volumeBackupExtendedZodModel>;
|
||||
Reference in New Issue
Block a user