Adding methods to delete backups on remote storage s3 and local. Some refactoring on models db and adding common.ts model with createdAt, updatedAt, and deletedAt.

This commit is contained in:
charlesgauthereau
2025-08-29 15:31:23 +02:00
parent c8da2579e8
commit 45e3b31fda
48 changed files with 310 additions and 148 deletions
@@ -6,7 +6,7 @@ import { sendEmail } from "@/utils/email-helper";
import TestEmailSettings from "../../../../../../emails/TestEmailSettings";
import { render } from "@react-email/render";
import { toast } from "sonner";
import {Setting} from "@/db/schema/00_setting";
import {Setting} from "@/db/schema/01_setting";
import {EmailFormType} from "@/components/wrappers/dashboard/admin/admin-email-tab/email-form/email-form.schema";
export type SettingsEmailTabProps = {
@@ -12,7 +12,7 @@ import {useRouter} from "next/navigation";
import {
updateStorageSettingsAction
} from "@/components/wrappers/dashboard/admin/admin-storage-tab/storage-s3/s3-form.action";
import {Setting} from "@/db/schema/00_setting";
import {Setting} from "@/db/schema/01_setting";
import {S3FormType} from "@/components/wrappers/dashboard/admin/admin-storage-tab/storage-s3/s3-form.schema";
export type SettingsStorageTabProps = {
@@ -3,8 +3,8 @@
import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs";
import {SettingsEmailTab} from "@/components/wrappers/dashboard/admin/admin-email-tab/settings-email-tab";
import {SettingsStorageTab} from "@/components/wrappers/dashboard/admin/admin-storage-tab/settings-storage-tab";
import {User, UserWithAccounts} from "@/db/schema/01_user";
import {Setting} from "@/db/schema/00_setting";
import {User, UserWithAccounts} from "@/db/schema/02_user";
import {Setting} from "@/db/schema/01_setting";
import {useEffect, useState} from "react";
import {useRouter, useSearchParams} from "next/navigation";
import {AdminUsersTable} from "@/components/wrappers/dashboard/admin/admin-user-tab/admin-user-table";
@@ -1,4 +1,4 @@
import {User, UserWithAccounts} from "@/db/schema/01_user";
import {User, UserWithAccounts} from "@/db/schema/02_user";
import {Card, CardContent, CardDescription, CardHeader, CardTitle} from "@/components/ui/card";
import {DataTable} from "@/components/wrappers/common/table/data-table";
import {usersColumnsAdmin} from "@/components/wrappers/dashboard/admin/admin-user-tab/columns-users";
@@ -9,7 +9,7 @@ import {useState} from "react";
import {Trash2} from "lucide-react";
import {deleteUserAction} from "@/components/wrappers/dashboard/profile/button-delete-account/delete-account.action";
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
import {UserWithAccounts} from "@/db/schema/01_user";
import {UserWithAccounts} from "@/db/schema/02_user";
import {authClient, useSession} from "@/lib/auth/auth-client";
import {formatFrenchDate} from "@/utils/date-formatting";
import {providerSwitch} from "@/components/wrappers/common/provider-switch";
@@ -4,7 +4,7 @@ import {getServerUrl} from "@/utils/get-server-url";
import {PasswordInput} from "@/components/wrappers/auth/password-input/password-input";
import {useState} from "react";
import {CopyButton} from "@/components/wrappers/common/button/copy-button";
import {Agent} from "@/db/schema/07_agent";
import {Agent} from "@/db/schema/08_agent";
export type AgentCardKeyProps = {
agent: Agent;
@@ -4,7 +4,7 @@ import { Card, CardContent, CardHeader } from "@/components/ui/card";
import Link from "next/link";
import { formatDateLastContact } from "@/utils/date-formatting";
import { ConnectionCircle } from "@/components/wrappers/common/connection-circle";
import {Agent} from "@/db/schema/07_agent";
import {Agent} from "@/db/schema/08_agent";
export type agentCardProps = {
data: Agent;
@@ -8,7 +8,7 @@ import { PropsWithChildren } from "react";
import { CopyButton } from "@/components/wrappers/common/button/copy-button";
import { getServerUrl } from "@/utils/get-server-url";
import { CodeSnippet } from "@/components/wrappers/code-snippet/code-snippet";
import {Agent} from "@/db/schema/07_agent";
import {Agent} from "@/db/schema/08_agent";
export type agentRegistrationDialogProps = PropsWithChildren<{
agent: Agent;
@@ -7,7 +7,7 @@ import {ServerActionResult} from "@/types/action-type";
import {eq} from "drizzle-orm";
import {db} from "@/db";
import * as drizzleDb from "@/db";
import {Agent} from "@/db/schema/07_agent";
import {Agent} from "@/db/schema/08_agent";
export const deleteAgentAction = userAction.schema(z.string()).action(async ({parsedInput}): Promise<ServerActionResult<Agent>> => {
try {
@@ -1,13 +1,14 @@
"use server";
import { z } from "zod";
import { userAction } from "@/safe-actions";
import { db } from "@/db";
import { ServerActionResult } from "@/types/action-type";
import {z} from "zod";
import {userAction} from "@/safe-actions";
import {db} from "@/db";
import {ServerActionResult} from "@/types/action-type";
import * as drizzleDb from "@/db";
import {Backup} from "@/db/schema/06_database";
import {Backup} from "@/db/schema/07_database";
import {withUpdatedAt} from "@/db/utils";
export const backupButtonAction = userAction.schema(z.string()).action(async ({ parsedInput }): Promise<ServerActionResult<Backup>> => {
export const backupButtonAction = userAction.schema(z.string()).action(async ({parsedInput}): Promise<ServerActionResult<Backup>> => {
try {
const [createdBackup] = await db
.insert(drizzleDb.schemas.backup)
@@ -22,7 +23,7 @@ export const backupButtonAction = userAction.schema(z.string()).action(async ({
value: createdBackup,
actionSuccess: {
message: "Backup has been successfully created.",
messageParams: { databaseId: parsedInput },
messageParams: {databaseId: parsedInput},
},
};
} catch (error) {
@@ -34,7 +35,7 @@ export const backupButtonAction = userAction.schema(z.string()).action(async ({
message: "Failed to create backup.",
status: 500,
cause: error instanceof Error ? error.message : "Unknown error",
messageParams: { databaseId: parsedInput },
messageParams: {databaseId: parsedInput},
},
};
}
@@ -6,7 +6,7 @@ import { redirect } from "next/navigation";
import { CircleUser, LogOut, ShieldHalf } from "lucide-react";
import { signOut } from "@/lib/auth/auth-client";
import { useRouter } from "next/navigation";
import { User } from "@/db/schema/01_user";
import { User } from "@/db/schema/02_user";
export type LoggedInDropdownProps = PropsWithChildren<{
user: User;
@@ -11,7 +11,7 @@ import { useMutation } from "@tanstack/react-query";
import { toast } from "sonner";
import { useRouter } from "next/navigation";
import { updateDatabaseBackupPolicyAction } from "@/components/wrappers/dashboard/database/cron-button/cron.action";
import {Database} from "@/db/schema/06_database";
import {Database} from "@/db/schema/07_database";
export type CronButtonProps = {
database: Database;
@@ -6,7 +6,7 @@ import {useRouter} from "next/navigation";
import {toast} from "sonner";
import {Button} from "@/components/ui/button";
import {Separator} from "@/components/ui/separator";
import {Database} from "@/db/schema/06_database";
import {Database} from "@/db/schema/07_database";
export type CronInputProps = {
database: Database;
@@ -20,14 +20,14 @@ import {
OrganizationFormSchema,
OrganizationFormType
} from "@/components/wrappers/dashboard/organization/organization-form/organization-form.schema";
import {MemberWithUser, OrganizationWithMembers} from "@/db/schema/02_organization";
import {MemberWithUser, OrganizationWithMembers} from "@/db/schema/03_organization";
import {
deleteOrganizationAction,
updateOrganizationAction
} from "@/components/wrappers/dashboard/organization/organization.action";
import {toast} from "sonner";
import {User as BetterAuthUser} from "better-auth";
import {User} from "@/db/schema/01_user";
import {User} from "@/db/schema/02_user";
export type organizationFormProps = {
defaultValues?: OrganizationWithMembers;
@@ -11,7 +11,7 @@ import {db} from "@/db";
import {and, eq, inArray} from "drizzle-orm";
import {auth, checkSlugOrganization, createOrganization, deleteOrganization} from "@/lib/auth/auth";
import {slugify} from "@/utils/slugify";
import {Organization} from "@/db/schema/02_organization";
import {Organization} from "@/db/schema/03_organization";
import * as drizzleDb from "@/db";
import {headers} from "next/headers";
@@ -6,7 +6,7 @@ import { uploadImageAction } from "@/features/upload/public/upload.action";
import { useMutation } from "@tanstack/react-query";
import { updateImageUserAction } from "@/components/wrappers/dashboard/profile/avatar/avatar.action";
import { useRouter } from "next/navigation";
import { User } from "@/db/schema/01_user";
import { User } from "@/db/schema/02_user";
import {ChangeEvent} from "react";
export type AvatarWithUploadProps = {
@@ -2,7 +2,7 @@
import {Card, CardContent, CardHeader} from "@/components/ui/card";
import {formatDateLastContact} from "@/utils/date-formatting";
import {Database} from "@/db/schema/06_database";
import {Database} from "@/db/schema/07_database";
export type DatabaseKpiPro = {
successRate: any;
@@ -7,8 +7,8 @@ import {eventUpdate} from "@/types/events";
import {backupColumns} from "@/features/dashboard/backup/columns";
import {restoreColumns} from "@/features/dashboard/restore/columns";
import {DataTable} from "@/components/wrappers/common/table/data-table";
import {Backup, Database, DatabaseWith, Restoration} from "@/db/schema/06_database";
import {Setting} from "@/db/schema/00_setting";
import {Backup, Database, DatabaseWith, Restoration} from "@/db/schema/07_database";
import {Setting} from "@/db/schema/01_setting";
import {DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger} from "@/components/ui/dropdown-menu";
import {MoreHorizontal, Trash2} from "lucide-react";
import {useMutation} from "@tanstack/react-query";
@@ -62,6 +62,8 @@ export const DatabaseTabs = (props: DatabaseTabsProps) => {
const backupDeleted = await deleteBackupAction({
backupId: backup.id,
databaseId: backup.databaseId,
file: backup.file!,
projectSlug: props.database?.project?.slug!
});
return {
success: backupDeleted?.data?.success,
@@ -2,7 +2,7 @@
import { Card, CardContent, CardHeader } from "@/components/ui/card";
import Link from "next/link";
import {ProjectWith} from "@/db/schema/05_project";
import {ProjectWith} from "@/db/schema/06_project";
export type projectCardProps = {
data: ProjectWith;
@@ -5,7 +5,7 @@ import Image from "next/image";
import { Card, CardContent, CardHeader } from "@/components/ui/card";
import { ConnectionCircle } from "@/components/wrappers/common/connection-circle";
import { formatDateLastContact } from "@/utils/date-formatting";
import {Database} from "@/db/schema/06_database";
import {Database} from "@/db/schema/07_database";
import {Avatar, AvatarFallback, AvatarImage} from "@/components/ui/avatar";
export type projectDatabaseCardProps = {
@@ -6,9 +6,9 @@ import { z } from "zod";
import { ServerActionResult } from "@/types/action-type";
import { db } from "@/db";
import { eq, inArray } from "drizzle-orm";
import {Project} from "@/db/schema/05_project";
import {Project} from "@/db/schema/06_project";
import * as drizzleDb from "@/db";
import {Database} from "@/db/schema/06_database";
import {Database} from "@/db/schema/07_database";
import {slugify} from "@/utils/slugify";
export const createProjectAction = userAction
@@ -11,8 +11,8 @@ import { createProjectAction, updateProjectAction } from "@/components/wrappers/
import { useRouter } from "next/navigation";
import { MultiSelect } from "@/components/wrappers/common/multiselect/multi-select";
import { toast } from "sonner";
import {DatabaseWith} from "@/db/schema/06_database";
import {Organization} from "@/db/schema/02_organization";
import {DatabaseWith} from "@/db/schema/07_database";
import {Organization} from "@/db/schema/03_organization";
export type projectFormProps = {
defaultValues?: ProjectType;
@@ -1,7 +1,7 @@
"use client";
import {ColumnDef} from "@tanstack/react-table";
import {MemberWithUser} from "@/db/schema/02_organization";
import {MemberWithUser} from "@/db/schema/03_organization";
import {useState} from "react";
import {authClient, useSession} from "@/lib/auth/auth-client";
import {useMutation} from "@tanstack/react-query";
@@ -1,7 +1,7 @@
import {DataTable} from "@/components/wrappers/common/table/data-table";
import {usersColumnsAdmin} from "@/components/wrappers/dashboard/admin/columns-users";
import {MemberWithUser, Organization, OrganizationWithMembers} from "@/db/schema/02_organization";
import {OrganizationInvitation} from "@/db/schema/04_invitation";
import {MemberWithUser, Organization, OrganizationWithMembers} from "@/db/schema/03_organization";
import {OrganizationInvitation} from "@/db/schema/05_invitation";
import {organizationMemberColumns} from "@/components/wrappers/dashboard/settings/columns-organization-members";
+8 -8
View File
@@ -1,13 +1,13 @@
import {drizzle} from "drizzle-orm/node-postgres";
import * as settings from "./schema/00_setting";
import * as user from "./schema/01_user";
import * as organisation from "./schema/02_organization";
import * as invitation from "./schema/03_member";
import * as member from "./schema/04_invitation";
import * as project from "./schema/05_project";
import * as agent from "./schema/07_agent";
import * as database from "./schema/06_database";
import * as settings from "./schema/01_setting";
import * as user from "./schema/02_user";
import * as organisation from "./schema/03_organization";
import * as invitation from "./schema/04_member";
import * as member from "./schema/05_invitation";
import * as project from "./schema/06_project";
import * as agent from "./schema/08_agent";
import * as database from "./schema/07_database";
import {Pool} from "pg";
+7
View File
@@ -29,6 +29,13 @@
"when": 1756321312852,
"tag": "0003_absent_maestro",
"breakpoints": true
},
{
"idx": 4,
"version": "7",
"when": 1756473637441,
"tag": "0004_dazzling_hawkeye",
"breakpoints": true
}
]
}
+15
View File
@@ -0,0 +1,15 @@
import {timestamp} from "drizzle-orm/pg-core";
import {z} from "zod";
export const timestamps = {
updatedAt: timestamp("updated_at"),
createdAt: timestamp("created_at").defaultNow().notNull(),
deletedAt: timestamp("deleted_at"),
}
export function schemaWithoutMeta<
T extends z.ZodTypeAny
>(schema: T) {
// @ts-ignore
return schema.omit({id: true, createdAt: true, updatedAt: true, deletedAt: true});
}
@@ -2,6 +2,7 @@ import { pgTable, timestamp, uuid, varchar } from "drizzle-orm/pg-core";
import { typeStorageEnum } from "./types";
import { createSelectSchema } from "drizzle-zod";
import { z } from "zod";
import {timestamps} from "@/db/schema/00_common";
export const setting = pgTable("settings", {
id: uuid("id").primaryKey().defaultRandom(),
@@ -16,8 +17,7 @@ export const setting = pgTable("settings", {
smtpHost: varchar("smtp_host", { length: 255 }),
smtpPort: varchar("smtp_port", { length: 255 }),
smtpUser: varchar("smtp_user", { length: 255 }),
createdAt: timestamp("created_at").notNull().defaultNow(),
updatedAt: timestamp("updated_at"),
...timestamps
});
export const settingSchema = createSelectSchema(setting);
@@ -1,12 +1,13 @@
import { relations } from "drizzle-orm";
import { boolean, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core";
import { createSelectSchema } from "drizzle-zod";
import { z } from "zod";
import { project } from "./05_project";
import {member, OrganizationMember} from "@/db/schema/03_member";
import {invitation} from "@/db/schema/04_invitation";
import {organization} from "@/db/schema/02_organization";
import {relations} from "drizzle-orm";
import {boolean, pgTable, text, timestamp, uuid} from "drizzle-orm/pg-core";
import {createSelectSchema} from "drizzle-zod";
import {z} from "zod";
import {project} from "./06_project";
import {member, OrganizationMember} from "@/db/schema/04_member";
import {invitation} from "@/db/schema/05_invitation";
import {organization} from "@/db/schema/03_organization";
import {Account} from "better-auth";
import {timestamps} from "@/db/schema/00_common";
export const user = pgTable("user", {
id: uuid("id").defaultRandom().primaryKey(),
@@ -14,28 +15,26 @@ export const user = pgTable("user", {
email: text("email").notNull().unique(),
emailVerified: boolean("email_verified").notNull(),
image: text("image"),
createdAt: timestamp("created_at").notNull().defaultNow(),
updatedAt: timestamp("updated_at"),
role: text("role"),
banned: boolean("banned"),
banReason: text("ban_reason"),
banExpires: timestamp("ban_expires"),
deletedAt: timestamp("deleted_at"),
...timestamps
});
export const session = pgTable("session", {
id: uuid("id").defaultRandom().primaryKey(),
expiresAt: timestamp("expires_at").notNull(),
token: text("token").notNull().unique(),
createdAt: timestamp("created_at").notNull().defaultNow(),
updatedAt: timestamp("updated_at"),
ipAddress: text("ip_address"),
userAgent: text("user_agent"),
userId: uuid("user_id")
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
.references(() => user.id, {onDelete: "cascade"}),
impersonatedBy: text("impersonated_by"), //id or name ????
activeOrganizationId: text("active_organization_id"),
...timestamps
});
export const account = pgTable("account", {
@@ -44,7 +43,7 @@ export const account = pgTable("account", {
providerId: text("provider_id").notNull(),
userId: uuid("user_id")
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
.references(() => user.id, {onDelete: "cascade"}),
accessToken: text("access_token"),
refreshToken: text("refresh_token"),
idToken: text("id_token"),
@@ -52,8 +51,8 @@ export const account = pgTable("account", {
refreshTokenExpiresAt: timestamp("refresh_token_expires_at"),
scope: text("scope"),
password: text("password"),
createdAt: timestamp("created_at").notNull().defaultNow(),
updatedAt: timestamp("updated_at"),
...timestamps
});
export const verification = pgTable("verification", {
@@ -61,32 +60,32 @@ export const verification = pgTable("verification", {
identifier: text("identifier").notNull(),
value: text("value").notNull(),
expiresAt: timestamp("expires_at").notNull(),
createdAt: timestamp("created_at").notNull().defaultNow(),
updatedAt: timestamp("updated_at"),
...timestamps
});
export const userRelations = relations(user, ({ many }) => ({
export const userRelations = relations(user, ({many}) => ({
sessions: many(session),
accounts: many(account),
memberships: many(member),
invitations: many(invitation),
}));
export const sessionRelations = relations(session, ({ one }) => ({
export const sessionRelations = relations(session, ({one}) => ({
user: one(user, {
fields: [session.userId],
references: [user.id],
}),
}));
export const accountRelations = relations(account, ({ one }) => ({
export const accountRelations = relations(account, ({one}) => ({
user: one(user, {
fields: [account.userId],
references: [user.id],
}),
}));
export const projectRelations = relations(project, ({ one }) => ({
export const projectRelations = relations(project, ({one}) => ({
organization: one(organization, {
fields: [project.organizationId],
references: [organization.id],
@@ -1,20 +1,20 @@
import { pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core";
import { relations } from "drizzle-orm";
import { project } from "./05_project";
import { project } from "./06_project";
import { createSelectSchema } from "drizzle-zod";
import { z } from "zod";
import {invitation, OrganizationInvitation} from "@/db/schema/04_invitation";
import {member, OrganizationMember} from "@/db/schema/03_member";
import {User} from "@/db/schema/01_user";
import {invitation, OrganizationInvitation} from "@/db/schema/05_invitation";
import {member, OrganizationMember} from "@/db/schema/04_member";
import {User} from "@/db/schema/02_user";
import {timestamps} from "@/db/schema/00_common";
export const organization = pgTable("organization", {
id: uuid("id").defaultRandom().primaryKey(),
name: text("name").notNull(),
slug: text("slug").unique().notNull(),
logo: text("logo"),
createdAt: timestamp("created_at").notNull().defaultNow(),
updatedAt: timestamp("updated_at"),
metadata: text("metadata"),
...timestamps
});
@@ -1,9 +1,10 @@
import {pgTable, text, timestamp, uuid} from "drizzle-orm/pg-core";
import {user} from "@/db/schema/01_user";
import {organization} from "@/db/schema/02_organization";
import {user} from "@/db/schema/02_user";
import {organization} from "@/db/schema/03_organization";
import {relations} from "drizzle-orm";
import {createSelectSchema} from "drizzle-zod";
import {z} from "zod";
import {timestamps} from "@/db/schema/00_common";
export const member = pgTable("member", {
@@ -15,8 +16,7 @@ export const member = pgTable("member", {
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
role: text("role").notNull(),
createdAt: timestamp("created_at").notNull().defaultNow(),
updatedAt: timestamp("updated_at"),
...timestamps
});
export const memberRelations = relations(member, ({ one }) => ({
@@ -1,9 +1,10 @@
import {relations} from "drizzle-orm";
import {user} from "@/db/schema/01_user";
import {organization} from "@/db/schema/02_organization";
import {user} from "@/db/schema/02_user";
import {organization} from "@/db/schema/03_organization";
import {createSelectSchema} from "drizzle-zod";
import {z} from "zod";
import {pgTable, text, timestamp, uuid} from "drizzle-orm/pg-core";
import {timestamps} from "@/db/schema/00_common";
export const invitation = pgTable("invitation", {
@@ -18,6 +19,7 @@ export const invitation = pgTable("invitation", {
inviterId: uuid("inviter_id")
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
...timestamps
});
@@ -1,20 +1,21 @@
import { pgTable, text, boolean, uuid, timestamp } from "drizzle-orm/pg-core";
import { relations } from "drizzle-orm";
import { Organization, organization } from "./02_organization";
import { Organization, organization } from "./03_organization";
import { createSelectSchema } from "drizzle-zod";
import { z } from "zod";
import { Database, database } from "./06_database";
import { Database, database } from "./07_database";
import {timestamps} from "@/db/schema/00_common";
export const project = pgTable("projects", {
id: uuid("id").primaryKey().defaultRandom(),
slug: text("slug").notNull().unique(),
name: text("name").notNull().notNull(),
isArchived: boolean("is_archived").default(false),
createdAt: timestamp("created_at").notNull().defaultNow(),
updatedAt: timestamp("updated_at"),
organizationId: uuid("organization_id")
.notNull()
.references(() => organization.id),
...timestamps
});
export const projectRelations = relations(project, ({ one, many }) => ({
@@ -1,10 +1,11 @@
import {pgTable, text, boolean, timestamp, uuid, integer, pgEnum, uniqueIndex} from "drizzle-orm/pg-core";
import {Agent, agent} from "./07_agent";
import {Project, project} from "./05_project";
import {Agent, agent} from "./08_agent";
import {Project, project} from "./06_project";
import {relations} from "drizzle-orm";
import {dbmsEnum, statusEnum} from "./types";
import {createSelectSchema} from "drizzle-zod";
import {z} from "zod";
import {timestamps} from "@/db/schema/00_common";
export const database = pgTable("databases", {
id: uuid("id").primaryKey().defaultRandom(),
@@ -15,8 +16,6 @@ export const database = pgTable("databases", {
backupPolicy: text("backup_policy"),
isWaitingForBackup: boolean("is_waiting_for_backup").default(false).notNull(),
backupToRestore: text("backup_to_restore"),
createdAt: timestamp("created_at").notNull().defaultNow(),
updatedAt: timestamp("updated_at"),
agentId: uuid("agent_id")
.notNull()
.references(() => agent.id, {onDelete: "cascade"}),
@@ -24,6 +23,7 @@ export const database = pgTable("databases", {
projectId: uuid("project_id")
.references(() => project.id),
...timestamps
});
@@ -33,11 +33,10 @@ export const backup = pgTable(
id: uuid("id").primaryKey().defaultRandom(),
status: statusEnum("status").default("waiting").notNull(),
file: text("file"),
createdAt: timestamp("created_at").notNull().defaultNow(),
updatedAt: timestamp("updated_at"),
databaseId: uuid("database_id")
.notNull()
.references(() => database.id, {onDelete: "cascade"}),
...timestamps
},
// (table) => [uniqueIndex("database_id_status_unique").on(table.databaseId, table.status)]
);
@@ -54,20 +53,19 @@ export const retentionPolicy = pgTable("retention_policies", {
gfsWeekly: integer("gfs_weekly").default(4),
gfsMonthly: integer("gfs_monthly").default(12),
gfsYearly: integer("gfs_yearly").default(3),
createdAt: timestamp("created_at").notNull().defaultNow(),
updatedAt: timestamp("updated_at"),
...timestamps
});
export const restoration = pgTable("restorations", {
id: uuid("id").primaryKey().defaultRandom(),
status: statusEnum("status").default("waiting").notNull(),
createdAt: timestamp("created_at").notNull().defaultNow(),
updatedAt: timestamp("updated_at"),
backupId: uuid("backup_id")
.notNull()
.references(() => backup.id, {onDelete: "cascade"}),
databaseId: uuid("database_id").references(() => database.id, {onDelete: "cascade"}),
...timestamps
});
export const databaseRelations = relations(database, ({one, many}) => ({
@@ -107,6 +105,5 @@ export type DatabaseWith = Database & {
backups?: Backup[] | null;
restorations?: Restoration[] | null;
retentionPolicy?: RetentionPolicy | null;
};
@@ -1,8 +1,9 @@
import {boolean, pgTable, text, timestamp, uuid} from "drizzle-orm/pg-core";
import { createSelectSchema } from "drizzle-zod";
import { z } from "zod";
import {database} from "@/db/schema/06_database";
import {database} from "@/db/schema/07_database";
import {relations} from "drizzle-orm";
import {timestamps} from "@/db/schema/00_common";
export const agent = pgTable("agents", {
id: uuid("id").primaryKey().defaultRandom(),
@@ -10,9 +11,9 @@ export const agent = pgTable("agents", {
name: text("name").notNull().notNull(),
description: text("description").notNull(),
isArchived: boolean("is_archived").default(false),
createdAt: timestamp("created_at").notNull().defaultNow(),
updatedAt: timestamp("updated_at"),
lastContact: timestamp("last_contact"),
...timestamps
});
export const agentSchema = createSelectSchema(agent);
+3
View File
@@ -0,0 +1,3 @@
export function withUpdatedAt<T extends object>(data: T): T & { updatedAt: Date } {
return {...data, updatedAt: new Date()};
}
+10 -6
View File
@@ -21,10 +21,10 @@ import {createRestorationAction, deleteBackupAction} from "@/features/dashboard/
import {toast} from "sonner";
import {useRouter} from "next/navigation";
import {StatusBadge} from "@/components/wrappers/common/status-badge";
import {Backup, DatabaseWith} from "@/db/schema/06_database";
import {Backup, DatabaseWith} from "@/db/schema/07_database";
import {formatFrenchDate} from "@/utils/date-formatting";
import {TooltipCustom} from "@/components/wrappers/common/tooltip-custom";
import {Setting} from "@/db/schema/00_setting";
import {Setting} from "@/db/schema/01_setting";
import {SafeActionResult} from "next-safe-action";
import {ZodString} from "zod";
import {ServerActionResult} from "@/types/action-type";
@@ -80,18 +80,22 @@ export function backupColumns(isAlreadyRestore: boolean, settings: Setting, data
const mutationDeleteBackup = useMutation({
mutationFn: async () => {
const restoration = await deleteBackupAction({
const deletion = await deleteBackupAction({
backupId: rowData.id,
databaseId: rowData.databaseId,
file: rowData.file!,
projectSlug: database.project?.slug!
});
// @ts-ignore
if (restoration.data.success) {
if (deletion.data.success) {
// @ts-ignore
toast.success(restoration.data.actionSuccess.message);
toast.success(deletion.data.actionSuccess.message);
router.refresh();
} else {
// @ts-ignore
toast.error(restoration.data.actionError.message);
toast.error(deletion.data.actionError.message);
}
},
});
+1 -1
View File
@@ -13,7 +13,7 @@ import { Button } from "@/components/ui/button";
import {MoreHorizontal, Trash2} from "lucide-react";
import { ReloadIcon } from "@radix-ui/react-icons";
import { StatusBadge } from "@/components/wrappers/common/status-badge";
import {Backup, Restoration} from "@/db/schema/06_database";
import {Backup, Restoration} from "@/db/schema/07_database";
import {formatFrenchDate} from "@/utils/date-formatting";
import {useMutation} from "@tanstack/react-query";
import {
@@ -1,12 +1,20 @@
"use server"
import { userAction } from "@/safe-actions";
import { z } from "zod";
import { ServerActionResult } from "@/types/action-type";
import {userAction} from "@/safe-actions";
import {z} from "zod";
import {ServerActionResult} from "@/types/action-type";
import * as drizzleDb from "@/db";
import { db } from "@/db";
import { and, eq } from "drizzle-orm";
import {Backup, Restoration} from "@/db/schema/06_database";
import {db} from "@/db";
import {and, eq} from "drizzle-orm";
import {Backup, Restoration} from "@/db/schema/07_database";
import {NextResponse} from "next/server";
import {
deleteFileS3Private,
deleteLocalPrivate,
uploadLocalPrivate,
uploadS3Private
} from "@/features/upload/private/upload.action";
import {env} from "@/env.mjs";
export const deleteRestoreAction = userAction
.schema(
@@ -14,7 +22,7 @@ export const deleteRestoreAction = userAction
restorationId: z.string(),
})
)
.action(async ({ parsedInput }): Promise<ServerActionResult<Backup>> => {
.action(async ({parsedInput}): Promise<ServerActionResult<Backup>> => {
try {
await db
.delete(drizzleDb.schemas.restoration)
@@ -36,24 +44,60 @@ export const deleteRestoreAction = userAction
message: "Failed to delete restoration.",
status: 500,
cause: error instanceof Error ? error.message : "Unknown error",
messageParams: { message: "Error deleting the restoration" },
messageParams: {message: "Error deleting the restoration"},
},
};
}
});
export const deleteBackupAction = userAction
.schema(
z.object({
backupId: z.string(),
databaseId: z.string(),
projectSlug: z.string(),
file: z.string(),
})
)
.action(async ({ parsedInput }): Promise<ServerActionResult<Backup>> => {
.action(async ({parsedInput}): Promise<ServerActionResult<Backup>> => {
try {
const [settings] = await db.select().from(drizzleDb.schemas.setting).where(eq(drizzleDb.schemas.setting.name, "system")).limit(1);
if (!settings) {
return {
success: false,
actionError: {
message: "No settings found.",
status: 404,
cause: "No settings found.",
messageParams: {message: "Error deleting the backup"},
},
};
}
let success: boolean, message: string;
const result =
settings.storage === "local"
? await deleteLocalPrivate(parsedInput.file)
: await deleteFileS3Private(`${parsedInput.projectSlug}/${parsedInput.file}`, env.S3_BUCKET_NAME!);
({success, message} = result);
if (!success) {
return {
success: false,
actionError: {
message: message,
status: 404,
cause: "Unable to delete backup from storage",
messageParams: {message: "Error deleting the backup"},
},
};
}
await db
.delete(drizzleDb.schemas.backup)
.where(and(eq(drizzleDb.schemas.backup.id, parsedInput.backupId), eq(drizzleDb.schemas.backup.databaseId, parsedInput.databaseId)))
@@ -65,6 +109,7 @@ export const deleteBackupAction = userAction
.where(and(eq(drizzleDb.schemas.backup.id, parsedInput.backupId), eq(drizzleDb.schemas.backup.databaseId, parsedInput.databaseId)))
.execute();
if (backupExists.length === 0) {
return {
success: true,
@@ -78,8 +123,8 @@ export const deleteBackupAction = userAction
actionError: {
message: "Backup not found or already deleted.",
status: 404,
cause: "Backup could not be deleted.",
messageParams: { message: "Error deleting the backup" },
cause: "Backup could not be deleted (from database or remote storage).",
messageParams: {message: "Error deleting the backup"},
},
};
}
@@ -90,25 +135,24 @@ export const deleteBackupAction = userAction
message: "Failed to delete backup.",
status: 500,
cause: error instanceof Error ? error.message : "Unknown error",
messageParams: { message: "Error deleting the backup" },
messageParams: {message: "Error deleting the backup"},
},
};
}
});
export const rerunRestorationAction = userAction
.schema(
z.object({
restorationId: z.string(),
})
)
.action(async ({ parsedInput }): Promise<ServerActionResult<Restoration>> => {
.action(async ({parsedInput}): Promise<ServerActionResult<Restoration>> => {
try {
const updateResult = await db
.update(drizzleDb.schemas.restoration)
.set({ status: "waiting" })
.set({status: "waiting"})
.where(eq(drizzleDb.schemas.restoration.id, parsedInput.restorationId))
.returning()
.execute();
@@ -122,7 +166,7 @@ export const rerunRestorationAction = userAction
message: "Restoration not found.",
status: 404,
cause: "No restoration with the given ID exists.",
messageParams: { message: "Restoration not found" },
messageParams: {message: "Restoration not found"},
},
};
}
@@ -132,7 +176,7 @@ export const rerunRestorationAction = userAction
value: updatedRestoration,
actionSuccess: {
message: "Restoration has been requeued.",
messageParams: { restorationId: updatedRestoration.id },
messageParams: {restorationId: updatedRestoration.id},
},
};
} catch (error) {
@@ -142,7 +186,7 @@ export const rerunRestorationAction = userAction
message: "Failed to rerun restoration.",
status: 500,
cause: error instanceof Error ? error.message : "Unknown error",
messageParams: { message: "Error updating the restoration" },
messageParams: {message: "Error updating the restoration"},
},
};
}
@@ -157,7 +201,7 @@ export const createRestorationAction = userAction
databaseId: z.string(),
})
)
.action(async ({ parsedInput }): Promise<ServerActionResult<Restoration>> => {
.action(async ({parsedInput}): Promise<ServerActionResult<Restoration>> => {
try {
// Insert new restoration into the database
const restorationData = await db
@@ -177,7 +221,7 @@ export const createRestorationAction = userAction
value: createdRestoration,
actionSuccess: {
message: "Restoration has been successfully created.",
messageParams: { restorationId: createdRestoration.id },
messageParams: {restorationId: createdRestoration.id},
},
};
} catch (error) {
@@ -187,7 +231,7 @@ export const createRestorationAction = userAction
message: "Failed to create restoration.",
status: 500,
cause: error instanceof Error ? error.message : "Unknown error",
messageParams: { message: "Error creating the restoration" },
messageParams: {message: "Error creating the restoration"},
},
};
}
+50 -6
View File
@@ -4,15 +4,12 @@ import {mkdir, writeFile} from "fs/promises";
import path from "path";
import * as fs from "node:fs";
import {getServerUrl} from "@/utils/get-server-url";
import {createPresignedUrlToDownload, saveFileInBucket} from "@/utils/s3-file-management";
import crypto from "crypto";
import {createPresignedUrlToDownload, deleteFileFromBucket, saveFileInBucket} from "@/utils/s3-file-management";
import {env} from "@/env.mjs";
import {action, userAction} from "@/safe-actions";
import {action} from "@/safe-actions";
import {z} from "zod";
import {ServerActionResult} from "@/types/action-type";
import {Backup} from "@/db/schema/06_database";
import {db} from "@/db";
import * as drizzleDb from "@/db";
import {unlink} from "fs/promises";
const privateLocalDir = "private/uploads/files/";
const privateS3Dir = "backups/";
@@ -54,6 +51,53 @@ export async function uploadS3Private(fileName: string, buffer: any, bucketName:
}
export async function deleteFileS3Private(fileName: string, bucketName: string) {
try {
await deleteFileFromBucket({
bucketName,
fileName: `${privateS3Dir}${fileName}`,
});
return {
success: true,
message: "File deleted successfully",
};
} catch (error) {
console.error("Error occurred:", error);
throw new Error("An error occurred while deleting the private file");
}
}
/**
* Delete a file from local private storage
*/
export async function deleteLocalPrivate(fileName: string) {
try {
const filePath = path.join(process.cwd(), privateLocalDir, fileName);
// Delete locally
await unlink(filePath);
return {
success: true,
message: `File '${fileName}' deleted successfully`,
};
} catch (error: any) {
if (error.code === "ENOENT") {
return {
success: false,
message: `File '${fileName}' not found`,
};
}
console.error("Error occurred while deleting file:", error);
throw new Error("An error occurred while deleting the file");
}
}
// export async function getFileUrlPresignedLocal(fileName: string) {
// try {
// const filePath = path.join(privateLocalDir, fileName);
+1 -1
View File
@@ -11,7 +11,7 @@ import { UploadedObjectInfo } from "minio/src/internal/type";
import { getServerUrl } from "@/utils/get-server-url";
import { db } from "@/db";
import { eq } from "drizzle-orm";
import {Setting} from "@/db/schema/00_setting";
import {Setting} from "@/db/schema/01_setting";
import * as drizzleDb from "@/db";
export const uploadImageAction = userAction.schema(z.instanceof(FormData)).action(async ({ parsedInput: formData, ctx }) => {
+1 -1
View File
@@ -8,7 +8,7 @@ import {admin as adminPlugin, openAPI, Organization, organization} from "better-
import {ac, admin, orgAdmin, orgMember, orgOwner, pending, superadmin, user} from "@/lib/auth/permissions";
import {headers} from "next/headers";
import {count, eq} from "drizzle-orm";
import {OrganizationWithMembers, OrganizationWithMembersAndUsers} from "@/db/schema/02_organization";
import {OrganizationWithMembers, OrganizationWithMembersAndUsers} from "@/db/schema/03_organization";
export const auth = betterAuth({
database: drizzleAdapter(db, {
+1 -1
View File
@@ -2,7 +2,7 @@ import {db} from "@/db";
import {enforceRetentionCount} from "@/lib/tasks/database/retention-count";
import {enforceRetentionDays} from "@/lib/tasks/database/retention-days";
import {enforceRetentionGFS} from "@/lib/tasks/database/retention-gsf";
import {retentionPolicy} from "@/db/schema/06_database";
import {retentionPolicy} from "@/db/schema/07_database";
export const retentionCleanTask = async () => {
+40
View File
@@ -68,6 +68,46 @@ export async function createBucketIfNotExists(bucketName: string) {
}
}
/**
* Delete a file from a bucket
* @param bucketName name of the bucket
* @param fileName name of the file
* @returns true if deleted, false if not
*/
export async function deleteFileFromBucket({
bucketName,
fileName,
}: {
bucketName: string;
fileName: string;
}): Promise<boolean> {
const s3Client = await getS3Client();
try {
const fileExists = await checkFileExistsInBucket({ bucketName, fileName });
if (!fileExists) {
console.warn(`File not found: ${bucketName}/${fileName}`);
return false;
}
await s3Client.removeObject(bucketName, fileName);
console.log(`Deleted file: ${bucketName}/${fileName}`);
return true;
} catch (error: any) {
console.error("Error deleting file from bucket:", {
bucketName,
fileName,
error: error.message,
});
return false;
}
}
/**
* Save file in S3 bucket
* @param bucketName name of the bucket