outlet-orm 5.5.3 → 6.5.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.
@@ -0,0 +1,347 @@
1
+ /**
2
+ * BackupSocketServer
3
+ *
4
+ * A long-running TCP daemon that manages backup jobs for outlet-orm.
5
+ * Clients connect, send JSON commands, and receive JSON responses.
6
+ *
7
+ * Protocol: newline-delimited JSON (NDJSON) over TCP.
8
+ *
9
+ * ─── Client → Server commands ─────────────────────────────────────────
10
+ * { "action": "schedule", "type": "full|partial|journal",
11
+ * "config": { "intervalMs": 3600000, "tables": [...], "name": "...",
12
+ * "format": "sql|json", "flush": false,
13
+ * "encrypt": true, "encryptionPassword": "...", "saltLength": 6 } }
14
+ *
15
+ * { "action": "stop", "name": "job_name" }
16
+ * { "action": "stopAll" }
17
+ * { "action": "jobs" }
18
+ * { "action": "run", "type": "full|partial|journal",
19
+ * "tables": [...], "options": { "format": "sql", "filename": "..." } }
20
+ * { "action": "restore", "filePath": "/abs/path/to/backup.sql",
21
+ * "options": { "encryptionPassword": "..." } }
22
+ * { "action": "status" }
23
+ * { "action": "ping" }
24
+ *
25
+ * ─── Server → Client responses ────────────────────────────────────────
26
+ * { "ok": true, "result": <any> }
27
+ * { "ok": false, "error": "message" }
28
+ *
29
+ * ─── Server → All clients (push events) ──────────────────────────────
30
+ * { "event": "jobStart", "name": "...", "type": "..." }
31
+ * { "event": "jobDone", "name": "...", "type": "...", "filePath": "..." }
32
+ * { "event": "jobError", "name": "...", "type": "...", "error": "..." }
33
+ *
34
+ * Usage:
35
+ * const server = new BackupSocketServer(db, { port: 9119, backupPath: './database/backups' });
36
+ * await server.listen(); // starts the daemon
37
+ * // ...
38
+ * await server.close(); // graceful shutdown
39
+ */
40
+
41
+ 'use strict';
42
+
43
+ const net = require('net');
44
+ const events = require('events');
45
+ const BackupManager = require('./BackupManager');
46
+ const BackupScheduler = require('./BackupScheduler');
47
+
48
+ const DEFAULT_PORT = 9119;
49
+ const DEFAULT_HOST = '127.0.0.1';
50
+
51
+ class BackupSocketServer extends events.EventEmitter {
52
+ /**
53
+ * @param {import('../DatabaseConnection')} connection
54
+ * @param {object} [options]
55
+ * @param {number} [options.port=9119]
56
+ * @param {string} [options.host='127.0.0.1']
57
+ * @param {string} [options.backupPath] Forwarded to BackupManager
58
+ * @param {boolean} [options.encrypt] Default encryption for all jobs
59
+ * @param {string} [options.encryptionPassword] Default encryption password
60
+ * @param {number} [options.saltLength] Default grain de sable length
61
+ */
62
+ constructor(connection, options = {}) {
63
+ super();
64
+ this.connection = connection;
65
+ this.port = options.port || DEFAULT_PORT;
66
+ this.host = options.host || DEFAULT_HOST;
67
+
68
+ this._managerOptions = {
69
+ backupPath : options.backupPath,
70
+ encrypt : options.encrypt,
71
+ encryptionPassword : options.encryptionPassword,
72
+ saltLength : options.saltLength,
73
+ };
74
+
75
+ this.manager = new BackupManager(connection, this._managerOptions);
76
+ this.scheduler = new BackupScheduler(connection, this._managerOptions);
77
+
78
+ /** @type {Set<net.Socket>} */
79
+ this._clients = new Set();
80
+ this._server = null;
81
+ this._startTime = null;
82
+ }
83
+
84
+ // ─────────────────────────────────────────────────────────────────────────────
85
+ // Lifecycle
86
+ // ─────────────────────────────────────────────────────────────────────────────
87
+
88
+ /**
89
+ * Start the TCP server and begin accepting connections.
90
+ * @returns {Promise<void>}
91
+ */
92
+ listen() {
93
+ return new Promise((resolve, reject) => {
94
+ this._server = net.createServer((socket) => this._handleClient(socket));
95
+
96
+ this._server.on('error', (err) => {
97
+ this.emit('error', err);
98
+ reject(err);
99
+ });
100
+
101
+ this._server.listen(this.port, this.host, () => {
102
+ this._startTime = Date.now();
103
+ const addr = `${this.host}:${this.port}`;
104
+ console.log(`✓ BackupSocketServer listening on ${addr}`);
105
+ this.emit('listening', { host: this.host, port: this.port });
106
+ resolve();
107
+ });
108
+ });
109
+ }
110
+
111
+ /**
112
+ * Gracefully stop the server and all scheduled jobs.
113
+ * @returns {Promise<void>}
114
+ */
115
+ close() {
116
+ return new Promise((resolve) => {
117
+ this.scheduler.stopAll();
118
+
119
+ // Close all client connections
120
+ for (const sock of this._clients) {
121
+ try { sock.destroy(); } catch (_) { /* ignore */ }
122
+ }
123
+ this._clients.clear();
124
+
125
+ if (this._server) {
126
+ this._server.close(() => {
127
+ console.log('✓ BackupSocketServer closed');
128
+ this.emit('close');
129
+ resolve();
130
+ });
131
+ } else {
132
+ resolve();
133
+ }
134
+ });
135
+ }
136
+
137
+ /**
138
+ * Return server address info.
139
+ * @returns {{ host: string, port: number } | null}
140
+ */
141
+ address() {
142
+ if (!this._server) return null;
143
+ const addr = this._server.address();
144
+ return addr ? { host: addr.address, port: addr.port } : null;
145
+ }
146
+
147
+ // ─────────────────────────────────────────────────────────────────────────────
148
+ // Client handling
149
+ // ─────────────────────────────────────────────────────────────────────────────
150
+
151
+ /** @private */
152
+ _handleClient(socket) {
153
+ this._clients.add(socket);
154
+ let buffer = '';
155
+
156
+ socket.setEncoding('utf8');
157
+
158
+ socket.on('data', (chunk) => {
159
+ buffer += chunk;
160
+ // Messages are newline-delimited
161
+ const lines = buffer.split('\n');
162
+ buffer = lines.pop(); // keep incomplete last line
163
+
164
+ for (const line of lines) {
165
+ const trimmed = line.trim();
166
+ if (!trimmed) continue;
167
+ this._processMessage(socket, trimmed);
168
+ }
169
+ });
170
+
171
+ socket.on('close', () => {
172
+ this._clients.delete(socket);
173
+ });
174
+
175
+ socket.on('error', (err) => {
176
+ this._clients.delete(socket);
177
+ this.emit('clientError', err);
178
+ });
179
+ }
180
+
181
+ /** @private */
182
+ async _processMessage(socket, raw) {
183
+ let msg;
184
+ try {
185
+ msg = JSON.parse(raw);
186
+ } catch (_) {
187
+ this._send(socket, { ok: false, error: 'Invalid JSON' });
188
+ return;
189
+ }
190
+
191
+ try {
192
+ const result = await this._dispatch(msg);
193
+ this._send(socket, { ok: true, result });
194
+ } catch (err) {
195
+ this._send(socket, { ok: false, error: err.message || String(err) });
196
+ }
197
+ }
198
+
199
+ /** @private */
200
+ async _dispatch(msg) {
201
+ switch (msg.action) {
202
+
203
+ case 'ping':
204
+ return 'pong';
205
+
206
+ case 'status':
207
+ return {
208
+ uptime : this._startTime ? Date.now() - this._startTime : 0,
209
+ jobs : this.scheduler.activeJobs(),
210
+ clients: this._clients.size,
211
+ };
212
+
213
+ case 'jobs':
214
+ return this.scheduler.activeJobs();
215
+
216
+ case 'schedule': {
217
+ const type = msg.type;
218
+ const config = Object.assign({}, msg.config || {});
219
+
220
+ // Merge per-job encryption options with server defaults
221
+ const jobManagerOpts = this._buildManagerOptions(config);
222
+
223
+ // Use a mutable ref so callbacks always carry the final job name
224
+ // (which may be auto-generated if config.name was not provided)
225
+ const nameRef = { value: config.name || type };
226
+
227
+ // Wrap success/error to emit events to all connected clients
228
+ const originalOnSuccess = config.onSuccess;
229
+ const originalOnError = config.onError;
230
+
231
+ config.onSuccess = (filePath) => {
232
+ this._broadcast({ event: 'jobDone', name: nameRef.value, type, filePath });
233
+ if (typeof originalOnSuccess === 'function') originalOnSuccess(filePath);
234
+ };
235
+ config.onError = (err) => {
236
+ this._broadcast({ event: 'jobError', name: nameRef.value, type, error: err.message });
237
+ if (typeof originalOnError === 'function') originalOnError(err);
238
+ };
239
+
240
+ // Create a dedicated scheduler with the right per-job manager options
241
+ const jobScheduler = this._buildScheduler(jobManagerOpts);
242
+ const name = jobScheduler.schedule(type, config);
243
+
244
+ // Update ref with actual name (covers auto-generated names)
245
+ nameRef.value = name;
246
+
247
+ // Persist the interval handle in the main scheduler so status/stop work
248
+ this.scheduler._jobs.set(name, jobScheduler._jobs.get(name));
249
+
250
+ this._broadcast({ event: 'jobStart', name, type });
251
+ return name;
252
+ }
253
+
254
+ case 'stop':
255
+ this.scheduler.stop(msg.name);
256
+ return true;
257
+
258
+ case 'stopAll':
259
+ this.scheduler.stopAll();
260
+ return true;
261
+
262
+ case 'run': {
263
+ const type = msg.type;
264
+ const options = msg.options || {};
265
+ const tables = msg.tables;
266
+ const runManagerOpts = this._buildManagerOptions(options);
267
+ const runManager = new BackupManager(this.connection, runManagerOpts);
268
+
269
+ let filePath;
270
+ if (type === 'full') {
271
+ filePath = await runManager.full(options);
272
+ } else if (type === 'partial') {
273
+ if (!Array.isArray(tables) || tables.length === 0) {
274
+ throw new Error("'tables' array is required for partial backup");
275
+ }
276
+ filePath = await runManager.partial(tables, options);
277
+ } else if (type === 'journal') {
278
+ filePath = await runManager.journal(options);
279
+ } else {
280
+ throw new Error(`Unknown backup type "${type}"`);
281
+ }
282
+ return filePath;
283
+ }
284
+
285
+ case 'restore': {
286
+ if (!msg.filePath) {
287
+ throw new Error("'filePath' is required for restore");
288
+ }
289
+ // Build a restore-capable manager (needs encryptionPassword if file is encrypted)
290
+ const restoreOpts = Object.assign({}, this._managerOptions, {
291
+ encrypt : false, // manager doesn't need to encrypt on restore
292
+ encryptionPassword: (msg.options && msg.options.encryptionPassword)
293
+ || this._managerOptions.encryptionPassword,
294
+ });
295
+ const restoreManager = new BackupManager(this.connection, restoreOpts);
296
+ return restoreManager.restore(msg.filePath, msg.options || {});
297
+ }
298
+
299
+ default:
300
+ throw new Error(`Unknown action "${msg.action}"`);
301
+ }
302
+ }
303
+
304
+ // ─────────────────────────────────────────────────────────────────────────────
305
+ // Helpers
306
+ // ─────────────────────────────────────────────────────────────────────────────
307
+
308
+ /** @private */
309
+ _send(socket, payload) {
310
+ if (socket.writable) {
311
+ socket.write(JSON.stringify(payload) + '\n');
312
+ }
313
+ }
314
+
315
+ /** @private */
316
+ _broadcast(payload) {
317
+ const line = JSON.stringify(payload) + '\n';
318
+ for (const sock of this._clients) {
319
+ if (sock.writable) sock.write(line);
320
+ }
321
+ this.emit('event', payload);
322
+ }
323
+
324
+ /**
325
+ * Build BackupManager options, merging server defaults with per-call overrides.
326
+ * @private
327
+ */
328
+ _buildManagerOptions(overrides = {}) {
329
+ return {
330
+ backupPath : overrides.backupPath || this._managerOptions.backupPath,
331
+ encrypt : overrides.encrypt != null ? overrides.encrypt : this._managerOptions.encrypt,
332
+ encryptionPassword: overrides.encryptionPassword || this._managerOptions.encryptionPassword,
333
+ saltLength : overrides.saltLength != null ? overrides.saltLength : this._managerOptions.saltLength,
334
+ };
335
+ }
336
+
337
+ /**
338
+ * Build a throw-away BackupScheduler with a given manager.
339
+ * @private
340
+ */
341
+ _buildScheduler(managerOptions) {
342
+ const s = new BackupScheduler(this.connection, managerOptions);
343
+ return s;
344
+ }
345
+ }
346
+
347
+ module.exports = BackupSocketServer;
package/src/Model.js CHANGED
@@ -665,6 +665,147 @@ class Model {
665
665
  return this.query().with(...relations);
666
666
  }
667
667
 
668
+ // ==================== Convenience Query Methods ====================
669
+
670
+ /**
671
+ * Find the first record matching conditions or create a new one
672
+ * @param {Object} conditions - Where conditions to search
673
+ * @param {Object} [values={}] - Additional attributes for creation
674
+ * @returns {Promise<Model>}
675
+ */
676
+ static async firstOrCreate(conditions, values = {}) {
677
+ const query = this.query();
678
+ for (const [key, val] of Object.entries(conditions)) {
679
+ query.where(key, val);
680
+ }
681
+ const existing = await query.first();
682
+ if (existing) return existing;
683
+ return this.create({ ...conditions, ...values });
684
+ }
685
+
686
+ /**
687
+ * Find the first record matching conditions or return a new (unsaved) instance
688
+ * @param {Object} conditions - Where conditions to search
689
+ * @param {Object} [values={}] - Additional attributes for the new instance
690
+ * @returns {Promise<Model>}
691
+ */
692
+ static async firstOrNew(conditions, values = {}) {
693
+ const query = this.query();
694
+ for (const [key, val] of Object.entries(conditions)) {
695
+ query.where(key, val);
696
+ }
697
+ const existing = await query.first();
698
+ if (existing) return existing;
699
+ const instance = new this({ ...conditions, ...values });
700
+ return instance;
701
+ }
702
+
703
+ /**
704
+ * Find a record matching conditions and update it, or create a new one
705
+ * @param {Object} conditions - Where conditions to search
706
+ * @param {Object} values - Attributes to update or set on creation
707
+ * @returns {Promise<Model>}
708
+ */
709
+ static async updateOrCreate(conditions, values = {}) {
710
+ const query = this.query();
711
+ for (const [key, val] of Object.entries(conditions)) {
712
+ query.where(key, val);
713
+ }
714
+ const existing = await query.first();
715
+ if (existing) {
716
+ for (const [key, val] of Object.entries(values)) {
717
+ existing.setAttribute(key, val);
718
+ }
719
+ await existing.save();
720
+ return existing;
721
+ }
722
+ return this.create({ ...conditions, ...values });
723
+ }
724
+
725
+ /**
726
+ * Insert or update multiple records in bulk.
727
+ * @param {Array<Object>} rows - Array of records to upsert
728
+ * @param {string|string[]} uniqueBy - Column(s) that determine uniqueness
729
+ * @param {string[]} [update] - Columns to update on conflict (default: all non-unique columns)
730
+ * @returns {Promise<any>}
731
+ */
732
+ static async upsert(rows, uniqueBy, update) {
733
+ if (!rows || rows.length === 0) return;
734
+ this.ensureConnection();
735
+ const uniqueCols = Array.isArray(uniqueBy) ? uniqueBy : [uniqueBy];
736
+
737
+ // Determine columns to update on conflict
738
+ const allCols = Object.keys(rows[0]);
739
+ const updateCols = update || allCols.filter(c => !uniqueCols.includes(c));
740
+
741
+ // Build driver-specific upsert SQL
742
+ const table = this.table;
743
+ const columns = allCols;
744
+ const placeholders = rows.map(() => `(${columns.map(() => '?').join(', ')})`).join(', ');
745
+ const values = rows.flatMap(r => columns.map(c => r[c] !== undefined ? r[c] : null));
746
+
747
+ const conn = this.connection;
748
+ const driver = conn.config ? conn.config.driver : 'mysql';
749
+
750
+ let sql;
751
+ if (driver === 'sqlite') {
752
+ const updateSet = updateCols.map(c => `\`${c}\` = excluded.\`${c}\``).join(', ');
753
+ sql = `INSERT INTO \`${table}\` (${columns.map(c => `\`${c}\``).join(', ')}) VALUES ${placeholders} ON CONFLICT (${uniqueCols.map(c => `\`${c}\``).join(', ')}) DO UPDATE SET ${updateSet}`;
754
+ } else if (driver === 'postgres' || driver === 'postgresql') {
755
+ const updateSet = updateCols.map(c => `"${c}" = EXCLUDED."${c}"`).join(', ');
756
+ sql = `INSERT INTO "${table}" (${columns.map(c => `"${c}"`).join(', ')}) VALUES ${placeholders} ON CONFLICT (${uniqueCols.map(c => `"${c}"`).join(', ')}) DO UPDATE SET ${updateSet}`;
757
+ } else {
758
+ // MySQL: INSERT ... ON DUPLICATE KEY UPDATE
759
+ const updateSet = updateCols.map(c => `\`${c}\` = VALUES(\`${c}\`)`).join(', ');
760
+ sql = `INSERT INTO \`${table}\` (${columns.map(c => `\`${c}\``).join(', ')}) VALUES ${placeholders} ON DUPLICATE KEY UPDATE ${updateSet}`;
761
+ }
762
+
763
+ return conn.execute(sql, values);
764
+ }
765
+
766
+ // ==================== Observer ====================
767
+
768
+ /**
769
+ * Register an observer class that listens to model events.
770
+ * The observer may define methods: creating, created, updating, updated,
771
+ * saving, saved, deleting, deleted, restoring, restored.
772
+ * @param {Object|Function} observer - Observer instance or class
773
+ */
774
+ static observe(observer) {
775
+ const instance = typeof observer === 'function' ? new observer() : observer;
776
+ const events = [
777
+ 'creating', 'created', 'updating', 'updated',
778
+ 'saving', 'saved', 'deleting', 'deleted',
779
+ 'restoring', 'restored'
780
+ ];
781
+ for (const event of events) {
782
+ if (typeof instance[event] === 'function') {
783
+ this.on(event, (model) => instance[event](model));
784
+ }
785
+ }
786
+ }
787
+
788
+ // ==================== Cursor / Stream ====================
789
+
790
+ /**
791
+ * Lazily iterate over all matching records using an async generator.
792
+ * Yields one model instance at a time, consuming minimal memory.
793
+ * @param {number} [chunkSize=100] - Number of records per internal query
794
+ * @returns {AsyncGenerator<Model>}
795
+ */
796
+ static async *cursor(chunkSize = 100) {
797
+ let offset = 0;
798
+ while (true) {
799
+ const results = await this.query().limit(chunkSize).offset(offset).get();
800
+ if (results.length === 0) break;
801
+ for (const model of results) {
802
+ yield model;
803
+ }
804
+ if (results.length < chunkSize) break;
805
+ offset += chunkSize;
806
+ }
807
+ }
808
+
668
809
  /**
669
810
  * Include hidden attributes in query results
670
811
  * @returns {QueryBuilder}
@@ -704,18 +845,24 @@ class Model {
704
845
  }
705
846
 
706
847
  /**
707
- * Set an attribute
848
+ * Set an attribute (runs mutator if defined)
708
849
  * @param {string} key
709
850
  * @param {any} value
710
851
  * @returns {this}
711
852
  */
712
853
  setAttribute(key, value) {
854
+ // Check for mutator: set{Key}Attribute
855
+ const mutator = `set${key.charAt(0).toUpperCase()}${key.slice(1).replace(/_([a-z])/g, (_, c) => c.toUpperCase())}Attribute`;
856
+ if (typeof this[mutator] === 'function') {
857
+ this[mutator](value);
858
+ return this;
859
+ }
713
860
  this.attributes[key] = this.castAttribute(key, value);
714
861
  return this;
715
862
  }
716
863
 
717
864
  /**
718
- * Get an attribute
865
+ * Get an attribute (runs accessor if defined)
719
866
  * @param {string} key
720
867
  * @returns {any}
721
868
  */
@@ -723,6 +870,11 @@ class Model {
723
870
  if (this.relations[key]) {
724
871
  return this.relations[key];
725
872
  }
873
+ // Check for accessor: get{Key}Attribute
874
+ const accessor = `get${key.charAt(0).toUpperCase()}${key.slice(1).replace(/_([a-z])/g, (_, c) => c.toUpperCase())}Attribute`;
875
+ if (typeof this[accessor] === 'function') {
876
+ return this[accessor](this.attributes[key]);
877
+ }
726
878
  return this.castAttribute(key, this.attributes[key]);
727
879
  }
728
880
 
@@ -623,6 +623,88 @@ class QueryBuilder {
623
623
  return result;
624
624
  }
625
625
 
626
+ /**
627
+ * Get the first record matching current wheres or create a new one
628
+ * @param {Object} [values={}] - Additional attributes to merge on creation
629
+ * @returns {Promise<Model>}
630
+ */
631
+ async firstOrCreate(values = {}) {
632
+ const existing = await this.first();
633
+ if (existing) return existing;
634
+ // Build conditions from current wheres
635
+ const conditions = {};
636
+ for (const w of this.wheres) {
637
+ if (w.type === 'basic' && w.operator === '=') {
638
+ conditions[w.column] = w.value;
639
+ }
640
+ }
641
+ const instance = new this.model({ ...conditions, ...values });
642
+ return instance.save();
643
+ }
644
+
645
+ /**
646
+ * Get the first record matching current wheres or return a new (unsaved) instance
647
+ * @param {Object} [values={}] - Additional attributes for the instance
648
+ * @returns {Promise<Model>}
649
+ */
650
+ async firstOrNew(values = {}) {
651
+ const existing = await this.first();
652
+ if (existing) return existing;
653
+ const conditions = {};
654
+ for (const w of this.wheres) {
655
+ if (w.type === 'basic' && w.operator === '=') {
656
+ conditions[w.column] = w.value;
657
+ }
658
+ }
659
+ return new this.model({ ...conditions, ...values });
660
+ }
661
+
662
+ /**
663
+ * Find a record matching current wheres and update it, or create a new one
664
+ * @param {Object} values - Attributes to update or set on creation
665
+ * @returns {Promise<Model>}
666
+ */
667
+ async updateOrCreate(values = {}) {
668
+ const existing = await this.first();
669
+ if (existing) {
670
+ for (const [key, val] of Object.entries(values)) {
671
+ existing.setAttribute(key, val);
672
+ }
673
+ await existing.save();
674
+ return existing;
675
+ }
676
+ const conditions = {};
677
+ for (const w of this.wheres) {
678
+ if (w.type === 'basic' && w.operator === '=') {
679
+ conditions[w.column] = w.value;
680
+ }
681
+ }
682
+ const instance = new this.model({ ...conditions, ...values });
683
+ return instance.save();
684
+ }
685
+
686
+ /**
687
+ * Lazily iterate over matching records using an async generator.
688
+ * Yields one model instance at a time, consuming minimal memory.
689
+ * @param {number} [chunkSize=100] - Number of records per internal query
690
+ * @returns {AsyncGenerator<Model>}
691
+ */
692
+ async *cursor(chunkSize = 100) {
693
+ let offset = 0;
694
+ while (true) {
695
+ const cloned = this.clone();
696
+ cloned.limitValue = chunkSize;
697
+ cloned.offsetValue = offset;
698
+ const results = await cloned.get();
699
+ if (results.length === 0) break;
700
+ for (const model of results) {
701
+ yield model;
702
+ }
703
+ if (results.length < chunkSize) break;
704
+ offset += chunkSize;
705
+ }
706
+ }
707
+
626
708
  /**
627
709
  * Paginate the results
628
710
  * @param {number} page
package/src/index.js CHANGED
@@ -21,6 +21,13 @@ const MigrationManager = require('./Migrations/MigrationManager');
21
21
  const Seeder = require('./Seeders/Seeder');
22
22
  const SeederManager = require('./Seeders/SeederManager');
23
23
 
24
+ // Backup
25
+ const BackupManager = require('./Backup/BackupManager');
26
+ const BackupScheduler = require('./Backup/BackupScheduler');
27
+ const BackupEncryption = require('./Backup/BackupEncryption');
28
+ const BackupSocketServer = require('./Backup/BackupSocketServer');
29
+ const BackupSocketClient = require('./Backup/BackupSocketClient');
30
+
24
31
  module.exports = {
25
32
  // Core
26
33
  Model,
@@ -51,5 +58,12 @@ module.exports = {
51
58
 
52
59
  // Seeders
53
60
  Seeder,
54
- SeederManager
61
+ SeederManager,
62
+
63
+ // Backup
64
+ BackupManager,
65
+ BackupScheduler,
66
+ BackupEncryption,
67
+ BackupSocketServer,
68
+ BackupSocketClient
55
69
  };