dbkeeper 1.0.0

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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Faysal Sarker
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,240 @@
1
+ <div align="center">
2
+
3
+ <img src="./assets/DB-Keeper.png" alt="DBKeeper - Automatic Database Backup" width="100%">
4
+
5
+ </div>
6
+
7
+ <div align="center">
8
+
9
+ <pre>
10
+ ██████╗ ██████╗ ██╗ ██╗███████╗███████╗██████╗ ███████╗██████╗
11
+ ██╔══██╗██╔══██╗██║ ██╔╝██╔════╝██╔════╝██╔══██╗██╔════╝██╔══██╗
12
+ ██║ ██║██████╔╝█████╔╝ █████╗ █████╗ ██████╔╝█████╗ ██████╔╝
13
+ ██║ ██║██╔══██╗██╔═██╗ ██╔══╝ ██╔══╝ ██╔═══╝ ██╔══╝ ██╔══██╗
14
+ ██████╔╝██████╔╝██║ ██╗███████╗███████╗██║ ███████╗██║ ██║
15
+ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝╚══════╝╚═╝ ╚══════╝╚═╝ ╚═╝
16
+ </pre>
17
+
18
+ </div>
19
+
20
+ **Reliable, self-hosted database backups for MongoDB and beyond.**
21
+
22
+ [![npm version](https://img.shields.io/npm/v/dbkeeper)](https://www.npmjs.com/package/dbkeeper)
23
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](./LICENSE)
24
+ [![Node.js](https://img.shields.io/node/v/dbkeeper)](https://nodejs.org)
25
+
26
+ ---
27
+
28
+
29
+
30
+
31
+ ## Why this exists
32
+
33
+ Most backup solutions are either cloud-locked SaaS products with recurring fees, or bare shell scripts with no retry logic, no rotation safety, and no notifications. dbkeeper is a self-hosted CLI tool that handles rolling backup retention, safe rotation (never deletes an old backup before a new one is verified uploaded), and email notifications — all in one composable package. No subscription. No vendor lock-in. You own your data pipeline.
34
+
35
+ ---
36
+
37
+ ## Features
38
+
39
+ - **MongoDB backup** via `mongodump` — produces a compressed `.tar.gz` archive
40
+ - **Google Drive storage** — uploads via service account credentials, no OAuth flow required
41
+ - **Rolling retention** — keep the N most recent backups; older ones are deleted automatically
42
+ - **Safe rotation** — old backups are only deleted *after* the new upload is confirmed with a remote ID
43
+ - **Email notifications** — configurable success and/or failure alerts via any SMTP server
44
+ - **TypeScript-first** — full type declarations included; works equally well from plain JavaScript
45
+
46
+ ---
47
+
48
+ ## Installation
49
+
50
+ ```bash
51
+ # Global (recommended for CLI use)
52
+ npm install -g dbkeeper
53
+
54
+ # Local dev dependency (for programmatic use or CI)
55
+ npm install --save-dev dbkeeper
56
+ ```
57
+
58
+ ---
59
+
60
+ ## Quick Start
61
+
62
+ **1. Create a `backup.config.js` in your project root:**
63
+
64
+ ```js
65
+ // backup.config.js
66
+ require('dotenv').config();
67
+
68
+ module.exports = {
69
+ database: {
70
+ type: 'mongo',
71
+ uri: process.env.MONGO_URI,
72
+ },
73
+ storage: {
74
+ type: 'gdrive',
75
+ credentialsPath: process.env.GDRIVE_CREDENTIALS_PATH ?? './gdrive-credentials.json',
76
+ folderId: process.env.GDRIVE_FOLDER_ID,
77
+ },
78
+ retention: {
79
+ keep: 24, // keep the 24 most recent backups
80
+ },
81
+ notifications: {
82
+ success: false,
83
+ failure: true,
84
+ to: process.env.NOTIFY_EMAIL,
85
+ smtp: {
86
+ host: process.env.SMTP_HOST,
87
+ port: 587,
88
+ auth: {
89
+ user: process.env.SMTP_USER,
90
+ pass: process.env.SMTP_PASS,
91
+ },
92
+ },
93
+ },
94
+ };
95
+ ```
96
+
97
+ **2. Run a backup:**
98
+
99
+ ```bash
100
+ dbkeeper run
101
+ # or with a custom config path:
102
+ dbkeeper run --config ./config/backup.config.js
103
+ ```
104
+
105
+ **3. Check the last result:**
106
+
107
+ ```bash
108
+ dbkeeper status
109
+ ```
110
+
111
+ ---
112
+
113
+ ## Configuration Reference
114
+
115
+ All configuration is provided via a `backup.config.js` (or any `.js` file) that exports a plain object.
116
+
117
+ ### `database`
118
+
119
+ | Field | Type | Required | Description |
120
+ |--------|----------|----------|--------------------------------------------------|
121
+ | `type` | `string` | ✅ | Database type. Currently only `"mongo"`. |
122
+ | `uri` | `string` | ✅ | MongoDB connection URI (supports Atlas SRV URIs). |
123
+
124
+ ### `storage`
125
+
126
+ | Field | Type | Required | Description |
127
+ |---------------------|----------|----------|-------------------------------------------------------------------|
128
+ | `type` | `string` | ✅ | Storage backend. Currently only `"gdrive"`. |
129
+ | `credentialsPath` | `string` | ✅ | Path to the Google Drive service account JSON key file. |
130
+ | `folderId` | `string` | ✅ | ID of the Drive folder where backups will be stored. |
131
+
132
+ ### `retention`
133
+
134
+ | Field | Type | Required | Description |
135
+ |--------|--------------------|----------|-------------------------------------------------------------------------------|
136
+ | `keep` | `number \| "unlimited"` | ✅ | Number of recent backups to keep. Use `"unlimited"` to disable deletion. |
137
+
138
+ ### `notifications`
139
+
140
+ | Field | Type | Required | Description |
141
+ |-----------|-----------|----------|-----------------------------------------------------------------|
142
+ | `success` | `boolean` | ✅ | Send an email when a backup succeeds. |
143
+ | `failure` | `boolean` | ✅ | Send an email when a backup fails. |
144
+ | `to` | `string` | ✅ | Recipient email address. |
145
+ | `smtp` | `object` | ✴️ * | SMTP connection config (required if `success` or `failure` is `true`). |
146
+
147
+ **`smtp` sub-fields:**
148
+
149
+ | Field | Type | Default | Description |
150
+ |----------|-----------|---------|---------------------------------------|
151
+ | `host` | `string` | — | SMTP server hostname. |
152
+ | `port` | `number` | `587` | SMTP port. |
153
+ | `secure` | `boolean` | `false` | Use TLS (`true` for port 465). |
154
+ | `auth` | `object` | — | `{ user: string, pass: string }` |
155
+
156
+ ---
157
+
158
+ ## Google Drive Setup
159
+
160
+ dbkeeper authenticates with Google Drive via a **service account** — there's no OAuth browser flow.
161
+
162
+ 1. **Enable the Drive API** — Go to [Google Cloud Console](https://console.cloud.google.com) → APIs & Services → Enable APIs → search for "Google Drive API" and enable it.
163
+
164
+ 2. **Create a service account** — Go to IAM & Admin → Service Accounts → Create Service Account. Give it a name (e.g. `dbkeeper-backup`), skip the optional role assignment.
165
+
166
+ 3. **Download the JSON key** — Inside your new service account, go to Keys → Add Key → Create new key → JSON. Save the file somewhere secure (e.g. `./gdrive-credentials.json`). **Do not commit this file to version control.**
167
+
168
+ 4. **Share the target folder** — In Google Drive, right-click your backup folder → Share → paste in the service account's email address (e.g. `dbkeeper-backup@your-project.iam.gserviceaccount.com`) and give it **Editor** access.
169
+
170
+ 5. **Get the folder ID** — Open the folder in Drive. The URL will look like `https://drive.google.com/drive/folders/1AbCdEfGhIjKlMnOpQrStUvWxYz`. The folder ID is the last path segment.
171
+
172
+ ---
173
+
174
+ ## How Retention Works
175
+
176
+ After each successful upload, dbkeeper lists all backup files in your Drive folder and applies retention. Given `keep: N`:
177
+
178
+ - It sorts all backups by creation date (oldest first).
179
+ - It keeps the N most recent files.
180
+ - It deletes everything older.
181
+ - **Deletion only happens after the new upload has been verified** — dbkeeper confirms the remote file ID is non-empty before touching any existing backups.
182
+
183
+ **Example** with `keep: 3`, running daily:
184
+
185
+ ```
186
+ Before run: backup-day1.tar.gz backup-day2.tar.gz backup-day3.tar.gz
187
+ After run: backup-day2.tar.gz backup-day3.tar.gz backup-day4.tar.gz
188
+ (day1 deleted, day4 uploaded — always 3 in storage)
189
+ ```
190
+
191
+ Set `keep: "unlimited"` to accumulate all backups indefinitely.
192
+
193
+ ---
194
+
195
+ ## Programmatic API
196
+
197
+ dbkeeper also exports its internals for use in custom scripts or other tooling:
198
+
199
+ ```ts
200
+ import { runBackup, mongoAdapter, gdriveAdapter, loadConfig } from 'dbkeeper';
201
+
202
+ const config = loadConfig('./backup.config.js');
203
+ const result = await runBackup(config, mongoAdapter, gdriveAdapter);
204
+
205
+ if (result.ok) {
206
+ console.log(`Backed up to Drive: ${result.remoteId}`);
207
+ }
208
+ ```
209
+
210
+ You can also bring your own adapters by implementing the `DatabaseAdapter` or `StorageAdapter` interfaces:
211
+
212
+ ```ts
213
+ import { DatabaseAdapter, StorageAdapter } from 'dbkeeper';
214
+ ```
215
+
216
+ ---
217
+
218
+ ## Roadmap
219
+
220
+ The following are **not yet implemented**. This list is here so you know exactly what dbkeeper does and doesn't do today:
221
+
222
+ - [ ] PostgreSQL support
223
+ - [ ] MySQL / MariaDB support
224
+ - [ ] Amazon S3 storage backend
225
+ - [ ] Encryption at rest (GPG or AES)
226
+ - [ ] Restore verification (test-restore and checksum validation)
227
+ - [ ] Web dashboard / status UI
228
+ - [ ] Cron scheduling built into the CLI (currently relies on system cron or a process manager)
229
+
230
+ ---
231
+
232
+ ## Contributing
233
+
234
+ dbkeeper is open source and welcomes contributions. If you hit a bug, have a feature request, or want to add a new database or storage adapter, open an issue or a pull request on [GitHub](https://github.com/<your-github-username>/dbkeeper). The codebase is intentionally small and straightforward — adapters are just two-method interfaces, so adding support for a new backend is a well-defined, self-contained task.
235
+
236
+ ---
237
+
238
+ ## License
239
+
240
+ MIT © Faysal Sarker — see [LICENSE](./LICENSE)
package/dist/cli.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * dbkeeper CLI entry point.
4
+ *
5
+ * Thin wrapper — all real logic lives in src/core/runBackup.ts.
6
+ * Supported commands (v1):
7
+ * dbkeeper run Execute one backup immediately
8
+ * dbkeeper status Show the last backup result from the status file
9
+ */
10
+ import 'dotenv/config';
package/dist/cli.js ADDED
@@ -0,0 +1,166 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ /**
4
+ * dbkeeper CLI entry point.
5
+ *
6
+ * Thin wrapper — all real logic lives in src/core/runBackup.ts.
7
+ * Supported commands (v1):
8
+ * dbkeeper run Execute one backup immediately
9
+ * dbkeeper status Show the last backup result from the status file
10
+ */
11
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
12
+ if (k2 === undefined) k2 = k;
13
+ var desc = Object.getOwnPropertyDescriptor(m, k);
14
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
15
+ desc = { enumerable: true, get: function() { return m[k]; } };
16
+ }
17
+ Object.defineProperty(o, k2, desc);
18
+ }) : (function(o, m, k, k2) {
19
+ if (k2 === undefined) k2 = k;
20
+ o[k2] = m[k];
21
+ }));
22
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
23
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
24
+ }) : function(o, v) {
25
+ o["default"] = v;
26
+ });
27
+ var __importStar = (this && this.__importStar) || (function () {
28
+ var ownKeys = function(o) {
29
+ ownKeys = Object.getOwnPropertyNames || function (o) {
30
+ var ar = [];
31
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
32
+ return ar;
33
+ };
34
+ return ownKeys(o);
35
+ };
36
+ return function (mod) {
37
+ if (mod && mod.__esModule) return mod;
38
+ var result = {};
39
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
40
+ __setModuleDefault(result, mod);
41
+ return result;
42
+ };
43
+ })();
44
+ Object.defineProperty(exports, "__esModule", { value: true });
45
+ const path = __importStar(require("path"));
46
+ const fs = __importStar(require("fs"));
47
+ require("dotenv/config");
48
+ const loadConfig_1 = require("./config/loadConfig");
49
+ const runBackup_1 = require("./core/runBackup");
50
+ const mongo_1 = require("./database/mongo");
51
+ const gdrive_1 = require("./storage/gdrive");
52
+ const logger_1 = require("./core/logger");
53
+ // ---------------------------------------------------------------------------
54
+ // Status file — persisted to cwd so it survives process restarts
55
+ // ---------------------------------------------------------------------------
56
+ const STATUS_FILE = path.resolve(process.cwd(), '.dbkeeper-status.json');
57
+ function saveStatus(result) {
58
+ try {
59
+ fs.writeFileSync(STATUS_FILE, JSON.stringify(result, null, 2), 'utf8');
60
+ }
61
+ catch {
62
+ // Non-fatal
63
+ }
64
+ }
65
+ function readStatus() {
66
+ try {
67
+ if (!fs.existsSync(STATUS_FILE))
68
+ return null;
69
+ return JSON.parse(fs.readFileSync(STATUS_FILE, 'utf8'));
70
+ }
71
+ catch {
72
+ return null;
73
+ }
74
+ }
75
+ // ---------------------------------------------------------------------------
76
+ // Command: run
77
+ // ---------------------------------------------------------------------------
78
+ async function cmdRun(configPath) {
79
+ const config = (0, loadConfig_1.loadConfig)(configPath);
80
+ // Select adapters based on config types
81
+ // (v1 only supports mongo + gdrive — add more cases here for future adapters)
82
+ if (config.database.type !== 'mongo') {
83
+ logger_1.logger.error(`Unsupported database type: "${config.database.type}". Only "mongo" is supported in v1.`);
84
+ process.exit(1);
85
+ }
86
+ if (config.storage.type !== 'gdrive') {
87
+ logger_1.logger.error(`Unsupported storage type: "${config.storage.type}". Only "gdrive" is supported in v1.`);
88
+ process.exit(1);
89
+ }
90
+ const result = await (0, runBackup_1.runBackup)(config, mongo_1.mongoAdapter, gdrive_1.gdriveAdapter);
91
+ saveStatus(result);
92
+ if (!result.ok) {
93
+ process.exit(1);
94
+ }
95
+ }
96
+ // ---------------------------------------------------------------------------
97
+ // Command: status
98
+ // ---------------------------------------------------------------------------
99
+ function cmdStatus() {
100
+ const status = readStatus();
101
+ if (!status) {
102
+ console.log('No backup has been run yet. Execute `dbkeeper run` first.');
103
+ return;
104
+ }
105
+ if (status.ok) {
106
+ console.log(`Last backup: SUCCESS`);
107
+ console.log(` Timestamp: ${new Date(status.timestamp).toISOString()}`);
108
+ console.log(` File: ${path.basename(status.filePath)}`);
109
+ console.log(` Size: ${formatBytes(status.sizeBytes)}`);
110
+ console.log(` Remote ID: ${status.remoteId}`);
111
+ console.log(` Retained: ${status.remainingCount} backup(s) in storage`);
112
+ console.log(` Deleted: ${status.deletedCount} old backup(s)`);
113
+ }
114
+ else {
115
+ console.log(`Last backup: FAILED`);
116
+ console.log(` Timestamp: ${new Date(status.timestamp).toISOString()}`);
117
+ console.log(` Error: ${status.error?.message ?? 'unknown'}`);
118
+ }
119
+ }
120
+ // ---------------------------------------------------------------------------
121
+ // CLI routing
122
+ // ---------------------------------------------------------------------------
123
+ function printHelp() {
124
+ console.log(`
125
+ dbkeeper — MongoDB backup tool
126
+
127
+ Usage:
128
+ dbkeeper run [--config <path>] Run a backup immediately
129
+ dbkeeper status Show the last backup result
130
+
131
+ Options:
132
+ --config <path> Path to backup.config.js (default: ./backup.config.js)
133
+ `);
134
+ }
135
+ async function main() {
136
+ const args = process.argv.slice(2);
137
+ const command = args[0];
138
+ if (!command || command === '--help' || command === '-h') {
139
+ printHelp();
140
+ return;
141
+ }
142
+ const configFlagIndex = args.indexOf('--config');
143
+ const configPath = configFlagIndex !== -1 ? args[configFlagIndex + 1] : undefined;
144
+ switch (command) {
145
+ case 'run':
146
+ await cmdRun(configPath);
147
+ break;
148
+ case 'status':
149
+ cmdStatus();
150
+ break;
151
+ default:
152
+ console.error(`Unknown command: "${command}". Run \`dbkeeper --help\` for usage.`);
153
+ process.exit(1);
154
+ }
155
+ }
156
+ function formatBytes(bytes) {
157
+ if (bytes < 1024)
158
+ return `${bytes} B`;
159
+ if (bytes < 1024 * 1024)
160
+ return `${(bytes / 1024).toFixed(1)} KB`;
161
+ return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
162
+ }
163
+ main().catch((err) => {
164
+ logger_1.logger.error(`Unhandled error: ${err.message}`);
165
+ process.exit(1);
166
+ });
@@ -0,0 +1,6 @@
1
+ import { BackupConfig } from '../types';
2
+ /**
3
+ * Loads `backup.config.js` from the consuming project's root (cwd).
4
+ * Throws a clear error if the file is missing or invalid.
5
+ */
6
+ export declare function loadConfig(configPath?: string): BackupConfig;
@@ -0,0 +1,74 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.loadConfig = loadConfig;
37
+ const path = __importStar(require("path"));
38
+ const fs = __importStar(require("fs"));
39
+ /**
40
+ * Loads `backup.config.js` from the consuming project's root (cwd).
41
+ * Throws a clear error if the file is missing or invalid.
42
+ */
43
+ function loadConfig(configPath) {
44
+ const resolvedPath = configPath
45
+ ? path.resolve(configPath)
46
+ : path.resolve(process.cwd(), 'backup.config.js');
47
+ if (!fs.existsSync(resolvedPath)) {
48
+ throw new Error(`dbkeeper: config file not found at "${resolvedPath}".\n` +
49
+ `Create a backup.config.js in your project root, or pass --config <path>.`);
50
+ }
51
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
52
+ const raw = require(resolvedPath);
53
+ if (!raw || typeof raw !== 'object') {
54
+ throw new Error(`dbkeeper: "${resolvedPath}" must export a plain object. Got: ${typeof raw}`);
55
+ }
56
+ const config = raw;
57
+ // Basic structural validation — keeps error messages actionable
58
+ if (!config.database?.type) {
59
+ throw new Error('dbkeeper: config.database.type is required (e.g. "mongo")');
60
+ }
61
+ if (!config.database?.uri) {
62
+ throw new Error('dbkeeper: config.database.uri is required');
63
+ }
64
+ if (!config.storage?.type) {
65
+ throw new Error('dbkeeper: config.storage.type is required (e.g. "gdrive")');
66
+ }
67
+ if (config.retention?.keep === undefined) {
68
+ throw new Error('dbkeeper: config.retention.keep is required (use a number or "unlimited")');
69
+ }
70
+ if (!config.notifications?.to) {
71
+ throw new Error('dbkeeper: config.notifications.to (recipient email) is required');
72
+ }
73
+ return config;
74
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Lightweight structured logger.
3
+ *
4
+ * Prefixes every line with a timestamp and log level.
5
+ * Replace this with a structured logger (pino, winston, etc.) if needed
6
+ * without touching any other module — all internal code imports from here.
7
+ */
8
+ export type LogLevel = 'info' | 'warn' | 'error' | 'debug';
9
+ export declare const logger: {
10
+ info: (msg: string) => void;
11
+ warn: (msg: string) => void;
12
+ error: (msg: string) => void;
13
+ debug: (msg: string) => void;
14
+ };
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ /**
3
+ * Lightweight structured logger.
4
+ *
5
+ * Prefixes every line with a timestamp and log level.
6
+ * Replace this with a structured logger (pino, winston, etc.) if needed
7
+ * without touching any other module — all internal code imports from here.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.logger = void 0;
11
+ function stamp() {
12
+ return new Date().toISOString();
13
+ }
14
+ function write(level, message) {
15
+ const line = `[${stamp()}] [${level.toUpperCase()}] ${message}`;
16
+ if (level === 'error') {
17
+ process.stderr.write(line + '\n');
18
+ }
19
+ else {
20
+ process.stdout.write(line + '\n');
21
+ }
22
+ }
23
+ exports.logger = {
24
+ info: (msg) => write('info', msg),
25
+ warn: (msg) => write('warn', msg),
26
+ error: (msg) => write('error', msg),
27
+ debug: (msg) => write('debug', msg),
28
+ };
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Pure rolling-retention logic.
3
+ *
4
+ * No I/O, no network calls — fully unit-testable.
5
+ * The caller is responsible for actually deleting the returned IDs.
6
+ */
7
+ export interface RemoteFile {
8
+ remoteId: string;
9
+ createdAt: Date;
10
+ }
11
+ export interface RetentionResult {
12
+ /** Files that should be deleted to satisfy the keep limit. */
13
+ toDelete: RemoteFile[];
14
+ /** How many files will remain after deletion. */
15
+ remainingCount: number;
16
+ }
17
+ /**
18
+ * Given the current list of remote backup files (oldest-first) and the keep
19
+ * limit, return which files need to be deleted and how many will remain.
20
+ *
21
+ * @param existing All existing backups, **including** the one just uploaded.
22
+ * @param keep Maximum number of backups to retain, or "unlimited".
23
+ */
24
+ export declare function applyRetention(existing: RemoteFile[], keep: number | 'unlimited'): RetentionResult;
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ /**
3
+ * Pure rolling-retention logic.
4
+ *
5
+ * No I/O, no network calls — fully unit-testable.
6
+ * The caller is responsible for actually deleting the returned IDs.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.applyRetention = applyRetention;
10
+ /**
11
+ * Given the current list of remote backup files (oldest-first) and the keep
12
+ * limit, return which files need to be deleted and how many will remain.
13
+ *
14
+ * @param existing All existing backups, **including** the one just uploaded.
15
+ * @param keep Maximum number of backups to retain, or "unlimited".
16
+ */
17
+ function applyRetention(existing, keep) {
18
+ if (keep === 'unlimited') {
19
+ return { toDelete: [], remainingCount: existing.length };
20
+ }
21
+ if (typeof keep !== 'number' || keep < 1) {
22
+ throw new Error(`retention.keep must be a positive integer or "unlimited". Got: ${keep}`);
23
+ }
24
+ if (existing.length <= keep) {
25
+ return { toDelete: [], remainingCount: existing.length };
26
+ }
27
+ // Sort by date ascending so the oldest entries are at the front
28
+ const sorted = [...existing].sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
29
+ const deleteCount = sorted.length - keep;
30
+ const toDelete = sorted.slice(0, deleteCount);
31
+ const remainingCount = sorted.length - deleteCount;
32
+ return { toDelete, remainingCount };
33
+ }
@@ -0,0 +1,20 @@
1
+ import { BackupConfig, DatabaseAdapter, StorageAdapter, BackupResult } from '../types';
2
+ /**
3
+ * Core backup orchestrator.
4
+ *
5
+ * Accepts adapters by interface — never imports mongo.ts or gdrive.ts directly.
6
+ * This is the key extension point: swap in any DatabaseAdapter / StorageAdapter
7
+ * to support new databases or storage backends without changing this file.
8
+ *
9
+ * Safe rotation order (hard requirement):
10
+ * 1. dump → create local backup file
11
+ * 2. upload → push to remote storage
12
+ * 3. verify → confirm the remote file ID exists
13
+ * 4. retain → delete old backups (ONLY after upload is confirmed)
14
+ * 5. notify → send success / failure email
15
+ *
16
+ * If any step before retention fails, we stop immediately:
17
+ * - Nothing is deleted from remote storage
18
+ * - A failure email is sent (if configured)
19
+ */
20
+ export declare function runBackup(config: BackupConfig, dbAdapter: DatabaseAdapter, storageAdapter: StorageAdapter): Promise<BackupResult>;