@dockstat/sqlite-wrapper 1.3.2 → 1.3.4

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/README.md CHANGED
@@ -12,22 +12,26 @@ Schema-first table helpers, an expressive chainable QueryBuilder, safe defaults
12
12
  ## 🆕 What's New in v1.3
13
13
 
14
14
  ### Bug Fixes
15
+
15
16
  - **Fixed Boolean parsing** — Boolean columns now correctly convert SQLite's `0`/`1` to JavaScript `true`/`false`
16
17
  - **Fixed Wrong packing** — Before the `publish` script was added, workspace dependencies were not correctly propagated
17
18
 
18
19
  ### New Features
20
+
19
21
  - **Auto-detection of JSON & Boolean columns** — No more manual parser configuration! Columns using `column.json()` or `column.boolean()` are automatically detected from schema
20
22
  - **Automatic backups with retention** — Configure `autoBackup` to create periodic backups with automatic cleanup of old files
21
23
  - **Backup & Restore API** — New `backup()`, `restore()`, and `listBackups()` methods
22
24
  - **`getPath()` method** — Get the database file path
23
25
 
24
26
  ### Architecture Improvements
27
+
25
28
  - **New `utils/` module** — Reusable utilities for SQL building, logging, and row transformation
26
29
  - **Structured logging** — Cleaner, more consistent log output with dedicated loggers per component
27
30
  - **Reduced code duplication** — Extracted common patterns into shared utilities
28
31
  - **Better maintainability** — Clearer separation of concerns across modules
29
32
 
30
33
  ### Breaking Changes
34
+
31
35
  - None! v1.3 is fully backward compatible with v1.2.x
32
36
 
33
37
  ---
@@ -284,7 +288,10 @@ const activeAdmins = userTable
284
288
  .all();
285
289
 
286
290
  // Get first match
287
- const user = userTable.select(["*"]).where({ email: "alice@example.com" }).first();
291
+ const user = userTable
292
+ .select(["*"])
293
+ .where({ email: "alice@example.com" })
294
+ .first();
288
295
 
289
296
  // Count records
290
297
  const count = userTable.where({ active: true }).count();
@@ -321,7 +328,10 @@ userTable.insertOrIgnore({ email: "existing@example.com", name: "Name" });
321
328
  userTable.insertOrReplace({ email: "existing@example.com", name: "New Name" });
322
329
 
323
330
  // Insert and get the row back
324
- const newUser = userTable.insertAndGet({ name: "Charlie", email: "charlie@example.com" });
331
+ const newUser = userTable.insertAndGet({
332
+ name: "Charlie",
333
+ email: "charlie@example.com",
334
+ });
325
335
  ```
326
336
 
327
337
  ### UPDATE Operations
package/package.json CHANGED
@@ -1,19 +1,17 @@
1
1
  {
2
2
  "name": "@dockstat/sqlite-wrapper",
3
- "version": "1.3.2",
3
+ "version": "1.3.4",
4
4
  "description": "A TypeScript wrapper around bun:sqlite with type-safe query building",
5
5
  "type": "module",
6
- "main": "./index.ts",
7
- "types": "./index.ts",
6
+ "main": "./src/index.ts",
7
+ "types": "./src/index.ts",
8
8
  "exports": {
9
- ".": "./index.ts",
10
- "./types": "./types.ts",
11
- "./utils": "./utils/index.ts"
9
+ ".": "./src/index.ts",
10
+ "./types": "./src/types.ts",
11
+ "./utils": "./src/utils/index.ts"
12
12
  },
13
13
  "files": [
14
- "index.ts",
15
- "query-builder/**/*",
16
- "utils/**/*",
14
+ "src",
17
15
  "README.md",
18
16
  "types.ts"
19
17
  ],
package/src/index.ts ADDED
@@ -0,0 +1,590 @@
1
+ import { Database, type SQLQueryBindings } from "bun:sqlite"
2
+ import { Logger } from "@dockstat/logger"
3
+ import type { ColumnDefinition, IndexColumn, IndexMethod, Parser, TableOptions } from "./types"
4
+ import { backup as helperBackup } from "./lib/backup/backup"
5
+ import { setupAutoBackup as helperSetupAutoBackup } from "./lib/backup/setupAutoBackup"
6
+
7
+ // helpers
8
+ import { createIndex as helperCreateIndex } from "./lib/index/createIndex"
9
+ import { dropIndex as helperDropIndex } from "./lib/index/dropIndex"
10
+ import { buildColumnSQL } from "./lib/table/buildColumnSQL"
11
+ import { buildTableConstraints } from "./lib/table/buildTableConstraint"
12
+ import { getTableComment as helperGetTableComment } from "./lib/table/getTableComment"
13
+ import { isTableSchema } from "./lib/table/isTableSchema"
14
+ import { setTableComment as helperSetTableComment } from "./lib/table/setTableComment"
15
+ import { QueryBuilder } from "./query-builder/index"
16
+ import { createLogger, type SqliteLogger } from "./utils"
17
+ import { listBackups as helperListBackups } from "./lib/backup/listBackups"
18
+ import { restore as helperRestore } from "./lib/backup/restore"
19
+
20
+ // Re-export all types and utilities
21
+ export { QueryBuilder }
22
+ export type {
23
+ ArrayKey,
24
+ ColumnConstraints,
25
+ ColumnDefinition,
26
+ ColumnNames,
27
+ DefaultExpression,
28
+ DeleteResult,
29
+ ForeignKeyAction,
30
+ InsertOptions,
31
+ InsertResult,
32
+ RegexCondition,
33
+ SQLiteType,
34
+ TableConstraints,
35
+ TableOptions,
36
+ TableSchema,
37
+ UpdateResult,
38
+ WhereCondition,
39
+ } from "./types"
40
+
41
+ // Re-export helper utilities
42
+ export {
43
+ column,
44
+ defaultExpr,
45
+ SQLiteFunctions,
46
+ SQLiteKeywords,
47
+ SQLiteTypes,
48
+ sql,
49
+ } from "./types"
50
+
51
+ /**
52
+ * Auto-backup configuration options
53
+ */
54
+ export interface AutoBackupOptions {
55
+ /** Enable automatic backups */
56
+ enabled: boolean
57
+ /** Directory to store backup files */
58
+ directory: string
59
+ /** Backup interval in milliseconds (default: 1 hour) */
60
+ intervalMs?: number
61
+ /** Maximum number of backups to retain (default: 10) */
62
+ maxBackups?: number
63
+ /** Prefix for backup filenames (default: 'backup') */
64
+ filenamePrefix?: string
65
+ /** Whether to compress backups using gzip (default: false) */
66
+ compress?: boolean
67
+ }
68
+
69
+ /**
70
+ * Database configuration options
71
+ */
72
+ export interface DBOptions {
73
+ /** PRAGMA settings to apply on database open */
74
+ pragmas?: Array<[string, SQLQueryBindings]>
75
+ /** Paths to SQLite extensions to load */
76
+ loadExtensions?: string[]
77
+ /** Auto-backup configuration */
78
+ autoBackup?: AutoBackupOptions
79
+ }
80
+
81
+ class DB {
82
+ protected db: Database
83
+ protected dbPath: string
84
+ private autoBackupTimer: ReturnType<typeof setInterval> | null = null
85
+ private autoBackupOptions: AutoBackupOptions | null = null
86
+ private baseLogger: Logger
87
+ private dbLog: SqliteLogger
88
+ private backupLog: SqliteLogger
89
+ private tableLog: SqliteLogger
90
+
91
+ /**
92
+ * Open or create a SQLite database at `path`.
93
+ *
94
+ * @param path - Path to the SQLite file (e.g. "app.db"). Use ":memory:" for in-memory DB.
95
+ * @param options - Optional database configuration
96
+ */
97
+ constructor(path: string, options?: DBOptions, baseLogger?: Logger) {
98
+ if (!baseLogger) {
99
+ this.baseLogger = new Logger("Sqlite-Wrapper")
100
+ } else {
101
+ this.baseLogger = baseLogger
102
+ }
103
+
104
+ // Wire base logger so sqlite-wrapper logs inherit the same LogHook/parents as the consumer.
105
+ this.dbLog = createLogger("DB", this.baseLogger)
106
+ this.backupLog = createLogger("Backup", this.baseLogger)
107
+ this.tableLog = createLogger("Table", this.baseLogger)
108
+
109
+ this.dbLog.connection(path, "open")
110
+
111
+ this.dbPath = path
112
+ this.db = new Database(path)
113
+
114
+ // Apply PRAGMA settings if provided
115
+ if (options?.pragmas) {
116
+ for (const [name, value] of options.pragmas) {
117
+ this.pragma(name, value)
118
+ }
119
+ }
120
+
121
+ // Load extensions if provided
122
+ if (options?.loadExtensions) {
123
+ for (const extensionPath of options.loadExtensions) {
124
+ this.loadExtension(extensionPath)
125
+ }
126
+ }
127
+
128
+ // Setup auto-backup if configured
129
+ if (options?.autoBackup?.enabled) {
130
+ const res = helperSetupAutoBackup(this.dbPath, this.db, this.backupLog, options.autoBackup)
131
+ this.autoBackupTimer = res.timer
132
+ this.autoBackupOptions = res.autoBackupOptions
133
+ }
134
+ }
135
+
136
+ /**
137
+ * List all available backups
138
+ *
139
+ * @returns Array of backup file information
140
+ */
141
+ listBackups(): Array<{ filename: string; path: string; size: number; created: Date }> {
142
+ return helperListBackups(this.autoBackupOptions, this.backupLog)
143
+ }
144
+
145
+ /**
146
+ * Create a backup of the database
147
+ *
148
+ * @param customPath - Optional custom path for the backup file. If not provided, uses auto-backup settings or generates a timestamped filename.
149
+ * @returns The path to the created backup file
150
+ */
151
+ backup(customPath?: string): string {
152
+ return helperBackup(
153
+ this.dbPath,
154
+ this.db,
155
+ this.backupLog,
156
+ this.autoBackupOptions ?? undefined,
157
+ customPath
158
+ )
159
+ }
160
+
161
+ /**
162
+ * Stop auto-backup if it's running
163
+ */
164
+ stopAutoBackup(): void {
165
+ if (this.autoBackupTimer) {
166
+ clearInterval(this.autoBackupTimer)
167
+ this.autoBackupTimer = null
168
+ this.backupLog.info("Auto-backup stopped")
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Get the database file path
174
+ */
175
+ getPath(): string {
176
+ return this.dbPath
177
+ }
178
+
179
+ /**
180
+ * Get a typed QueryBuilder for a given table name.
181
+ */
182
+ table<T extends Record<string, unknown>>(
183
+ tableName: string,
184
+ parser: Partial<Parser<T>> = {}
185
+ ): QueryBuilder<T> {
186
+ const pObj: Parser<T> = {
187
+ JSON: parser.JSON || [],
188
+ MODULE: parser.MODULE || {},
189
+ BOOLEAN: parser.BOOLEAN || [],
190
+ }
191
+
192
+ this.tableLog.debug(`Creating QueryBuilder for: ${tableName}`)
193
+ return new QueryBuilder<T>(this.db, tableName, pObj, this.baseLogger)
194
+ }
195
+
196
+ /**
197
+ * Close the underlying SQLite database handle.
198
+ * Also stops auto-backup if it's running.
199
+ */
200
+ close(): void {
201
+ this.dbLog.connection(this.dbPath, "close")
202
+ this.stopAutoBackup()
203
+ this.db.close()
204
+ }
205
+
206
+ restore(backupPath: string, targetPath?: string): void {
207
+ const reopened = helperRestore(this.dbPath, this.db, this.backupLog, backupPath, targetPath)
208
+ if (reopened instanceof Database) {
209
+ this.db = reopened
210
+ }
211
+ }
212
+
213
+ /**
214
+ * Create a table with comprehensive type safety and feature support.
215
+ *
216
+ * @param tableName - Table name to create.
217
+ * @param columns - Column definitions (string, legacy object, or type-safe schema).
218
+ * @param options - Table options including constraints and metadata.
219
+ *
220
+ * @throws {Error} If column definitions are invalid or constraints conflict.
221
+ */
222
+ createTable<_T extends Record<string, unknown> = Record<string, unknown>>(
223
+ tableName: string,
224
+ columns: Record<keyof _T, ColumnDefinition>,
225
+ options?: TableOptions<_T>
226
+ ): QueryBuilder<_T> {
227
+ const temp = options?.temporary ? "TEMPORARY " : tableName === ":memory" ? "TEMPORARY " : ""
228
+ const ifNot = options?.ifNotExists ? "IF NOT EXISTS " : ""
229
+ const withoutRowId = options?.withoutRowId ? " WITHOUT ROWID" : ""
230
+
231
+ const quoteIdent = (s: string) => `"${s.replace(/"/g, '""')}"`
232
+
233
+ let columnDefs: string
234
+ let tableConstraints: string[] = []
235
+
236
+ if (isTableSchema(columns)) {
237
+ // comprehensive type-safe approach
238
+ const parts: string[] = []
239
+ for (const [colName, colDef] of Object.entries(columns)) {
240
+ if (!colName) continue
241
+
242
+ const sqlDef = buildColumnSQL(colName, colDef)
243
+ parts.push(`${quoteIdent(colName)} ${sqlDef}`)
244
+ }
245
+
246
+ if (parts.length === 0) {
247
+ throw new Error("No columns provided")
248
+ }
249
+
250
+ columnDefs = parts.join(", ")
251
+
252
+ // Add table-level constraints
253
+ if (options?.constraints) {
254
+ tableConstraints = buildTableConstraints(options.constraints)
255
+ }
256
+ } else {
257
+ // Original object-based approach
258
+ const parts: string[] = []
259
+ for (const [col, def] of Object.entries(columns)) {
260
+ if (!col) continue
261
+
262
+ const defTrim = def || ""
263
+ if (!defTrim) {
264
+ throw new Error(`Missing SQL type/constraints for column "${col}"`)
265
+ }
266
+ parts.push(`${quoteIdent(col)} ${defTrim}`)
267
+ }
268
+
269
+ if (parts.length === 0) {
270
+ throw new Error("No columns provided")
271
+ }
272
+ columnDefs = parts.join(", ")
273
+ }
274
+
275
+ const allDefinitions = [columnDefs, ...tableConstraints].join(", ")
276
+
277
+ const columnNames = Object.keys(columns)
278
+ this.tableLog.tableCreate(tableName, columnNames)
279
+
280
+ const sql = `CREATE ${temp}TABLE ${ifNot}${quoteIdent(
281
+ tableName
282
+ )} (${allDefinitions})${withoutRowId};`
283
+
284
+ this.db.run(sql)
285
+
286
+ // Store table comment as metadata if provided
287
+ if (options?.comment) {
288
+ helperSetTableComment(this.db, this.tableLog, tableName, options.comment)
289
+ }
290
+
291
+ // Auto-detect JSON and BOOLEAN columns from schema
292
+ const autoDetectedJson: Array<keyof _T> = []
293
+ const autoDetectedBoolean: Array<keyof _T> = []
294
+
295
+ if (isTableSchema(columns)) {
296
+ for (const [colName, colDef] of Object.entries(columns)) {
297
+ if (colDef.type === "JSON") {
298
+ autoDetectedJson.push(colName as keyof _T)
299
+ }
300
+ if (colDef.type === "BOOLEAN") {
301
+ autoDetectedBoolean.push(colName as keyof _T)
302
+ }
303
+ }
304
+ }
305
+
306
+ // Merge auto-detected columns with user-provided parser options
307
+ const userJson = options?.parser?.JSON || []
308
+ const userBoolean = options?.parser?.BOOLEAN || []
309
+ const userModule = options?.parser?.MODULE || {}
310
+
311
+ // Combine and deduplicate
312
+ const mergedJson = [...new Set([...autoDetectedJson, ...userJson])] as Array<keyof _T>
313
+ const mergedBoolean = [...new Set([...autoDetectedBoolean, ...userBoolean])] as Array<keyof _T>
314
+
315
+ const pObj = {
316
+ JSON: mergedJson,
317
+ MODULE: userModule,
318
+ BOOLEAN: mergedBoolean,
319
+ }
320
+
321
+ this.tableLog.parserConfig(
322
+ pObj.JSON.map(String),
323
+ pObj.BOOLEAN.map(String),
324
+ Object.keys(pObj.MODULE)
325
+ )
326
+
327
+ return this.table<_T>(tableName, pObj)
328
+ }
329
+
330
+ /**
331
+ * Create an index on a table
332
+ */
333
+ createIndex(
334
+ indexName: string,
335
+ tableName: string,
336
+ columns: IndexColumn | IndexColumn[],
337
+ options?: {
338
+ unique?: boolean
339
+ ifNotExists?: boolean
340
+ /** Alias for `where` */
341
+ partial?: string
342
+ /** WHERE clause for partial indexes */
343
+ where?: string
344
+ /** Index method (USING ...) */
345
+ using?: IndexMethod
346
+ }
347
+ ): void {
348
+ helperCreateIndex(this.db, indexName, tableName, columns, options)
349
+ }
350
+
351
+ /**
352
+ * Drop a table
353
+ */
354
+ dropTable(tableName: string, options?: { ifExists?: boolean }): void {
355
+ const ifExists = options?.ifExists ? "IF EXISTS " : ""
356
+ const quoteIdent = (s: string) => `"${s.replace(/"/g, '""')}"`
357
+ const sql = `DROP TABLE ${ifExists}${quoteIdent(tableName)};`
358
+ this.db.run(sql)
359
+ }
360
+
361
+ /**
362
+ * Drop an index
363
+ */
364
+ dropIndex(indexName: string, options?: { ifExists?: boolean }): void {
365
+ helperDropIndex(this.db, indexName, options)
366
+ }
367
+
368
+ /**
369
+ * Store table comment as metadata (using a system table if needed)
370
+ */
371
+ getTableComment(tableName: string): string | null {
372
+ return helperGetTableComment(this.db, tableName)
373
+ }
374
+
375
+ /**
376
+ * runute a raw SQL statement
377
+ */
378
+ run(sql: string): void {
379
+ this.tableLog.debug(`Running SQL: ${sql}`)
380
+ this.db.run(sql)
381
+ }
382
+
383
+ /**
384
+ * Prepare a SQL statement for repeated runution
385
+ */
386
+ prepare(sql: string) {
387
+ return this.db.prepare(sql)
388
+ }
389
+
390
+ /**
391
+ * runute a transaction
392
+ */
393
+ transaction<T>(fn: () => T): T {
394
+ return this.db.transaction(fn)()
395
+ }
396
+
397
+ /**
398
+ * Begin a transaction manually
399
+ */
400
+ begin(mode?: "DEFERRED" | "IMMEDIATE" | "EXCLUSIVE"): void {
401
+ const modeStr = mode ? ` ${mode}` : ""
402
+ this.db.run(`BEGIN${modeStr}`)
403
+ }
404
+
405
+ /**
406
+ * Commit a transaction
407
+ */
408
+ commit(): void {
409
+ this.dbLog.transaction("commit")
410
+ this.run("COMMIT")
411
+ }
412
+
413
+ /**
414
+ * Rollback a transaction
415
+ */
416
+ rollback(): void {
417
+ this.dbLog.transaction("rollback")
418
+ this.run("ROLLBACK")
419
+ }
420
+
421
+ /**
422
+ * Create a savepoint
423
+ */
424
+ savepoint(name: string): void {
425
+ const quotedName = `"${name.replace(/"/g, '""')}"`
426
+ this.db.run(`SAVEPOINT ${quotedName}`)
427
+ }
428
+
429
+ /**
430
+ * Release a savepoint
431
+ */
432
+ releaseSavepoint(name: string): void {
433
+ const quotedName = `"${name.replace(/"/g, '""')}"`
434
+ this.db.run(`RELEASE SAVEPOINT ${quotedName}`)
435
+ }
436
+
437
+ /**
438
+ * Rollback to a savepoint
439
+ */
440
+ rollbackToSavepoint(name: string): void {
441
+ const quotedName = `"${name.replace(/"/g, '""')}"`
442
+ this.db.run(`ROLLBACK TO SAVEPOINT ${quotedName}`)
443
+ }
444
+
445
+ /**
446
+ * Vacuum the database (reclaim space and optimize)
447
+ */
448
+ vacuum() {
449
+ const result = this.db.run("VACUUM")
450
+ this.dbLog.debug("Vacuum completed")
451
+ return result
452
+ }
453
+
454
+ /**
455
+ * Analyze the database (update statistics for query optimizer)
456
+ */
457
+ analyze(tableName?: string): void {
458
+ if (tableName) {
459
+ const quotedName = `"${tableName.replace(/"/g, '""')}"`
460
+ this.db.run(`ANALYZE ${quotedName}`)
461
+ } else {
462
+ this.db.run("ANALYZE")
463
+ }
464
+ }
465
+
466
+ /**
467
+ * Check database integrity
468
+ */
469
+ integrityCheck(): Array<{ integrity_check: string }> {
470
+ const stmt = this.db.prepare("PRAGMA integrity_check")
471
+ return stmt.all() as Array<{ integrity_check: string }>
472
+ }
473
+
474
+ /**
475
+ * Get database schema information
476
+ */
477
+ getSchema(): Array<{ name: string; type: string; sql: string }> {
478
+ const stmt = this.db.prepare(`
479
+ SELECT name, type, sql
480
+ FROM sqlite_master
481
+ WHERE type IN ('table', 'index', 'view', 'trigger')
482
+ ORDER BY type, name
483
+ `)
484
+ return stmt.all() as Array<{ name: string; type: string; sql: string }>
485
+ }
486
+
487
+ /**
488
+ * Get table info (columns, types, constraints)
489
+ */
490
+ getTableInfo(tableName: string): Array<{
491
+ cid: number
492
+ name: string
493
+ type: string
494
+ notnull: number
495
+ dflt_value: SQLQueryBindings
496
+ pk: number
497
+ }> {
498
+ const stmt = this.db.prepare(`PRAGMA table_info("${tableName.replace(/"/g, '""')}")`)
499
+ return stmt.all() as Array<{
500
+ cid: number
501
+ name: string
502
+ type: string
503
+ notnull: number
504
+ dflt_value: SQLQueryBindings
505
+ pk: number
506
+ }>
507
+ }
508
+
509
+ /**
510
+ * Get foreign key information for a table
511
+ */
512
+ getForeignKeys(tableName: string): Array<{
513
+ id: number
514
+ seq: number
515
+ table: string
516
+ from: string
517
+ to: string
518
+ on_update: string
519
+ on_delete: string
520
+ match: string
521
+ }> {
522
+ const stmt = this.db.prepare(`PRAGMA foreign_key_list("${tableName.replace(/"/g, '""')}")`)
523
+ return stmt.all() as Array<{
524
+ id: number
525
+ seq: number
526
+ table: string
527
+ from: string
528
+ to: string
529
+ on_update: string
530
+ on_delete: string
531
+ match: string
532
+ }>
533
+ }
534
+
535
+ /**
536
+ * Get index information for a table
537
+ */
538
+ getIndexes(tableName: string): Array<{
539
+ name: string
540
+ unique: number
541
+ origin: string
542
+ partial: number
543
+ }> {
544
+ const stmt = this.db.prepare(`PRAGMA index_list("${tableName.replace(/"/g, '""')}")`)
545
+ return stmt.all() as Array<{
546
+ name: string
547
+ unique: number
548
+ origin: string
549
+ partial: number
550
+ }>
551
+ }
552
+
553
+ /**
554
+ * Set or get a PRAGMA value.
555
+ *
556
+ * @param name - PRAGMA name (e.g., "foreign_keys", "journal_mode")
557
+ * @param value - Value to set (omit to get current value)
558
+ * @returns Current value when getting, undefined when setting
559
+ */
560
+ pragma(name: string, value?: SQLQueryBindings): SQLQueryBindings | undefined {
561
+ if (value !== undefined) {
562
+ this.db.run(`PRAGMA ${name} = ${value}`)
563
+ return undefined
564
+ }
565
+ const result = this.db.prepare(`PRAGMA ${name}`).get() as Record<string, SQLQueryBindings>
566
+ return Object.values(result)[0]
567
+ }
568
+
569
+ /**
570
+ * Load a SQLite extension.
571
+ *
572
+ * @param path - Absolute path to the compiled SQLite extension
573
+ */
574
+ loadExtension(path: string): void {
575
+ this.db.loadExtension(path)
576
+ }
577
+
578
+ /**
579
+ * Get direct access to the underlying SQLite database instance.
580
+ * Use this for advanced operations not covered by the wrapper.
581
+ *
582
+ * @returns The underlying Database instance
583
+ */
584
+ getDb(): Database {
585
+ return this.db
586
+ }
587
+ }
588
+
589
+ export { DB }
590
+ export default DB
@@ -0,0 +1,38 @@
1
+ import type { AutoBackupOptions } from "../../index"
2
+
3
+ /**
4
+ * Apply retention policy to remove old backups
5
+ */
6
+ export function applyRetentionPolicy(backupLog: any, autoBackupOptions: AutoBackupOptions): void {
7
+ if (!autoBackupOptions) return
8
+
9
+ const fs = require("node:fs")
10
+ const path = require("node:path")
11
+
12
+ const backupDir = autoBackupOptions.directory
13
+ const prefix = autoBackupOptions.filenamePrefix || "backup"
14
+ const maxBackups = autoBackupOptions.maxBackups || 10
15
+
16
+ try {
17
+ const files = fs
18
+ .readdirSync(backupDir)
19
+ .filter((file: string) => file.startsWith(prefix) && file.endsWith(".db"))
20
+ .map((file: string) => ({
21
+ name: file,
22
+ path: path.join(backupDir, file),
23
+ mtime: fs.statSync(path.join(backupDir, file)).mtime.getTime(),
24
+ }))
25
+ .sort((a: { mtime: number }, b: { mtime: number }) => b.mtime - a.mtime) // newest first
26
+
27
+ if (files.length > maxBackups) {
28
+ const toDelete = files.slice(maxBackups)
29
+ for (const file of toDelete) {
30
+ fs.unlinkSync(file.path)
31
+ backupLog.debug(`Removed old backup: ${file.name}`)
32
+ }
33
+ backupLog.info(`Retention policy applied: removed ${toDelete.length} old backup(s)`)
34
+ }
35
+ } catch (error) {
36
+ backupLog.error(`Failed to apply retention policy: ${error}`)
37
+ }
38
+ }