@lazyapps/readmodel-backup-mongodb 0.0.0-init.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/index.js +232 -0
- package/package.json +42 -0
package/index.js
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { getLogger } from '@lazyapps/logger';
|
|
2
|
+
|
|
3
|
+
const generateBackupId = (readModelName) =>
|
|
4
|
+
`backup_${Date.now()}_${readModelName}`;
|
|
5
|
+
|
|
6
|
+
const parseMaxAge = (maxAge) => {
|
|
7
|
+
const match = maxAge.match(/^(\d+)([dhm])$/);
|
|
8
|
+
if (!match) return 0;
|
|
9
|
+
const value = parseInt(match[1], 10);
|
|
10
|
+
const unit = match[2];
|
|
11
|
+
if (unit === 'd') return value * 24 * 60 * 60 * 1000;
|
|
12
|
+
if (unit === 'h') return value * 60 * 60 * 1000;
|
|
13
|
+
if (unit === 'm') return value * 60 * 1000;
|
|
14
|
+
return 0;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const mongoBackup =
|
|
18
|
+
({ metadataCollection = 'admin.backups' } = {}) =>
|
|
19
|
+
(storage) => ({
|
|
20
|
+
createBackup: (correlationId, readModelName, collectionNames) => {
|
|
21
|
+
const log = getLogger('RM/Backup', correlationId);
|
|
22
|
+
const backupId = generateBackupId(readModelName);
|
|
23
|
+
const timestamp = Date.now();
|
|
24
|
+
|
|
25
|
+
log.debug(
|
|
26
|
+
`Creating backup ${backupId} for ${readModelName} (collections: ${JSON.stringify(collectionNames)})`,
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
return collectionNames
|
|
30
|
+
.reduce(
|
|
31
|
+
(chain, colName) =>
|
|
32
|
+
chain.then(() =>
|
|
33
|
+
storage.copyCollection(
|
|
34
|
+
correlationId,
|
|
35
|
+
colName,
|
|
36
|
+
`${backupId}_${colName}`,
|
|
37
|
+
),
|
|
38
|
+
),
|
|
39
|
+
Promise.resolve(),
|
|
40
|
+
)
|
|
41
|
+
.then(() =>
|
|
42
|
+
storage
|
|
43
|
+
.perRequest(correlationId)
|
|
44
|
+
.find('readmodel.state', { name: readModelName })
|
|
45
|
+
.toArray(),
|
|
46
|
+
)
|
|
47
|
+
.then((docs) => {
|
|
48
|
+
const eventTimestamp = docs[0]?.lastProjectedEventTimestamp || 0;
|
|
49
|
+
const metadata = {
|
|
50
|
+
backupId,
|
|
51
|
+
readModelName,
|
|
52
|
+
timestamp,
|
|
53
|
+
eventTimestamp,
|
|
54
|
+
collections: collectionNames,
|
|
55
|
+
};
|
|
56
|
+
return storage
|
|
57
|
+
.perRequest(correlationId)
|
|
58
|
+
.insertOne(metadataCollection, metadata)
|
|
59
|
+
.then(() => {
|
|
60
|
+
log.debug(`Backup ${backupId} created successfully`);
|
|
61
|
+
return { backupId, timestamp, eventTimestamp };
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
listBackups: (readModelName) =>
|
|
67
|
+
storage
|
|
68
|
+
.perRequest('backup')
|
|
69
|
+
.find(metadataCollection, { readModelName })
|
|
70
|
+
.sort({ timestamp: -1 })
|
|
71
|
+
.toArray()
|
|
72
|
+
.then((docs) => docs.map(({ _id, ...rest }) => rest)),
|
|
73
|
+
|
|
74
|
+
restoreBackup: (correlationId, readModelName, backupId) => {
|
|
75
|
+
const log = getLogger('RM/Backup', correlationId);
|
|
76
|
+
log.debug(`Restoring backup ${backupId} for ${readModelName}`);
|
|
77
|
+
|
|
78
|
+
return storage
|
|
79
|
+
.perRequest(correlationId)
|
|
80
|
+
.find(metadataCollection, { backupId })
|
|
81
|
+
.toArray()
|
|
82
|
+
.then((docs) => {
|
|
83
|
+
if (!docs.length)
|
|
84
|
+
return Promise.reject(new Error(`Backup ${backupId} not found`));
|
|
85
|
+
return docs[0];
|
|
86
|
+
})
|
|
87
|
+
.then((metadata) =>
|
|
88
|
+
metadata.collections
|
|
89
|
+
.reduce(
|
|
90
|
+
(chain, colName) =>
|
|
91
|
+
chain.then(() =>
|
|
92
|
+
storage
|
|
93
|
+
.dropCollection(correlationId, colName)
|
|
94
|
+
.then(() =>
|
|
95
|
+
storage.copyCollection(
|
|
96
|
+
correlationId,
|
|
97
|
+
`${backupId}_${colName}`,
|
|
98
|
+
colName,
|
|
99
|
+
),
|
|
100
|
+
),
|
|
101
|
+
),
|
|
102
|
+
Promise.resolve(),
|
|
103
|
+
)
|
|
104
|
+
.then(() =>
|
|
105
|
+
storage.updateLastProjectedEventTimestamps(
|
|
106
|
+
correlationId,
|
|
107
|
+
[readModelName],
|
|
108
|
+
metadata.eventTimestamp,
|
|
109
|
+
),
|
|
110
|
+
)
|
|
111
|
+
.then(() => {
|
|
112
|
+
log.debug(`Backup ${backupId} restored successfully`);
|
|
113
|
+
}),
|
|
114
|
+
);
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
deleteBackup: (correlationId, backupId) => {
|
|
118
|
+
const log = getLogger('RM/Backup', correlationId);
|
|
119
|
+
log.debug(`Deleting backup ${backupId}`);
|
|
120
|
+
|
|
121
|
+
return storage
|
|
122
|
+
.perRequest(correlationId)
|
|
123
|
+
.find(metadataCollection, { backupId })
|
|
124
|
+
.toArray()
|
|
125
|
+
.then((docs) => {
|
|
126
|
+
if (!docs.length) return Promise.resolve();
|
|
127
|
+
const metadata = docs[0];
|
|
128
|
+
return metadata.collections
|
|
129
|
+
.reduce(
|
|
130
|
+
(chain, colName) =>
|
|
131
|
+
chain.then(() =>
|
|
132
|
+
storage.dropCollection(
|
|
133
|
+
correlationId,
|
|
134
|
+
`${backupId}_${colName}`,
|
|
135
|
+
),
|
|
136
|
+
),
|
|
137
|
+
Promise.resolve(),
|
|
138
|
+
)
|
|
139
|
+
.then(() =>
|
|
140
|
+
storage
|
|
141
|
+
.perRequest(correlationId)
|
|
142
|
+
.deleteOne(metadataCollection, { backupId }),
|
|
143
|
+
)
|
|
144
|
+
.then(() => {
|
|
145
|
+
log.debug(`Backup ${backupId} deleted`);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
clearCollections: (correlationId, readModelName, collectionNames) => {
|
|
151
|
+
const log = getLogger('RM/Backup', correlationId);
|
|
152
|
+
log.debug(
|
|
153
|
+
`Clearing collections for ${readModelName}: ${JSON.stringify(collectionNames)}`,
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
return collectionNames
|
|
157
|
+
.reduce(
|
|
158
|
+
(chain, colName) =>
|
|
159
|
+
chain.then(() => storage.dropCollection(correlationId, colName)),
|
|
160
|
+
Promise.resolve(),
|
|
161
|
+
)
|
|
162
|
+
.then(() =>
|
|
163
|
+
storage.updateLastProjectedEventTimestamps(
|
|
164
|
+
correlationId,
|
|
165
|
+
[readModelName],
|
|
166
|
+
0,
|
|
167
|
+
),
|
|
168
|
+
)
|
|
169
|
+
.then(() => {
|
|
170
|
+
log.debug(`Collections cleared for ${readModelName}`);
|
|
171
|
+
});
|
|
172
|
+
},
|
|
173
|
+
|
|
174
|
+
cleanupBackups: (readModelName, retentionPolicy) => {
|
|
175
|
+
const log = getLogger('RM/Backup', 'cleanup');
|
|
176
|
+
|
|
177
|
+
return storage
|
|
178
|
+
.perRequest('cleanup')
|
|
179
|
+
.find(metadataCollection, { readModelName })
|
|
180
|
+
.sort({ timestamp: -1 })
|
|
181
|
+
.toArray()
|
|
182
|
+
.then((backups) => {
|
|
183
|
+
let toDelete = [];
|
|
184
|
+
|
|
185
|
+
if (
|
|
186
|
+
retentionPolicy.maxCount &&
|
|
187
|
+
backups.length > retentionPolicy.maxCount
|
|
188
|
+
) {
|
|
189
|
+
toDelete = backups.slice(retentionPolicy.maxCount);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (retentionPolicy.maxAge) {
|
|
193
|
+
const cutoff = Date.now() - parseMaxAge(retentionPolicy.maxAge);
|
|
194
|
+
const aged = backups.filter((b) => b.timestamp < cutoff);
|
|
195
|
+
toDelete = [...new Set([...toDelete, ...aged])];
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (!toDelete.length) return Promise.resolve();
|
|
199
|
+
|
|
200
|
+
log.debug(
|
|
201
|
+
`Cleaning up ${toDelete.length} old backups for ${readModelName}`,
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
return toDelete.reduce(
|
|
205
|
+
(chain, backup) =>
|
|
206
|
+
chain.then(() =>
|
|
207
|
+
backup.collections
|
|
208
|
+
.reduce(
|
|
209
|
+
(innerChain, colName) =>
|
|
210
|
+
innerChain.then(() =>
|
|
211
|
+
storage.dropCollection(
|
|
212
|
+
'cleanup',
|
|
213
|
+
`${backup.backupId}_${colName}`,
|
|
214
|
+
),
|
|
215
|
+
),
|
|
216
|
+
Promise.resolve(),
|
|
217
|
+
)
|
|
218
|
+
.then(() =>
|
|
219
|
+
storage
|
|
220
|
+
.perRequest('cleanup')
|
|
221
|
+
.deleteOne(metadataCollection, {
|
|
222
|
+
backupId: backup.backupId,
|
|
223
|
+
}),
|
|
224
|
+
),
|
|
225
|
+
),
|
|
226
|
+
Promise.resolve(),
|
|
227
|
+
);
|
|
228
|
+
});
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
export const __testing__ = { generateBackupId, parseMaxAge };
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@lazyapps/readmodel-backup-mongodb",
|
|
3
|
+
"version": "0.0.0-init.0",
|
|
4
|
+
"description": "MongoDB-based backup and restore for LazyApps read models",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"files": [
|
|
7
|
+
"index.js"
|
|
8
|
+
],
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "vitest"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"event-sourcing",
|
|
14
|
+
"cqrs",
|
|
15
|
+
"lazyapps",
|
|
16
|
+
"read-models",
|
|
17
|
+
"backup",
|
|
18
|
+
"mongodb"
|
|
19
|
+
],
|
|
20
|
+
"author": "Oliver Sturm",
|
|
21
|
+
"license": "ISC",
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "git+https://github.com/oliversturm/lazyapps-libs.git",
|
|
25
|
+
"directory": "packages/readmodel-backup-mongodb"
|
|
26
|
+
},
|
|
27
|
+
"bugs": {
|
|
28
|
+
"url": "https://github.com/oliversturm/lazyapps-libs/issues"
|
|
29
|
+
},
|
|
30
|
+
"homepage": "https://github.com/oliversturm/lazyapps-libs/tree/main/packages/readmodel-backup-mongodb#readme",
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=18.20.3 || >=20.18.0"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"eslint": "^8.46.0",
|
|
36
|
+
"vitest": "^4.0.18"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@lazyapps/logger": "workspace:^"
|
|
40
|
+
},
|
|
41
|
+
"type": "module"
|
|
42
|
+
}
|