@arch-cadre/backup-module 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,131 @@
1
+ "use strict";
2
+ "use server";
3
+
4
+ Object.defineProperty(exports, "__esModule", {
5
+ value: true
6
+ });
7
+ exports.createBackup = createBackup;
8
+ exports.deleteBackup = deleteBackup;
9
+ exports.getBackups = getBackups;
10
+ exports.restoreBackup = restoreBackup;
11
+ var _nodeChild_process = require("node:child_process");
12
+ var _promises = _interopRequireDefault(require("node:fs/promises"));
13
+ var _nodePath = _interopRequireDefault(require("node:path"));
14
+ var _nodeUtil = require("node:util");
15
+ var _server = require("@arch-cadre/core/server");
16
+ var _drizzleOrm = require("drizzle-orm");
17
+ var _schema = require("./schema.cjs");
18
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
19
+ const execAsync = (0, _nodeUtil.promisify)(_nodeChild_process.exec);
20
+ const BACKUP_DIR = _nodePath.default.join(process.cwd(), "backups");
21
+ async function getBackups() {
22
+ return await _server.db.select().from(_schema.backupTable).orderBy((0, _drizzleOrm.desc)(_schema.backupTable.createdAt));
23
+ }
24
+ async function createBackup() {
25
+ const timestamp = (/* @__PURE__ */new Date()).toISOString().replace(/[:.]/g, "-");
26
+ const filename = `backup-${timestamp}.sql`;
27
+ const filePath = _nodePath.default.join(BACKUP_DIR, filename);
28
+ try {
29
+ await _promises.default.mkdir(BACKUP_DIR, {
30
+ recursive: true
31
+ });
32
+ const dbUrl = process.env.DATABASE_URL;
33
+ if (!dbUrl) throw new Error("DATABASE_URL is not defined");
34
+ await execAsync(`pg_dump "${dbUrl}" -c --if-exists -x -O > "${filePath}"`);
35
+ const stats = await _promises.default.stat(filePath);
36
+ const [backup] = await _server.db.insert(_schema.backupTable).values({
37
+ filename,
38
+ size: Math.round(stats.size),
39
+ status: "success"
40
+ }).returning();
41
+ const {
42
+ user
43
+ } = await (0, _server.getCurrentSession)();
44
+ await _server.eventBus.publish("activity.create", {
45
+ action: "backup.create",
46
+ description: `Created database backup: ${filename}`,
47
+ userId: user?.id,
48
+ metadata: {
49
+ filename,
50
+ size: stats.size,
51
+ backupId: backup.id
52
+ }
53
+ }, "backup");
54
+ return {
55
+ success: true,
56
+ backup
57
+ };
58
+ } catch (error) {
59
+ console.error("[Backup] Create backup failed:", error);
60
+ return {
61
+ success: false,
62
+ error: error.message
63
+ };
64
+ }
65
+ }
66
+ async function restoreBackup(id) {
67
+ try {
68
+ const [backup] = await _server.db.select().from(_schema.backupTable).where((0, _drizzleOrm.eq)(_schema.backupTable.id, id));
69
+ if (!backup) throw new Error("Backup not found");
70
+ const filePath = _nodePath.default.join(BACKUP_DIR, backup.filename);
71
+ const dbUrl = process.env.DATABASE_URL;
72
+ if (!dbUrl) throw new Error("DATABASE_URL is not defined");
73
+ await _promises.default.access(filePath);
74
+ await execAsync(`psql "${dbUrl}" < "${filePath}"`);
75
+ const {
76
+ user
77
+ } = await (0, _server.getCurrentSession)();
78
+ await _server.eventBus.publish("activity.create", {
79
+ action: "backup.restore",
80
+ description: `Restored database from backup: ${backup.filename}`,
81
+ userId: user?.id,
82
+ metadata: {
83
+ filename: backup.filename,
84
+ backupId: backup.id
85
+ }
86
+ }, "backup");
87
+ return {
88
+ success: true
89
+ };
90
+ } catch (error) {
91
+ console.error("[Backup] Restore backup failed:", error);
92
+ return {
93
+ success: false,
94
+ error: error.message
95
+ };
96
+ }
97
+ }
98
+ async function deleteBackup(id) {
99
+ try {
100
+ const [backup] = await _server.db.select().from(_schema.backupTable).where((0, _drizzleOrm.eq)(_schema.backupTable.id, id));
101
+ if (!backup) throw new Error("Backup not found");
102
+ const filePath = _nodePath.default.join(BACKUP_DIR, backup.filename);
103
+ try {
104
+ await _promises.default.unlink(filePath);
105
+ } catch (_e) {
106
+ console.warn(`[Backup] File ${filePath} could not be deleted or doesn't exist`);
107
+ }
108
+ await _server.db.delete(_schema.backupTable).where((0, _drizzleOrm.eq)(_schema.backupTable.id, id));
109
+ const {
110
+ user
111
+ } = await (0, _server.getCurrentSession)();
112
+ await _server.eventBus.publish("activity.create", {
113
+ action: "backup.delete",
114
+ description: `Deleted database backup: ${backup.filename}`,
115
+ userId: user?.id,
116
+ metadata: {
117
+ filename: backup.filename,
118
+ backupId: id
119
+ }
120
+ }, "backup");
121
+ return {
122
+ success: true
123
+ };
124
+ } catch (error) {
125
+ console.error("[Backup] Delete backup failed:", error);
126
+ return {
127
+ success: false,
128
+ error: error.message
129
+ };
130
+ }
131
+ }
@@ -0,0 +1,24 @@
1
+ export declare function getBackups(): Promise<any>;
2
+ export declare function createBackup(): Promise<{
3
+ success: boolean;
4
+ backup: any;
5
+ error?: undefined;
6
+ } | {
7
+ success: boolean;
8
+ error: string;
9
+ backup?: undefined;
10
+ }>;
11
+ export declare function restoreBackup(id: string): Promise<{
12
+ success: boolean;
13
+ error?: undefined;
14
+ } | {
15
+ success: boolean;
16
+ error: string;
17
+ }>;
18
+ export declare function deleteBackup(id: string): Promise<{
19
+ success: boolean;
20
+ error?: undefined;
21
+ } | {
22
+ success: boolean;
23
+ error: string;
24
+ }>;
@@ -0,0 +1,101 @@
1
+ "use server";
2
+ import { exec } from "node:child_process";
3
+ import fs from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { promisify } from "node:util";
6
+ import { db, eventBus, getCurrentSession } from "@arch-cadre/core/server";
7
+ import { desc, eq } from "drizzle-orm";
8
+ import { backupTable } from "./schema.mjs";
9
+ const execAsync = promisify(exec);
10
+ const BACKUP_DIR = path.join(process.cwd(), "backups");
11
+ export async function getBackups() {
12
+ return await db.select().from(backupTable).orderBy(desc(backupTable.createdAt));
13
+ }
14
+ export async function createBackup() {
15
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
16
+ const filename = `backup-${timestamp}.sql`;
17
+ const filePath = path.join(BACKUP_DIR, filename);
18
+ try {
19
+ await fs.mkdir(BACKUP_DIR, { recursive: true });
20
+ const dbUrl = process.env.DATABASE_URL;
21
+ if (!dbUrl) throw new Error("DATABASE_URL is not defined");
22
+ await execAsync(`pg_dump "${dbUrl}" -c --if-exists -x -O > "${filePath}"`);
23
+ const stats = await fs.stat(filePath);
24
+ const [backup] = await db.insert(backupTable).values({
25
+ filename,
26
+ size: Math.round(stats.size),
27
+ status: "success"
28
+ }).returning();
29
+ const { user } = await getCurrentSession();
30
+ await eventBus.publish(
31
+ "activity.create",
32
+ {
33
+ action: "backup.create",
34
+ description: `Created database backup: ${filename}`,
35
+ userId: user?.id,
36
+ metadata: { filename, size: stats.size, backupId: backup.id }
37
+ },
38
+ "backup"
39
+ );
40
+ return { success: true, backup };
41
+ } catch (error) {
42
+ console.error("[Backup] Create backup failed:", error);
43
+ return { success: false, error: error.message };
44
+ }
45
+ }
46
+ export async function restoreBackup(id) {
47
+ try {
48
+ const [backup] = await db.select().from(backupTable).where(eq(backupTable.id, id));
49
+ if (!backup) throw new Error("Backup not found");
50
+ const filePath = path.join(BACKUP_DIR, backup.filename);
51
+ const dbUrl = process.env.DATABASE_URL;
52
+ if (!dbUrl) throw new Error("DATABASE_URL is not defined");
53
+ await fs.access(filePath);
54
+ await execAsync(`psql "${dbUrl}" < "${filePath}"`);
55
+ const { user } = await getCurrentSession();
56
+ await eventBus.publish(
57
+ "activity.create",
58
+ {
59
+ action: "backup.restore",
60
+ description: `Restored database from backup: ${backup.filename}`,
61
+ userId: user?.id,
62
+ metadata: { filename: backup.filename, backupId: backup.id }
63
+ },
64
+ "backup"
65
+ );
66
+ return { success: true };
67
+ } catch (error) {
68
+ console.error("[Backup] Restore backup failed:", error);
69
+ return { success: false, error: error.message };
70
+ }
71
+ }
72
+ export async function deleteBackup(id) {
73
+ try {
74
+ const [backup] = await db.select().from(backupTable).where(eq(backupTable.id, id));
75
+ if (!backup) throw new Error("Backup not found");
76
+ const filePath = path.join(BACKUP_DIR, backup.filename);
77
+ try {
78
+ await fs.unlink(filePath);
79
+ } catch (_e) {
80
+ console.warn(
81
+ `[Backup] File ${filePath} could not be deleted or doesn't exist`
82
+ );
83
+ }
84
+ await db.delete(backupTable).where(eq(backupTable.id, id));
85
+ const { user } = await getCurrentSession();
86
+ await eventBus.publish(
87
+ "activity.create",
88
+ {
89
+ action: "backup.delete",
90
+ description: `Deleted database backup: ${backup.filename}`,
91
+ userId: user?.id,
92
+ metadata: { filename: backup.filename, backupId: id }
93
+ },
94
+ "backup"
95
+ );
96
+ return { success: true };
97
+ } catch (error) {
98
+ console.error("[Backup] Delete backup failed:", error);
99
+ return { success: false, error: error.message };
100
+ }
101
+ }
@@ -0,0 +1,13 @@
1
+ # Database Backup Module
2
+
3
+ This module allows you to create full SQL snapshots of your database using `pg_dump`.
4
+
5
+ ## Features
6
+ - **Create Backup**: Generates a portable `.sql` file with all tables and data.
7
+ - **Restore**: Easily revert your database to a previous state.
8
+ - **Activity Logging**: Every backup action is recorded in the system logs.
9
+
10
+ ## Storage
11
+ All backups are stored in the root `/backups` directory of the project.
12
+
13
+ > **Warning**: Restoring a backup will overwrite all current data in the database. Always ensure you have a recent snapshot before performing a restore.
package/dist/index.cjs ADDED
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+
7
+ var _server = require("@arch-cadre/core/server");
8
+ var _manifest = _interopRequireDefault(require("../manifest.json"));
9
+ var _navigation = require("./navigation.cjs");
10
+ var _routes = require("./routes.cjs");
11
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
12
+ const backupModule = {
13
+ manifest: _manifest.default,
14
+ navigation: _navigation.navigation,
15
+ routes: {
16
+ private: _routes.privateRoutes
17
+ },
18
+ onEnable: async () => {
19
+ console.log("[Backup] Module enabled.");
20
+ },
21
+ onDisable: async () => {
22
+ try {
23
+ console.log("[Backup] Cleaning up database tables...");
24
+ await _server.db.execute(`DROP TABLE IF EXISTS backups;`);
25
+ console.log("[Backup] Database tables removed.");
26
+ } catch (error) {
27
+ console.error("[Backup] Failed to remove tables:", error);
28
+ }
29
+ console.log("[Backup] Module disabled.");
30
+ },
31
+ init: async () => {
32
+ console.log("[Backup] Module initialized.");
33
+ }
34
+ };
35
+ module.exports = backupModule;
@@ -0,0 +1,3 @@
1
+ import type { IModule } from "@arch-cadre/modules";
2
+ declare const backupModule: IModule;
3
+ export default backupModule;
package/dist/index.mjs ADDED
@@ -0,0 +1,28 @@
1
+ import { db } from "@arch-cadre/core/server";
2
+ import manifest from "../manifest.json";
3
+ import { navigation } from "./navigation.mjs";
4
+ import { privateRoutes } from "./routes.mjs";
5
+ const backupModule = {
6
+ manifest,
7
+ navigation,
8
+ routes: {
9
+ private: privateRoutes
10
+ },
11
+ onEnable: async () => {
12
+ console.log("[Backup] Module enabled.");
13
+ },
14
+ onDisable: async () => {
15
+ try {
16
+ console.log("[Backup] Cleaning up database tables...");
17
+ await db.execute(`DROP TABLE IF EXISTS backups;`);
18
+ console.log("[Backup] Database tables removed.");
19
+ } catch (error) {
20
+ console.error("[Backup] Failed to remove tables:", error);
21
+ }
22
+ console.log("[Backup] Module disabled.");
23
+ },
24
+ init: async () => {
25
+ console.log("[Backup] Module initialized.");
26
+ }
27
+ };
28
+ export default backupModule;
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.navigation = void 0;
7
+ const navigation = exports.navigation = {
8
+ admin: {
9
+ System: [{
10
+ title: "Backups",
11
+ url: "/backups",
12
+ icon: "solar:database-bold-duotone"
13
+ }]
14
+ }
15
+ };
@@ -0,0 +1,2 @@
1
+ import type { ModuleNavigation } from "@arch-cadre/modules";
2
+ export declare const navigation: ModuleNavigation;
@@ -0,0 +1,11 @@
1
+ export const navigation = {
2
+ admin: {
3
+ System: [
4
+ {
5
+ title: "Backups",
6
+ url: "/backups",
7
+ icon: "solar:database-bold-duotone"
8
+ }
9
+ ]
10
+ }
11
+ };
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.publicRoutes = exports.privateRoutes = void 0;
7
+ var _backupList = _interopRequireDefault(require("./ui/pages/backup-list.cjs"));
8
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
9
+ const publicRoutes = exports.publicRoutes = [];
10
+ const privateRoutes = exports.privateRoutes = [{
11
+ path: "/backups",
12
+ component: _backupList.default
13
+ }];
@@ -0,0 +1,3 @@
1
+ import type { PrivateRouteDefinition, PublicRouteDefinition } from "@arch-cadre/modules";
2
+ export declare const publicRoutes: PublicRouteDefinition[];
3
+ export declare const privateRoutes: PrivateRouteDefinition[];
@@ -0,0 +1,8 @@
1
+ import BackupListPage from "./ui/pages/backup-list.mjs";
2
+ export const publicRoutes = [];
3
+ export const privateRoutes = [
4
+ {
5
+ path: "/backups",
6
+ component: BackupListPage
7
+ }
8
+ ];
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.backupTable = void 0;
7
+ var _pgCore = require("drizzle-orm/pg-core");
8
+ const backupTable = exports.backupTable = (0, _pgCore.pgTable)("backups", {
9
+ id: (0, _pgCore.text)("id").$defaultFn(() => crypto.randomUUID()).notNull().primaryKey(),
10
+ filename: (0, _pgCore.text)("filename").notNull(),
11
+ size: (0, _pgCore.integer)("size").notNull(),
12
+ // in bytes
13
+ createdAt: (0, _pgCore.timestamp)("created_at", {
14
+ precision: 3
15
+ }).notNull().defaultNow(),
16
+ status: (0, _pgCore.text)("status").notNull()
17
+ // 'success', 'failed'
18
+ });
@@ -0,0 +1,82 @@
1
+ export declare const backupTable: import("drizzle-orm/pg-core").PgTableWithColumns<{
2
+ name: "backups";
3
+ schema: undefined;
4
+ columns: {
5
+ id: import("drizzle-orm/pg-core").PgBuildColumn<"backups", import("drizzle-orm/pg-core").SetIsPrimaryKey<import("drizzle-orm/pg-core").SetNotNull<import("drizzle-orm/pg-core").SetHasRuntimeDefault<import("drizzle-orm/pg-core").PgTextBuilder<[string, ...string[]]>>>>, {
6
+ name: string;
7
+ tableName: "backups";
8
+ dataType: "string";
9
+ data: string;
10
+ driverParam: string;
11
+ notNull: true;
12
+ hasDefault: true;
13
+ isPrimaryKey: false;
14
+ isAutoincrement: false;
15
+ hasRuntimeDefault: false;
16
+ enumValues: undefined;
17
+ identity: undefined;
18
+ generated: undefined;
19
+ }>;
20
+ filename: import("drizzle-orm/pg-core").PgBuildColumn<"backups", import("drizzle-orm/pg-core").SetNotNull<import("drizzle-orm/pg-core").PgTextBuilder<[string, ...string[]]>>, {
21
+ name: string;
22
+ tableName: "backups";
23
+ dataType: "string";
24
+ data: string;
25
+ driverParam: string;
26
+ notNull: true;
27
+ hasDefault: false;
28
+ isPrimaryKey: false;
29
+ isAutoincrement: false;
30
+ hasRuntimeDefault: false;
31
+ enumValues: undefined;
32
+ identity: undefined;
33
+ generated: undefined;
34
+ }>;
35
+ size: import("drizzle-orm/pg-core").PgBuildColumn<"backups", import("drizzle-orm/pg-core").SetNotNull<import("drizzle-orm/pg-core").PgIntegerBuilder>, {
36
+ name: string;
37
+ tableName: "backups";
38
+ dataType: "number int32";
39
+ data: number;
40
+ driverParam: string | number;
41
+ notNull: true;
42
+ hasDefault: false;
43
+ isPrimaryKey: false;
44
+ isAutoincrement: false;
45
+ hasRuntimeDefault: false;
46
+ enumValues: undefined;
47
+ identity: undefined;
48
+ generated: undefined;
49
+ }>;
50
+ createdAt: import("drizzle-orm/pg-core").PgBuildColumn<"backups", import("drizzle-orm/pg-core").SetHasDefault<import("drizzle-orm/pg-core").SetNotNull<import("drizzle-orm/pg-core").PgTimestampBuilder>>, {
51
+ name: string;
52
+ tableName: "backups";
53
+ dataType: "object date";
54
+ data: Date;
55
+ driverParam: string;
56
+ notNull: true;
57
+ hasDefault: true;
58
+ isPrimaryKey: false;
59
+ isAutoincrement: false;
60
+ hasRuntimeDefault: false;
61
+ enumValues: undefined;
62
+ identity: undefined;
63
+ generated: undefined;
64
+ }>;
65
+ status: import("drizzle-orm/pg-core").PgBuildColumn<"backups", import("drizzle-orm/pg-core").SetNotNull<import("drizzle-orm/pg-core").PgTextBuilder<[string, ...string[]]>>, {
66
+ name: string;
67
+ tableName: "backups";
68
+ dataType: "string";
69
+ data: string;
70
+ driverParam: string;
71
+ notNull: true;
72
+ hasDefault: false;
73
+ isPrimaryKey: false;
74
+ isAutoincrement: false;
75
+ hasRuntimeDefault: false;
76
+ enumValues: undefined;
77
+ identity: undefined;
78
+ generated: undefined;
79
+ }>;
80
+ };
81
+ dialect: "pg";
82
+ }>;
@@ -0,0 +1,10 @@
1
+ import { integer, pgTable, text, timestamp } from "drizzle-orm/pg-core";
2
+ export const backupTable = pgTable("backups", {
3
+ id: text("id").$defaultFn(() => crypto.randomUUID()).notNull().primaryKey(),
4
+ filename: text("filename").notNull(),
5
+ size: integer("size").notNull(),
6
+ // in bytes
7
+ createdAt: timestamp("created_at", { precision: 3 }).notNull().defaultNow(),
8
+ status: text("status").notNull()
9
+ // 'success', 'failed'
10
+ });
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ "use server";
3
+
4
+ Object.defineProperty(exports, "__esModule", {
5
+ value: true
6
+ });
7
+ exports.createBackupAction = createBackupAction;
8
+ exports.deleteBackupAction = deleteBackupAction;
9
+ exports.restoreBackupAction = restoreBackupAction;
10
+ var _cache = require("next/cache");
11
+ var _actions = require("../../actions.cjs");
12
+ async function createBackupAction() {
13
+ await (0, _actions.createBackup)();
14
+ (0, _cache.revalidatePath)("/module/backup");
15
+ }
16
+ async function restoreBackupAction(id) {
17
+ const result = await (0, _actions.restoreBackup)(id);
18
+ (0, _cache.revalidatePath)("/module/backup");
19
+ return result;
20
+ }
21
+ async function deleteBackupAction(id) {
22
+ await (0, _actions.deleteBackup)(id);
23
+ (0, _cache.revalidatePath)("/module/backup");
24
+ }
@@ -0,0 +1,9 @@
1
+ export declare function createBackupAction(): Promise<void>;
2
+ export declare function restoreBackupAction(id: string): Promise<{
3
+ success: boolean;
4
+ error?: undefined;
5
+ } | {
6
+ success: boolean;
7
+ error: string;
8
+ }>;
9
+ export declare function deleteBackupAction(id: string): Promise<void>;
@@ -0,0 +1,16 @@
1
+ "use server";
2
+ import { revalidatePath } from "next/cache";
3
+ import { createBackup, deleteBackup, restoreBackup } from "../../actions.mjs";
4
+ export async function createBackupAction() {
5
+ await createBackup();
6
+ revalidatePath("/module/backup");
7
+ }
8
+ export async function restoreBackupAction(id) {
9
+ const result = await restoreBackup(id);
10
+ revalidatePath("/module/backup");
11
+ return result;
12
+ }
13
+ export async function deleteBackupAction(id) {
14
+ await deleteBackup(id);
15
+ revalidatePath("/module/backup");
16
+ }
@@ -0,0 +1,100 @@
1
+ "use strict";
2
+ "use client";
3
+
4
+ Object.defineProperty(exports, "__esModule", {
5
+ value: true
6
+ });
7
+ exports.CreateBackupButton = CreateBackupButton;
8
+ exports.DeleteBackupButton = DeleteBackupButton;
9
+ exports.RestoreBackupButton = RestoreBackupButton;
10
+ var _ui = require("@arch-cadre/ui");
11
+ var _react = require("react");
12
+ var _backupActions = require("./backup-actions.cjs");
13
+ function CreateBackupButton() {
14
+ const [pending, setPending] = (0, _react.useState)(false);
15
+ const handleCreate = async () => {
16
+ setPending(true);
17
+ try {
18
+ await (0, _backupActions.createBackupAction)();
19
+ _ui.toast.success("Backup created successfully");
20
+ } catch (_error) {
21
+ _ui.toast.error("Failed to create backup");
22
+ } finally {
23
+ setPending(false);
24
+ }
25
+ };
26
+ return /* @__PURE__ */React.createElement(_ui.Button, {
27
+ onClick: handleCreate,
28
+ disabled: pending
29
+ }, pending ? /* @__PURE__ */React.createElement(_ui.Icon, {
30
+ icon: "svg-spinners:180-ring",
31
+ className: "mr-2 h-4 w-4"
32
+ }) : /* @__PURE__ */React.createElement(_ui.Icon, {
33
+ icon: "solar:add-circle-broken",
34
+ className: "mr-2 h-4 w-4"
35
+ }), "Create Backup");
36
+ }
37
+ function RestoreBackupButton({
38
+ id
39
+ }) {
40
+ const [pending, setPending] = (0, _react.useState)(false);
41
+ const handleRestore = async () => {
42
+ if (!confirm("WARNING: This will overwrite your current database. Are you sure you want to restore this backup?")) return;
43
+ setPending(true);
44
+ try {
45
+ const result = await (0, _backupActions.restoreBackupAction)(id);
46
+ if (result.success) {
47
+ _ui.toast.success("Database restored successfully. You might need to refresh the page.");
48
+ } else {
49
+ _ui.toast.error(`Failed to restore backup: ${result.error}`);
50
+ }
51
+ } catch (_error) {
52
+ _ui.toast.error("An unexpected error occurred during restore");
53
+ } finally {
54
+ setPending(false);
55
+ }
56
+ };
57
+ return /* @__PURE__ */React.createElement(_ui.Button, {
58
+ variant: "ghost",
59
+ size: "icon",
60
+ onClick: handleRestore,
61
+ disabled: pending,
62
+ title: "Restore this backup"
63
+ }, pending ? /* @__PURE__ */React.createElement(_ui.Icon, {
64
+ icon: "svg-spinners:180-ring",
65
+ className: "h-4 w-4"
66
+ }) : /* @__PURE__ */React.createElement(_ui.Icon, {
67
+ icon: "solar:restart-broken",
68
+ className: "h-4 w-4"
69
+ }));
70
+ }
71
+ function DeleteBackupButton({
72
+ id
73
+ }) {
74
+ const [pending, setPending] = (0, _react.useState)(false);
75
+ const handleDelete = async () => {
76
+ if (!confirm("Are you sure you want to delete this backup?")) return;
77
+ setPending(true);
78
+ try {
79
+ await (0, _backupActions.deleteBackupAction)(id);
80
+ _ui.toast.success("Backup deleted successfully");
81
+ } catch (_error) {
82
+ _ui.toast.error("Failed to delete backup");
83
+ } finally {
84
+ setPending(false);
85
+ }
86
+ };
87
+ return /* @__PURE__ */React.createElement(_ui.Button, {
88
+ variant: "ghost",
89
+ size: "icon",
90
+ onClick: handleDelete,
91
+ disabled: pending,
92
+ className: "text-destructive hover:text-destructive hover:bg-destructive/10"
93
+ }, pending ? /* @__PURE__ */React.createElement(_ui.Icon, {
94
+ icon: "svg-spinners:180-ring",
95
+ className: "h-4 w-4"
96
+ }) : /* @__PURE__ */React.createElement(_ui.Icon, {
97
+ icon: "solar:trash-bin-trash-broken",
98
+ className: "h-4 w-4"
99
+ }));
100
+ }
@@ -0,0 +1,7 @@
1
+ export declare function CreateBackupButton(): import("react").JSX.Element;
2
+ export declare function RestoreBackupButton({ id }: {
3
+ id: string;
4
+ }): import("react").JSX.Element;
5
+ export declare function DeleteBackupButton({ id }: {
6
+ id: string;
7
+ }): import("react").JSX.Element;
@@ -0,0 +1,84 @@
1
+ "use client";
2
+ import { Button, Icon, toast } from "@arch-cadre/ui";
3
+ import { useState } from "react";
4
+ import {
5
+ createBackupAction,
6
+ deleteBackupAction,
7
+ restoreBackupAction
8
+ } from "./backup-actions.mjs";
9
+ export function CreateBackupButton() {
10
+ const [pending, setPending] = useState(false);
11
+ const handleCreate = async () => {
12
+ setPending(true);
13
+ try {
14
+ await createBackupAction();
15
+ toast.success("Backup created successfully");
16
+ } catch (_error) {
17
+ toast.error("Failed to create backup");
18
+ } finally {
19
+ setPending(false);
20
+ }
21
+ };
22
+ return /* @__PURE__ */ React.createElement(Button, { onClick: handleCreate, disabled: pending }, pending ? /* @__PURE__ */ React.createElement(Icon, { icon: "svg-spinners:180-ring", className: "mr-2 h-4 w-4" }) : /* @__PURE__ */ React.createElement(Icon, { icon: "solar:add-circle-broken", className: "mr-2 h-4 w-4" }), "Create Backup");
23
+ }
24
+ export function RestoreBackupButton({ id }) {
25
+ const [pending, setPending] = useState(false);
26
+ const handleRestore = async () => {
27
+ if (!confirm(
28
+ "WARNING: This will overwrite your current database. Are you sure you want to restore this backup?"
29
+ ))
30
+ return;
31
+ setPending(true);
32
+ try {
33
+ const result = await restoreBackupAction(id);
34
+ if (result.success) {
35
+ toast.success(
36
+ "Database restored successfully. You might need to refresh the page."
37
+ );
38
+ } else {
39
+ toast.error(`Failed to restore backup: ${result.error}`);
40
+ }
41
+ } catch (_error) {
42
+ toast.error("An unexpected error occurred during restore");
43
+ } finally {
44
+ setPending(false);
45
+ }
46
+ };
47
+ return /* @__PURE__ */ React.createElement(
48
+ Button,
49
+ {
50
+ variant: "ghost",
51
+ size: "icon",
52
+ onClick: handleRestore,
53
+ disabled: pending,
54
+ title: "Restore this backup"
55
+ },
56
+ pending ? /* @__PURE__ */ React.createElement(Icon, { icon: "svg-spinners:180-ring", className: "h-4 w-4" }) : /* @__PURE__ */ React.createElement(Icon, { icon: "solar:restart-broken", className: "h-4 w-4" })
57
+ );
58
+ }
59
+ export function DeleteBackupButton({ id }) {
60
+ const [pending, setPending] = useState(false);
61
+ const handleDelete = async () => {
62
+ if (!confirm("Are you sure you want to delete this backup?")) return;
63
+ setPending(true);
64
+ try {
65
+ await deleteBackupAction(id);
66
+ toast.success("Backup deleted successfully");
67
+ } catch (_error) {
68
+ toast.error("Failed to delete backup");
69
+ } finally {
70
+ setPending(false);
71
+ }
72
+ };
73
+ return /* @__PURE__ */ React.createElement(
74
+ Button,
75
+ {
76
+ variant: "ghost",
77
+ size: "icon",
78
+ onClick: handleDelete,
79
+ disabled: pending,
80
+ className: "text-destructive hover:text-destructive hover:bg-destructive/10"
81
+ },
82
+ pending ? /* @__PURE__ */ React.createElement(Icon, { icon: "svg-spinners:180-ring", className: "h-4 w-4" }) : /* @__PURE__ */ React.createElement(Icon, { icon: "solar:trash-bin-trash-broken", className: "h-4 w-4" })
83
+ );
84
+ }
@@ -0,0 +1,56 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ module.exports = BackupListPage;
7
+ var _card = require("@arch-cadre/ui/components/card");
8
+ var _table = require("@arch-cadre/ui/components/table");
9
+ var _dateFns = require("date-fns");
10
+ var _locale = require("date-fns/locale");
11
+ var _actions = require("../../actions.cjs");
12
+ var _backupClient = require("../components/backup-client.cjs");
13
+ function formatBytes(bytes, decimals = 2) {
14
+ if (bytes === 0) return "0 Bytes";
15
+ const k = 1024;
16
+ const dm = decimals < 0 ? 0 : decimals;
17
+ const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
18
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
19
+ return parseFloat((bytes / k ** i).toFixed(dm)) + " " + sizes[i];
20
+ }
21
+ async function BackupListPage() {
22
+ const backups = await (0, _actions.getBackups)();
23
+ return /* @__PURE__ */React.createElement("div", {
24
+ className: "space-y-6"
25
+ }, /* @__PURE__ */React.createElement("div", {
26
+ className: "flex items-center justify-between"
27
+ }, /* @__PURE__ */React.createElement("div", {
28
+ className: "space-y-1"
29
+ }, /* @__PURE__ */React.createElement("h2", {
30
+ className: "text-3xl font-bold tracking-tight"
31
+ }, "Database Backups"), /* @__PURE__ */React.createElement("p", {
32
+ className: "text-muted-foreground text-sm"
33
+ }, "Manage your database snapshots. All backups are stored locally on the server.")), /* @__PURE__ */React.createElement(_backupClient.CreateBackupButton, null)), /* @__PURE__ */React.createElement(_card.Card, null, /* @__PURE__ */React.createElement(_card.CardHeader, null, /* @__PURE__ */React.createElement(_card.CardTitle, null, "Available Backups"), /* @__PURE__ */React.createElement(_card.CardDescription, null, "A list of all backups created in the system.")), /* @__PURE__ */React.createElement(_card.CardContent, null, /* @__PURE__ */React.createElement(_table.Table, null, /* @__PURE__ */React.createElement(_table.TableHeader, null, /* @__PURE__ */React.createElement(_table.TableRow, null, /* @__PURE__ */React.createElement(_table.TableHead, null, "Filename"), /* @__PURE__ */React.createElement(_table.TableHead, null, "Size"), /* @__PURE__ */React.createElement(_table.TableHead, null, "Created"), /* @__PURE__ */React.createElement(_table.TableHead, null, "Status"), /* @__PURE__ */React.createElement(_table.TableHead, {
34
+ className: "text-right"
35
+ }, "Actions"))), /* @__PURE__ */React.createElement(_table.TableBody, null, backups.length === 0 ? /* @__PURE__ */React.createElement(_table.TableRow, null, /* @__PURE__ */React.createElement(_table.TableCell, {
36
+ colSpan: 5,
37
+ className: "text-center py-10 text-muted-foreground"
38
+ }, "No backups found. Create your first backup to see it here.")) : backups.map(backup => /* @__PURE__ */React.createElement(_table.TableRow, {
39
+ key: backup.id
40
+ }, /* @__PURE__ */React.createElement(_table.TableCell, {
41
+ className: "font-medium"
42
+ }, backup.filename), /* @__PURE__ */React.createElement(_table.TableCell, null, formatBytes(backup.size)), /* @__PURE__ */React.createElement(_table.TableCell, {
43
+ title: backup.createdAt.toLocaleString()
44
+ }, (0, _dateFns.formatDistanceToNow)(backup.createdAt, {
45
+ addSuffix: true,
46
+ locale: _locale.pl
47
+ })), /* @__PURE__ */React.createElement(_table.TableCell, null, /* @__PURE__ */React.createElement("span", {
48
+ className: `px-2 py-1 rounded-full text-xs font-semibold ${backup.status === "success" ? "bg-green-100 text-green-700" : "bg-red-100 text-red-700"}`
49
+ }, backup.status)), /* @__PURE__ */React.createElement(_table.TableCell, {
50
+ className: "text-right space-x-1"
51
+ }, /* @__PURE__ */React.createElement(_backupClient.RestoreBackupButton, {
52
+ id: backup.id
53
+ }), /* @__PURE__ */React.createElement(_backupClient.DeleteBackupButton, {
54
+ id: backup.id
55
+ })))))))));
56
+ }
@@ -0,0 +1 @@
1
+ export default function BackupListPage(): Promise<import("react").JSX.Element>;
@@ -0,0 +1,51 @@
1
+ import {
2
+ Card,
3
+ CardContent,
4
+ CardDescription,
5
+ CardHeader,
6
+ CardTitle
7
+ } from "@arch-cadre/ui/components/card";
8
+ import {
9
+ Table,
10
+ TableBody,
11
+ TableCell,
12
+ TableHead,
13
+ TableHeader,
14
+ TableRow
15
+ } from "@arch-cadre/ui/components/table";
16
+ import { formatDistanceToNow } from "date-fns";
17
+ import { pl } from "date-fns/locale";
18
+ import { getBackups } from "../../actions.mjs";
19
+ import {
20
+ CreateBackupButton,
21
+ DeleteBackupButton,
22
+ RestoreBackupButton
23
+ } from "../components/backup-client.mjs";
24
+ function formatBytes(bytes, decimals = 2) {
25
+ if (bytes === 0) return "0 Bytes";
26
+ const k = 1024;
27
+ const dm = decimals < 0 ? 0 : decimals;
28
+ const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
29
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
30
+ return parseFloat((bytes / k ** i).toFixed(dm)) + " " + sizes[i];
31
+ }
32
+ export default async function BackupListPage() {
33
+ const backups = await getBackups();
34
+ return /* @__PURE__ */ React.createElement("div", { className: "space-y-6" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center justify-between" }, /* @__PURE__ */ React.createElement("div", { className: "space-y-1" }, /* @__PURE__ */ React.createElement("h2", { className: "text-3xl font-bold tracking-tight" }, "Database Backups"), /* @__PURE__ */ React.createElement("p", { className: "text-muted-foreground text-sm" }, "Manage your database snapshots. All backups are stored locally on the server.")), /* @__PURE__ */ React.createElement(CreateBackupButton, null)), /* @__PURE__ */ React.createElement(Card, null, /* @__PURE__ */ React.createElement(CardHeader, null, /* @__PURE__ */ React.createElement(CardTitle, null, "Available Backups"), /* @__PURE__ */ React.createElement(CardDescription, null, "A list of all backups created in the system.")), /* @__PURE__ */ React.createElement(CardContent, null, /* @__PURE__ */ React.createElement(Table, null, /* @__PURE__ */ React.createElement(TableHeader, null, /* @__PURE__ */ React.createElement(TableRow, null, /* @__PURE__ */ React.createElement(TableHead, null, "Filename"), /* @__PURE__ */ React.createElement(TableHead, null, "Size"), /* @__PURE__ */ React.createElement(TableHead, null, "Created"), /* @__PURE__ */ React.createElement(TableHead, null, "Status"), /* @__PURE__ */ React.createElement(TableHead, { className: "text-right" }, "Actions"))), /* @__PURE__ */ React.createElement(TableBody, null, backups.length === 0 ? /* @__PURE__ */ React.createElement(TableRow, null, /* @__PURE__ */ React.createElement(
35
+ TableCell,
36
+ {
37
+ colSpan: 5,
38
+ className: "text-center py-10 text-muted-foreground"
39
+ },
40
+ "No backups found. Create your first backup to see it here."
41
+ )) : backups.map((backup) => /* @__PURE__ */ React.createElement(TableRow, { key: backup.id }, /* @__PURE__ */ React.createElement(TableCell, { className: "font-medium" }, backup.filename), /* @__PURE__ */ React.createElement(TableCell, null, formatBytes(backup.size)), /* @__PURE__ */ React.createElement(TableCell, { title: backup.createdAt.toLocaleString() }, formatDistanceToNow(backup.createdAt, {
42
+ addSuffix: true,
43
+ locale: pl
44
+ })), /* @__PURE__ */ React.createElement(TableCell, null, /* @__PURE__ */ React.createElement(
45
+ "span",
46
+ {
47
+ className: `px-2 py-1 rounded-full text-xs font-semibold ${backup.status === "success" ? "bg-green-100 text-green-700" : "bg-red-100 text-red-700"}`
48
+ },
49
+ backup.status
50
+ )), /* @__PURE__ */ React.createElement(TableCell, { className: "text-right space-x-1" }, /* @__PURE__ */ React.createElement(RestoreBackupButton, { id: backup.id }), /* @__PURE__ */ React.createElement(DeleteBackupButton, { id: backup.id })))))))));
51
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "Settings saved successfully": "Settings saved successfully",
3
+ "Failed to save settings": "Failed to save settings",
4
+ "Loading Sample Settings...": "Loading Sample Settings...",
5
+ "Sample Settings": "Sample Settings",
6
+ "Configure your Sample Configuration.": "Configure your Sample Configuration.",
7
+ "Default Var": "Default Var",
8
+ "Save Configuration": "Save Configuration",
9
+ "General": "General",
10
+ "Sample Configuration": "Sample Configuration"
11
+ }
package/manifest.json ADDED
@@ -0,0 +1,11 @@
1
+ {
2
+ "id": "backup-module",
3
+ "name": "Database Backup",
4
+ "version": "0.0.1-beta",
5
+ "description": "Create and manage database backups.",
6
+ "enabled": true,
7
+ "system": true,
8
+ "dependencies": [],
9
+ "extends": [],
10
+ "isNpm": true
11
+ }
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@arch-cadre/backup-module",
3
+ "version": "0.0.1",
4
+ "description": "Backup module for Kryo framework",
5
+ "type": "module",
6
+ "exports": {
7
+ "./package.json": "./package.json",
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.mjs",
11
+ "require": "./dist/index.cjs"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "locales",
17
+ "manifest.json"
18
+ ],
19
+ "scripts": {
20
+ "release": "npm publish --access public --no-git-checks",
21
+ "clean": "rm -rf ./dist",
22
+ "switch:dev": "node scripts/switchToSrc.js",
23
+ "switch:prod": "node scripts/switchToDist.js",
24
+ "dev": "unbuild --stub",
25
+ "build": "unbuild"
26
+ },
27
+ "dependencies": {
28
+ "@arch-cadre/modules": "^0.0.15",
29
+ "@arch-cadre/ui": "^0.0.15",
30
+ "@hookform/resolvers": "^3.10.0",
31
+ "date-fns": "^4.1.0",
32
+ "drizzle-orm": "1.0.0-beta.15-859cf75",
33
+ "lucide-react": "^0.475.0",
34
+ "pg": "^8.18.0",
35
+ "react-hook-form": "^7.54.2",
36
+ "sonner": "^2.0.7",
37
+ "zod": "^3.24.1"
38
+ },
39
+ "devDependencies": {
40
+ "@arch-cadre/core": "^0.0.15",
41
+ "@types/pg": "^8.16.0",
42
+ "@types/react": "^19",
43
+ "next": "16.1.1",
44
+ "react": "^19.0.0",
45
+ "typescript": "^5.3.3",
46
+ "unbuild": "^3.6.1"
47
+ },
48
+ "peerDependencies": {
49
+ "@arch-cadre/core": "^0.0.15",
50
+ "@arch-cadre/intl": "^0.0.15",
51
+ "@arch-cadre/ui": "^0.0.15",
52
+ "next": ">=13.0.0",
53
+ "react": "^19.0.0"
54
+ },
55
+ "main": "./dist/index.mjs"
56
+ }