@tachybase/plugin-database-clean 1.6.2
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 +134 -0
- package/README.zh-CN.md +136 -0
- package/client.d.ts +2 -0
- package/client.js +1 -0
- package/dist/client/index.d.ts +1 -0
- package/dist/client/index.js +1 -0
- package/dist/client/locale.d.ts +7 -0
- package/dist/client/pages/TableDetail.d.ts +1 -0
- package/dist/client/pages/TableList.d.ts +1 -0
- package/dist/client/plugin.d.ts +7 -0
- package/dist/externalVersion.js +11 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +39 -0
- package/dist/locale/en-US.json +65 -0
- package/dist/locale/zh-CN.json +65 -0
- package/dist/node_modules/archiver/LICENSE +22 -0
- package/dist/node_modules/archiver/index.js +73 -0
- package/dist/node_modules/archiver/lib/core.js +974 -0
- package/dist/node_modules/archiver/lib/error.js +40 -0
- package/dist/node_modules/archiver/lib/plugins/json.js +110 -0
- package/dist/node_modules/archiver/lib/plugins/tar.js +167 -0
- package/dist/node_modules/archiver/lib/plugins/zip.js +120 -0
- package/dist/node_modules/archiver/package.json +1 -0
- package/dist/server/adapters/base-adapter.d.ts +63 -0
- package/dist/server/adapters/base-adapter.js +31 -0
- package/dist/server/adapters/index.d.ts +36 -0
- package/dist/server/adapters/index.js +86 -0
- package/dist/server/adapters/mysql-adapter.d.ts +13 -0
- package/dist/server/adapters/mysql-adapter.js +118 -0
- package/dist/server/adapters/postgres-adapter.d.ts +13 -0
- package/dist/server/adapters/postgres-adapter.js +115 -0
- package/dist/server/adapters/sqlite-adapter.d.ts +13 -0
- package/dist/server/adapters/sqlite-adapter.js +115 -0
- package/dist/server/collections/.gitkeep +0 -0
- package/dist/server/constants.d.ts +4 -0
- package/dist/server/constants.js +38 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +33 -0
- package/dist/server/plugin.d.ts +11 -0
- package/dist/server/plugin.js +61 -0
- package/dist/server/resourcers/database-clean.d.ts +31 -0
- package/dist/server/resourcers/database-clean.js +303 -0
- package/dist/server/services/database-service.d.ts +46 -0
- package/dist/server/services/database-service.js +138 -0
- package/dist/server/services/filtered-backup-service.d.ts +40 -0
- package/dist/server/services/filtered-backup-service.js +269 -0
- package/dist/server/utils/lock.d.ts +23 -0
- package/dist/server/utils/lock.js +62 -0
- package/dist/server/utils.d.ts +2 -0
- package/dist/server/utils.js +43 -0
- package/package.json +26 -0
- package/server.d.ts +2 -0
- package/server.js +1 -0
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
var __create = Object.create;
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
20
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
21
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
22
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
23
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
24
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
25
|
+
mod
|
|
26
|
+
));
|
|
27
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
28
|
+
var database_clean_exports = {};
|
|
29
|
+
__export(database_clean_exports, {
|
|
30
|
+
default: () => database_clean_default
|
|
31
|
+
});
|
|
32
|
+
module.exports = __toCommonJS(database_clean_exports);
|
|
33
|
+
var import_node_fs = __toESM(require("node:fs"));
|
|
34
|
+
var import_promises = __toESM(require("node:fs/promises"));
|
|
35
|
+
var import_node_path = __toESM(require("node:path"));
|
|
36
|
+
var import_server = require("@tego/server");
|
|
37
|
+
var import_constants = require("../constants");
|
|
38
|
+
var import_database_service = require("../services/database-service");
|
|
39
|
+
var import_filtered_backup_service = require("../services/filtered-backup-service");
|
|
40
|
+
var import_lock = require("../utils/lock");
|
|
41
|
+
var database_clean_default = {
|
|
42
|
+
name: "databaseClean",
|
|
43
|
+
actions: {
|
|
44
|
+
/**
|
|
45
|
+
* 获取表列表
|
|
46
|
+
*/
|
|
47
|
+
async list(ctx, next) {
|
|
48
|
+
const { page = import_server.DEFAULT_PAGE, pageSize = import_server.DEFAULT_PER_PAGE, sort } = ctx.action.params;
|
|
49
|
+
const service = new import_database_service.DatabaseService(ctx.tego);
|
|
50
|
+
const tables = await service.getWhitelistTables();
|
|
51
|
+
tables.sort((a, b) => {
|
|
52
|
+
const aTime = a.updatedAt || a.createdAt;
|
|
53
|
+
const bTime = b.updatedAt || b.createdAt;
|
|
54
|
+
if (!aTime && !bTime) return 0;
|
|
55
|
+
if (!aTime) return 1;
|
|
56
|
+
if (!bTime) return -1;
|
|
57
|
+
return bTime.getTime() - aTime.getTime();
|
|
58
|
+
});
|
|
59
|
+
const count = tables.length;
|
|
60
|
+
const start = (Number(page) - 1) * Number(pageSize);
|
|
61
|
+
const end = start + Number(pageSize);
|
|
62
|
+
const rows = tables.slice(start, end);
|
|
63
|
+
ctx.body = {
|
|
64
|
+
count,
|
|
65
|
+
rows,
|
|
66
|
+
page: Number(page),
|
|
67
|
+
pageSize: Number(pageSize),
|
|
68
|
+
totalPage: Math.ceil(count / Number(pageSize))
|
|
69
|
+
};
|
|
70
|
+
await next();
|
|
71
|
+
},
|
|
72
|
+
/**
|
|
73
|
+
* 获取表信息
|
|
74
|
+
*/
|
|
75
|
+
async get(ctx, next) {
|
|
76
|
+
const { filterByTk } = ctx.action.params;
|
|
77
|
+
if (!filterByTk) {
|
|
78
|
+
ctx.throw(400, "Table name is required");
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const collection = ctx.tego.db.getCollection(filterByTk);
|
|
82
|
+
if (!collection) {
|
|
83
|
+
ctx.throw(404, `Collection ${filterByTk} not found`);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (!import_constants.WHITELIST_TABLES.includes(filterByTk)) {
|
|
87
|
+
ctx.throw(403, `Collection ${filterByTk} is not in whitelist`);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const service = new import_database_service.DatabaseService(ctx.tego);
|
|
91
|
+
const tableInfo = await service.getTableInfo(filterByTk);
|
|
92
|
+
const rawAttributes = collection.model.rawAttributes || {};
|
|
93
|
+
const hasCreatedAt = collection.hasField("createdAt") || "createdAt" in rawAttributes;
|
|
94
|
+
const hasUpdatedAt = collection.hasField("updatedAt") || "updatedAt" in rawAttributes;
|
|
95
|
+
let minId = null;
|
|
96
|
+
let maxId = null;
|
|
97
|
+
if (tableInfo.rowCount > 0) {
|
|
98
|
+
const idRange = await service.getIdRange(filterByTk);
|
|
99
|
+
minId = idRange.minId;
|
|
100
|
+
maxId = idRange.maxId;
|
|
101
|
+
}
|
|
102
|
+
ctx.body = {
|
|
103
|
+
...tableInfo,
|
|
104
|
+
hasCreatedAt,
|
|
105
|
+
hasUpdatedAt,
|
|
106
|
+
minId,
|
|
107
|
+
maxId
|
|
108
|
+
};
|
|
109
|
+
await next();
|
|
110
|
+
},
|
|
111
|
+
/**
|
|
112
|
+
* 获取表数据
|
|
113
|
+
*/
|
|
114
|
+
async data(ctx, next) {
|
|
115
|
+
const { filterByTk, page = import_server.DEFAULT_PAGE, pageSize = import_server.DEFAULT_PER_PAGE, filter, sort } = ctx.action.params;
|
|
116
|
+
if (!filterByTk) {
|
|
117
|
+
ctx.throw(400, "Table name is required");
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const collection = ctx.tego.db.getCollection(filterByTk);
|
|
121
|
+
if (!collection) {
|
|
122
|
+
ctx.throw(404, `Collection ${filterByTk} not found`);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (!import_constants.WHITELIST_TABLES.includes(filterByTk)) {
|
|
126
|
+
ctx.throw(403, `Collection ${filterByTk} is not in whitelist`);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const repository = ctx.tego.db.getRepository(filterByTk);
|
|
130
|
+
const limit = Number(pageSize);
|
|
131
|
+
const offset = (Number(page) - 1) * limit;
|
|
132
|
+
let sortFields = [];
|
|
133
|
+
if (sort) {
|
|
134
|
+
sortFields = Array.isArray(sort) ? sort : [sort];
|
|
135
|
+
} else {
|
|
136
|
+
if (collection.hasField("updatedAt")) {
|
|
137
|
+
sortFields.push("-updatedAt");
|
|
138
|
+
}
|
|
139
|
+
if (collection.hasField("createdAt")) {
|
|
140
|
+
sortFields.push("-createdAt");
|
|
141
|
+
}
|
|
142
|
+
if (sortFields.length === 0) {
|
|
143
|
+
const primaryKey = collection.model.primaryKeyAttribute || "id";
|
|
144
|
+
sortFields.push(`-${primaryKey}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const [rows, count] = await repository.findAndCount({
|
|
148
|
+
filter: filter || {},
|
|
149
|
+
limit,
|
|
150
|
+
offset,
|
|
151
|
+
sort: sortFields
|
|
152
|
+
});
|
|
153
|
+
let filteredMinId = null;
|
|
154
|
+
let filteredMaxId = null;
|
|
155
|
+
if (count > 0) {
|
|
156
|
+
const primaryKey = collection.model.primaryKeyAttribute || "id";
|
|
157
|
+
const minResult = await repository.findOne({
|
|
158
|
+
filter: filter || {},
|
|
159
|
+
sort: [primaryKey],
|
|
160
|
+
fields: [primaryKey]
|
|
161
|
+
});
|
|
162
|
+
const maxResult = await repository.findOne({
|
|
163
|
+
filter: filter || {},
|
|
164
|
+
sort: [`-${primaryKey}`],
|
|
165
|
+
fields: [primaryKey]
|
|
166
|
+
});
|
|
167
|
+
if (minResult && maxResult) {
|
|
168
|
+
filteredMinId = minResult[primaryKey];
|
|
169
|
+
filteredMaxId = maxResult[primaryKey];
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
ctx.body = {
|
|
173
|
+
count,
|
|
174
|
+
rows,
|
|
175
|
+
page: Number(page),
|
|
176
|
+
pageSize: Number(pageSize),
|
|
177
|
+
totalPage: Math.ceil(count / limit),
|
|
178
|
+
filteredMinId,
|
|
179
|
+
filteredMaxId
|
|
180
|
+
};
|
|
181
|
+
await next();
|
|
182
|
+
},
|
|
183
|
+
/**
|
|
184
|
+
* 备份数据
|
|
185
|
+
*/
|
|
186
|
+
async backup(ctx, next) {
|
|
187
|
+
const { collectionName, filter } = ctx.action.params.values;
|
|
188
|
+
if (!collectionName) {
|
|
189
|
+
ctx.throw(400, "Collection name is required");
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
const collection = ctx.tego.db.getCollection(collectionName);
|
|
193
|
+
if (!collection) {
|
|
194
|
+
ctx.throw(404, `Collection ${collectionName} not found`);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
if (!import_constants.WHITELIST_TABLES.includes(collectionName)) {
|
|
198
|
+
ctx.throw(403, `Collection ${collectionName} is not in whitelist`);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
const lock = new import_lock.DatabaseCleanLock(ctx.tego);
|
|
202
|
+
const locked = await lock.acquire(collectionName);
|
|
203
|
+
if (!locked) {
|
|
204
|
+
ctx.throw(409, `Collection ${collectionName} is being processed`);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
try {
|
|
208
|
+
const backupService = new import_filtered_backup_service.FilteredBackupService(ctx.tego);
|
|
209
|
+
const result = await backupService.backupCollection({
|
|
210
|
+
collectionName,
|
|
211
|
+
filter,
|
|
212
|
+
appName: ctx.tego.name
|
|
213
|
+
});
|
|
214
|
+
ctx.body = {
|
|
215
|
+
fileName: result.fileName,
|
|
216
|
+
filePath: result.filePath,
|
|
217
|
+
status: "success"
|
|
218
|
+
};
|
|
219
|
+
} catch (error) {
|
|
220
|
+
ctx.throw(500, `Backup failed: ${error.message}`);
|
|
221
|
+
} finally {
|
|
222
|
+
await lock.release(collectionName);
|
|
223
|
+
}
|
|
224
|
+
await next();
|
|
225
|
+
},
|
|
226
|
+
/**
|
|
227
|
+
* 清理数据
|
|
228
|
+
*/
|
|
229
|
+
async clean(ctx, next) {
|
|
230
|
+
const { collectionName, filter, vacuumFull } = ctx.action.params.values;
|
|
231
|
+
if (!collectionName) {
|
|
232
|
+
ctx.throw(400, "Collection name is required");
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
const collection = ctx.tego.db.getCollection(collectionName);
|
|
236
|
+
if (!collection) {
|
|
237
|
+
ctx.throw(404, `Collection ${collectionName} not found`);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
if (!import_constants.WHITELIST_TABLES.includes(collectionName)) {
|
|
241
|
+
ctx.throw(403, `Collection ${collectionName} is not in whitelist`);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
const lock = new import_lock.DatabaseCleanLock(ctx.tego);
|
|
245
|
+
const locked = await lock.acquire(collectionName);
|
|
246
|
+
if (!locked) {
|
|
247
|
+
ctx.throw(409, `Collection ${collectionName} is being processed`);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
try {
|
|
251
|
+
const cleanService = new import_database_service.DatabaseService(ctx.tego);
|
|
252
|
+
const result = await cleanService.cleanData({
|
|
253
|
+
collectionName,
|
|
254
|
+
filter
|
|
255
|
+
});
|
|
256
|
+
await cleanService.vacuum(collectionName, vacuumFull);
|
|
257
|
+
ctx.body = {
|
|
258
|
+
deletedCount: result.deletedCount,
|
|
259
|
+
vacuumFull: !!vacuumFull,
|
|
260
|
+
status: "success"
|
|
261
|
+
};
|
|
262
|
+
} catch (error) {
|
|
263
|
+
ctx.throw(500, `Clean failed: ${error.message}`);
|
|
264
|
+
} finally {
|
|
265
|
+
await lock.release(collectionName);
|
|
266
|
+
}
|
|
267
|
+
await next();
|
|
268
|
+
},
|
|
269
|
+
/**
|
|
270
|
+
* 下载备份文件
|
|
271
|
+
*/
|
|
272
|
+
async download(ctx, next) {
|
|
273
|
+
const { filterByTk } = ctx.action.params;
|
|
274
|
+
if (!filterByTk) {
|
|
275
|
+
ctx.throw(400, "File name is required");
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
const storageDir = import_node_path.default.resolve(
|
|
279
|
+
process.env.TEGO_RUNTIME_HOME || process.cwd(),
|
|
280
|
+
"storage",
|
|
281
|
+
"backups",
|
|
282
|
+
ctx.tego.name !== "main" ? ctx.tego.name : ""
|
|
283
|
+
);
|
|
284
|
+
const filePath = import_node_path.default.resolve(storageDir, filterByTk);
|
|
285
|
+
try {
|
|
286
|
+
await import_promises.default.access(filePath);
|
|
287
|
+
} catch {
|
|
288
|
+
ctx.throw(404, `Backup file ${filterByTk} not found`);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
if (!filterByTk.startsWith("db-clean_")) {
|
|
292
|
+
ctx.throw(403, "Invalid backup file");
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
const stats = import_node_fs.default.statSync(filePath);
|
|
296
|
+
ctx.set("Content-Length", stats.size.toString());
|
|
297
|
+
ctx.set("Content-Type", "application/zip");
|
|
298
|
+
ctx.attachment(filterByTk);
|
|
299
|
+
ctx.body = import_node_fs.default.createReadStream(filePath);
|
|
300
|
+
await next();
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Application } from '@tego/server';
|
|
2
|
+
export interface TableInfo {
|
|
3
|
+
name: string;
|
|
4
|
+
origin: string;
|
|
5
|
+
size: number;
|
|
6
|
+
rowCount: number;
|
|
7
|
+
createdAt: Date | null;
|
|
8
|
+
updatedAt: Date | null;
|
|
9
|
+
}
|
|
10
|
+
export interface CleanOptions {
|
|
11
|
+
collectionName: string;
|
|
12
|
+
filter?: any;
|
|
13
|
+
}
|
|
14
|
+
export declare class DatabaseService {
|
|
15
|
+
private app;
|
|
16
|
+
private adapter;
|
|
17
|
+
constructor(app: Application);
|
|
18
|
+
/**
|
|
19
|
+
* 获取白名单中的表列表
|
|
20
|
+
*/
|
|
21
|
+
getWhitelistTables(): Promise<TableInfo[]>;
|
|
22
|
+
/**
|
|
23
|
+
* 获取单个表的信息
|
|
24
|
+
*/
|
|
25
|
+
getTableInfo(collectionName: string): Promise<TableInfo>;
|
|
26
|
+
/**
|
|
27
|
+
* 获取表的 ID 范围
|
|
28
|
+
*/
|
|
29
|
+
getIdRange(collectionName: string): Promise<{
|
|
30
|
+
minId: number | null;
|
|
31
|
+
maxId: number | null;
|
|
32
|
+
}>;
|
|
33
|
+
/**
|
|
34
|
+
* 清理符合筛选条件的数据
|
|
35
|
+
* 注意:清理操作使用通用的 repository.destroy,不依赖特定数据库
|
|
36
|
+
*/
|
|
37
|
+
cleanData(options: CleanOptions): Promise<{
|
|
38
|
+
deletedCount: number;
|
|
39
|
+
}>;
|
|
40
|
+
/**
|
|
41
|
+
* 执行 VACUUM 操作以释放磁盘空间
|
|
42
|
+
* @param collectionName 集合名称
|
|
43
|
+
* @param full 是否使用 VACUUM FULL(会锁表但真正释放空间)
|
|
44
|
+
*/
|
|
45
|
+
vacuum(collectionName: string, full?: boolean): Promise<void>;
|
|
46
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
5
|
+
var __export = (target, all) => {
|
|
6
|
+
for (var name in all)
|
|
7
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
8
|
+
};
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (let key of __getOwnPropNames(from))
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
+
}
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
18
|
+
var database_service_exports = {};
|
|
19
|
+
__export(database_service_exports, {
|
|
20
|
+
DatabaseService: () => DatabaseService
|
|
21
|
+
});
|
|
22
|
+
module.exports = __toCommonJS(database_service_exports);
|
|
23
|
+
var import_adapters = require("../adapters");
|
|
24
|
+
var import_constants = require("../constants");
|
|
25
|
+
class DatabaseService {
|
|
26
|
+
constructor(app) {
|
|
27
|
+
this.app = app;
|
|
28
|
+
this.adapter = import_adapters.DatabaseAdapterFactory.getAdapter(app);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* 获取白名单中的表列表
|
|
32
|
+
*/
|
|
33
|
+
async getWhitelistTables() {
|
|
34
|
+
const tables = [];
|
|
35
|
+
const existingCollections = /* @__PURE__ */ new Set();
|
|
36
|
+
for (const collection of this.app.db.collections.values()) {
|
|
37
|
+
if (import_constants.WHITELIST_TABLES.includes(collection.name)) {
|
|
38
|
+
existingCollections.add(collection.name);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
for (const tableName of import_constants.WHITELIST_TABLES) {
|
|
42
|
+
if (!existingCollections.has(tableName)) {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
const collection = this.app.db.getCollection(tableName);
|
|
46
|
+
if (!collection) {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
const tableInfo = await this.getTableInfo(collection.name);
|
|
51
|
+
tables.push(tableInfo);
|
|
52
|
+
} catch (error) {
|
|
53
|
+
this.app.logger.warn(`Failed to get info for table ${tableName}:`, error);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return tables;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* 获取单个表的信息
|
|
60
|
+
*/
|
|
61
|
+
async getTableInfo(collectionName) {
|
|
62
|
+
const collection = this.app.db.getCollection(collectionName);
|
|
63
|
+
if (!collection) {
|
|
64
|
+
throw new Error(`Collection ${collectionName} not found`);
|
|
65
|
+
}
|
|
66
|
+
if (!import_constants.WHITELIST_TABLES.includes(collectionName)) {
|
|
67
|
+
throw new Error(`Collection ${collectionName} is not in whitelist`);
|
|
68
|
+
}
|
|
69
|
+
const sizeInfo = await this.adapter.getTableSize(collection);
|
|
70
|
+
const size = sizeInfo.totalSize;
|
|
71
|
+
const rowCount = await this.adapter.getRowCount(collection);
|
|
72
|
+
let createdAt = null;
|
|
73
|
+
let updatedAt = null;
|
|
74
|
+
if (rowCount > 0) {
|
|
75
|
+
const rawAttributes = collection.model.rawAttributes || {};
|
|
76
|
+
const hasCreatedAt = collection.hasField("createdAt") || "createdAt" in rawAttributes;
|
|
77
|
+
const hasUpdatedAt = collection.hasField("updatedAt") || "updatedAt" in rawAttributes;
|
|
78
|
+
const timeRange = await this.adapter.getTimeRange(collection, hasCreatedAt, hasUpdatedAt);
|
|
79
|
+
createdAt = timeRange.minCreatedAt;
|
|
80
|
+
updatedAt = timeRange.maxUpdatedAt || timeRange.maxCreatedAt;
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
name: collectionName,
|
|
84
|
+
origin: collection.origin || "unknown",
|
|
85
|
+
size,
|
|
86
|
+
rowCount,
|
|
87
|
+
createdAt,
|
|
88
|
+
updatedAt
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* 获取表的 ID 范围
|
|
93
|
+
*/
|
|
94
|
+
async getIdRange(collectionName) {
|
|
95
|
+
const collection = this.app.db.getCollection(collectionName);
|
|
96
|
+
if (!collection) {
|
|
97
|
+
throw new Error(`Collection ${collectionName} not found`);
|
|
98
|
+
}
|
|
99
|
+
return await this.adapter.getIdRange(collection);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* 清理符合筛选条件的数据
|
|
103
|
+
* 注意:清理操作使用通用的 repository.destroy,不依赖特定数据库
|
|
104
|
+
*/
|
|
105
|
+
async cleanData(options) {
|
|
106
|
+
const { collectionName, filter } = options;
|
|
107
|
+
const collection = this.app.db.getCollection(collectionName);
|
|
108
|
+
if (!collection) {
|
|
109
|
+
throw new Error(`Collection ${collectionName} not found`);
|
|
110
|
+
}
|
|
111
|
+
if (!import_constants.WHITELIST_TABLES.includes(collectionName)) {
|
|
112
|
+
throw new Error(`Collection ${collectionName} is not in whitelist`);
|
|
113
|
+
}
|
|
114
|
+
const repository = this.app.db.getRepository(collectionName);
|
|
115
|
+
const result = await repository.destroy({
|
|
116
|
+
filter: filter || {}
|
|
117
|
+
});
|
|
118
|
+
return {
|
|
119
|
+
deletedCount: result
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* 执行 VACUUM 操作以释放磁盘空间
|
|
124
|
+
* @param collectionName 集合名称
|
|
125
|
+
* @param full 是否使用 VACUUM FULL(会锁表但真正释放空间)
|
|
126
|
+
*/
|
|
127
|
+
async vacuum(collectionName, full = false) {
|
|
128
|
+
const collection = this.app.db.getCollection(collectionName);
|
|
129
|
+
if (!collection) {
|
|
130
|
+
throw new Error(`Collection ${collectionName} not found`);
|
|
131
|
+
}
|
|
132
|
+
await this.adapter.vacuum(collection, full);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
136
|
+
0 && (module.exports = {
|
|
137
|
+
DatabaseService
|
|
138
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Application } from '@tego/server';
|
|
2
|
+
export interface BackupFilter {
|
|
3
|
+
createdAt?: {
|
|
4
|
+
$gte?: string;
|
|
5
|
+
$lte?: string;
|
|
6
|
+
};
|
|
7
|
+
updatedAt?: {
|
|
8
|
+
$gte?: string;
|
|
9
|
+
$lte?: string;
|
|
10
|
+
};
|
|
11
|
+
id?: {
|
|
12
|
+
$gte?: number;
|
|
13
|
+
$lte?: number;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export interface BackupOptions {
|
|
17
|
+
collectionName: string;
|
|
18
|
+
filter?: BackupFilter;
|
|
19
|
+
fileName?: string;
|
|
20
|
+
appName?: string;
|
|
21
|
+
}
|
|
22
|
+
export declare class FilteredBackupService {
|
|
23
|
+
private app;
|
|
24
|
+
private workDir;
|
|
25
|
+
constructor(app: Application);
|
|
26
|
+
/**
|
|
27
|
+
* 生成备份文件名
|
|
28
|
+
*/
|
|
29
|
+
static generateFileName(collectionName: string, filter?: BackupFilter): string;
|
|
30
|
+
/**
|
|
31
|
+
* 备份单个集合的筛选数据
|
|
32
|
+
*/
|
|
33
|
+
backupCollection(options: BackupOptions): Promise<{
|
|
34
|
+
fileName: string;
|
|
35
|
+
filePath: string;
|
|
36
|
+
}>;
|
|
37
|
+
private packDumpedDir;
|
|
38
|
+
private getStorageDir;
|
|
39
|
+
private clearWorkDir;
|
|
40
|
+
}
|