@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.
Files changed (53) hide show
  1. package/README.md +134 -0
  2. package/README.zh-CN.md +136 -0
  3. package/client.d.ts +2 -0
  4. package/client.js +1 -0
  5. package/dist/client/index.d.ts +1 -0
  6. package/dist/client/index.js +1 -0
  7. package/dist/client/locale.d.ts +7 -0
  8. package/dist/client/pages/TableDetail.d.ts +1 -0
  9. package/dist/client/pages/TableList.d.ts +1 -0
  10. package/dist/client/plugin.d.ts +7 -0
  11. package/dist/externalVersion.js +11 -0
  12. package/dist/index.d.ts +2 -0
  13. package/dist/index.js +39 -0
  14. package/dist/locale/en-US.json +65 -0
  15. package/dist/locale/zh-CN.json +65 -0
  16. package/dist/node_modules/archiver/LICENSE +22 -0
  17. package/dist/node_modules/archiver/index.js +73 -0
  18. package/dist/node_modules/archiver/lib/core.js +974 -0
  19. package/dist/node_modules/archiver/lib/error.js +40 -0
  20. package/dist/node_modules/archiver/lib/plugins/json.js +110 -0
  21. package/dist/node_modules/archiver/lib/plugins/tar.js +167 -0
  22. package/dist/node_modules/archiver/lib/plugins/zip.js +120 -0
  23. package/dist/node_modules/archiver/package.json +1 -0
  24. package/dist/server/adapters/base-adapter.d.ts +63 -0
  25. package/dist/server/adapters/base-adapter.js +31 -0
  26. package/dist/server/adapters/index.d.ts +36 -0
  27. package/dist/server/adapters/index.js +86 -0
  28. package/dist/server/adapters/mysql-adapter.d.ts +13 -0
  29. package/dist/server/adapters/mysql-adapter.js +118 -0
  30. package/dist/server/adapters/postgres-adapter.d.ts +13 -0
  31. package/dist/server/adapters/postgres-adapter.js +115 -0
  32. package/dist/server/adapters/sqlite-adapter.d.ts +13 -0
  33. package/dist/server/adapters/sqlite-adapter.js +115 -0
  34. package/dist/server/collections/.gitkeep +0 -0
  35. package/dist/server/constants.d.ts +4 -0
  36. package/dist/server/constants.js +38 -0
  37. package/dist/server/index.d.ts +1 -0
  38. package/dist/server/index.js +33 -0
  39. package/dist/server/plugin.d.ts +11 -0
  40. package/dist/server/plugin.js +61 -0
  41. package/dist/server/resourcers/database-clean.d.ts +31 -0
  42. package/dist/server/resourcers/database-clean.js +303 -0
  43. package/dist/server/services/database-service.d.ts +46 -0
  44. package/dist/server/services/database-service.js +138 -0
  45. package/dist/server/services/filtered-backup-service.d.ts +40 -0
  46. package/dist/server/services/filtered-backup-service.js +269 -0
  47. package/dist/server/utils/lock.d.ts +23 -0
  48. package/dist/server/utils/lock.js +62 -0
  49. package/dist/server/utils.d.ts +2 -0
  50. package/dist/server/utils.js +43 -0
  51. package/package.json +26 -0
  52. package/server.d.ts +2 -0
  53. 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
+ }