@wlfi-agent/cli 1.4.10 → 1.4.11
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/package.json +2 -8
- package/dist/db/index.cjs +0 -616
- package/dist/db/index.d.cts +0 -3
- package/dist/db/index.d.ts +0 -3
- package/dist/db/index.js +0 -616
package/package.json
CHANGED
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
"@wlf/common": "workspace:*",
|
|
6
6
|
"@wlf/logger": "workspace:*",
|
|
7
7
|
"commander": "catalog:default",
|
|
8
|
-
"execa": "catalog:utils",
|
|
9
8
|
"zod": "catalog:default"
|
|
10
9
|
},
|
|
11
10
|
"devDependencies": {
|
|
@@ -19,13 +18,8 @@
|
|
|
19
18
|
"wlfa": "dist/wlfa/index.js",
|
|
20
19
|
"wlfc": "dist/wlfc/index.js"
|
|
21
20
|
},
|
|
22
|
-
"files": ["dist"],
|
|
21
|
+
"files": ["dist/wlfa", "dist/wlfc"],
|
|
23
22
|
"exports": {
|
|
24
|
-
"./db": {
|
|
25
|
-
"types": "./dist/db/index.d.ts",
|
|
26
|
-
"import": "./dist/db/index.js",
|
|
27
|
-
"require": "./dist/db/index.cjs"
|
|
28
|
-
},
|
|
29
23
|
"./wlfa": {
|
|
30
24
|
"types": "./dist/wlfa/index.d.ts",
|
|
31
25
|
"import": "./dist/wlfa/index.js",
|
|
@@ -52,5 +46,5 @@
|
|
|
52
46
|
"typecheck": "tsc --noEmit"
|
|
53
47
|
},
|
|
54
48
|
"type": "module",
|
|
55
|
-
"version": "1.4.
|
|
49
|
+
"version": "1.4.11"
|
|
56
50
|
}
|
package/dist/db/index.cjs
DELETED
|
@@ -1,616 +0,0 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }// src/db/index.ts
|
|
2
|
-
var _path = require('path'); var _path2 = _interopRequireDefault(_path);
|
|
3
|
-
var _prompts = require('@inquirer/prompts');
|
|
4
|
-
var _execa = require('execa');
|
|
5
|
-
|
|
6
|
-
// src/db/dump.ts
|
|
7
|
-
var _crypto = require('crypto');
|
|
8
|
-
var _promises = require('fs/promises');
|
|
9
|
-
var _os = require('os');
|
|
10
|
-
|
|
11
|
-
var _dayjs = require('@wlf/common/dayjs');
|
|
12
|
-
var _utils = require('@wlf/common/utils');
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
// src/logger/index.ts
|
|
16
|
-
var _logger = require('@wlf/logger');
|
|
17
|
-
var logger = _logger.createLogger.call(void 0, {
|
|
18
|
-
stage: "local"
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
// src/db/dump.ts
|
|
22
|
-
var MAX_BACKUP_FILES = 10;
|
|
23
|
-
function isBackupFile(file) {
|
|
24
|
-
return file.startsWith("backup_") && !file.endsWith(".json");
|
|
25
|
-
}
|
|
26
|
-
async function getLatestAppliedMigration(configPath) {
|
|
27
|
-
const databaseUrl = process.env.DATABASE_URL;
|
|
28
|
-
if (!databaseUrl) {
|
|
29
|
-
throw new Error("DATABASE_URL is not set");
|
|
30
|
-
}
|
|
31
|
-
const dbUrl = new URL(databaseUrl);
|
|
32
|
-
const dbName = dbUrl.pathname.substring(1);
|
|
33
|
-
const dbUser = dbUrl.username;
|
|
34
|
-
const { stdout: containerIdOutput } = await _execa.$`docker ps --filter name=postgres --format {{.ID}}`;
|
|
35
|
-
const containerId = containerIdOutput.trim();
|
|
36
|
-
if (!containerId) {
|
|
37
|
-
throw new Error("PostgreSQL container not found. Is Docker running?");
|
|
38
|
-
}
|
|
39
|
-
const { stdout: queryOutput } = await _execa.$`docker exec ${containerId} sh -c ${`psql -U ${dbUser} -d ${dbName} -t -c "SELECT hash FROM drizzle.__drizzle_migrations ORDER BY created_at DESC LIMIT 1;"`}`;
|
|
40
|
-
const dbHash = queryOutput.trim();
|
|
41
|
-
if (!dbHash) {
|
|
42
|
-
throw new Error("No migrations found in the database");
|
|
43
|
-
}
|
|
44
|
-
const configDir = _path.dirname.call(void 0, configPath);
|
|
45
|
-
const migrationsPath = _path.join.call(void 0, configDir, "migrations");
|
|
46
|
-
const journalPath = _path.join.call(void 0, migrationsPath, "meta/_journal.json");
|
|
47
|
-
const journalContent = await _promises.readFile.call(void 0, journalPath, "utf-8");
|
|
48
|
-
const journal = JSON.parse(journalContent);
|
|
49
|
-
const entries = journal.entries.sort(
|
|
50
|
-
(a, b) => b.idx - a.idx
|
|
51
|
-
);
|
|
52
|
-
for (const entry of entries) {
|
|
53
|
-
const text = await _promises.readFile.call(void 0,
|
|
54
|
-
_path.join.call(void 0, migrationsPath, `${entry.tag}.sql`),
|
|
55
|
-
"utf-8"
|
|
56
|
-
);
|
|
57
|
-
const hash = _crypto.createHash.call(void 0, "sha256").update(text, "utf-8").digest("hex");
|
|
58
|
-
if (hash === dbHash) {
|
|
59
|
-
return { hash, name: entry.tag };
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
throw new Error("No matching migration found");
|
|
63
|
-
}
|
|
64
|
-
function getOptimalWorkerCount() {
|
|
65
|
-
const cpuCount = _os.cpus.call(void 0, ).length;
|
|
66
|
-
const maxWorkers = 8;
|
|
67
|
-
const cpuUtilization = 0.75;
|
|
68
|
-
return Math.max(
|
|
69
|
-
1,
|
|
70
|
-
Math.min(maxWorkers, Math.floor(cpuCount * cpuUtilization))
|
|
71
|
-
);
|
|
72
|
-
}
|
|
73
|
-
async function getBackupDir(project) {
|
|
74
|
-
const baseBackupDir = _path.join.call(void 0, process.cwd(), "backups");
|
|
75
|
-
const backupDir = project ? _path.join.call(void 0, baseBackupDir, project) : baseBackupDir;
|
|
76
|
-
try {
|
|
77
|
-
await _execa.$`mkdir -p ${backupDir}`;
|
|
78
|
-
} catch (error) {
|
|
79
|
-
logger.error({ error }, "Error creating backup directory:");
|
|
80
|
-
}
|
|
81
|
-
return backupDir;
|
|
82
|
-
}
|
|
83
|
-
async function cleanupOldBackups(project) {
|
|
84
|
-
try {
|
|
85
|
-
const backupFiles = await getBackupFiles(project);
|
|
86
|
-
if (backupFiles.length > MAX_BACKUP_FILES) {
|
|
87
|
-
logger.info(
|
|
88
|
-
"\n\u{1F9F9} Cleaning up old backups, keeping the 10 most recent..."
|
|
89
|
-
);
|
|
90
|
-
for (let i = MAX_BACKUP_FILES; i < backupFiles.length; i++) {
|
|
91
|
-
const backupFile = backupFiles[i];
|
|
92
|
-
if (_optionalChain([backupFile, 'optionalAccess', _ => _.path])) {
|
|
93
|
-
await Promise.all([_promises.rm.call(void 0, backupFile.path), _promises.rm.call(void 0, backupFile.meta)]);
|
|
94
|
-
logger.info(` Deleted: ${backupFile.name}`);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
} catch (error) {
|
|
99
|
-
logger.error({ error }, "Error cleaning up old backups:");
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
async function findBackupWithHash(hash, project) {
|
|
103
|
-
try {
|
|
104
|
-
const backupFiles = await getBackupFiles(project);
|
|
105
|
-
for (const backupFile of backupFiles) {
|
|
106
|
-
try {
|
|
107
|
-
const metaFileContent = await _promises.readFile.call(void 0, backupFile.meta, "utf-8");
|
|
108
|
-
const metaContent = JSON.parse(metaFileContent);
|
|
109
|
-
if (metaContent && metaContent.hash === hash) {
|
|
110
|
-
return backupFile;
|
|
111
|
-
}
|
|
112
|
-
} catch (error) {
|
|
113
|
-
logger.warn(
|
|
114
|
-
{ error },
|
|
115
|
-
`Could not read metadata for ${backupFile.name}:`
|
|
116
|
-
);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
return null;
|
|
120
|
-
} catch (error) {
|
|
121
|
-
logger.error({ error }, "Error checking for existing backups:");
|
|
122
|
-
return null;
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
async function createDatabaseBackup(configPath, project) {
|
|
126
|
-
try {
|
|
127
|
-
const backupDir = await getBackupDir(project);
|
|
128
|
-
const databaseUrl = process.env.DATABASE_URL;
|
|
129
|
-
if (!databaseUrl) {
|
|
130
|
-
throw new Error("DATABASE_URL is not set");
|
|
131
|
-
}
|
|
132
|
-
const dbUrl = new URL(databaseUrl);
|
|
133
|
-
const dbName = dbUrl.pathname.substring(1);
|
|
134
|
-
const dbUser = dbUrl.username;
|
|
135
|
-
const { name, hash } = await getLatestAppliedMigration(configPath);
|
|
136
|
-
const existingBackup = await findBackupWithHash(hash, project);
|
|
137
|
-
if (existingBackup) {
|
|
138
|
-
logger.info(
|
|
139
|
-
"\n\u2705 Skipping backup: Database state unchanged since last backup"
|
|
140
|
-
);
|
|
141
|
-
logger.info(` Existing backup: ${existingBackup.name}`);
|
|
142
|
-
return {
|
|
143
|
-
existing: true,
|
|
144
|
-
meta: existingBackup.meta,
|
|
145
|
-
path: existingBackup.path
|
|
146
|
-
};
|
|
147
|
-
}
|
|
148
|
-
const timestamp = _dayjs.dayjs.call(void 0, ).format("YYYYMMDD_HHmmss");
|
|
149
|
-
const backupDirName = `backup_${timestamp}_${name}`;
|
|
150
|
-
const jsonMeta = {
|
|
151
|
-
contents: { hash },
|
|
152
|
-
name: `backup_${timestamp}_${name}.json`
|
|
153
|
-
};
|
|
154
|
-
const backupDirPath = _path.join.call(void 0, backupDir, backupDirName);
|
|
155
|
-
const backupJsonPath = _path.join.call(void 0, backupDir, jsonMeta.name);
|
|
156
|
-
logger.info("\n\u{1F4E6} Creating database backup...");
|
|
157
|
-
const { stdout: containerIdOutput } = await _execa.$`docker ps --filter name=postgres --format {{.ID}}`;
|
|
158
|
-
const containerId = containerIdOutput.trim();
|
|
159
|
-
if (!containerId) {
|
|
160
|
-
throw new Error("PostgreSQL container not found. Is Docker running?");
|
|
161
|
-
}
|
|
162
|
-
const workerCount = getOptimalWorkerCount();
|
|
163
|
-
logger.info(`\u{1F527} Using ${workerCount} workers for backup`);
|
|
164
|
-
const containerBackupPath = "/tmp/db_backup";
|
|
165
|
-
await _execa.$`docker exec ${containerId} sh -c ${`pg_dump -U ${dbUser} -d ${dbName} -F d -j ${workerCount} --no-owner --no-acl -f ${containerBackupPath}`}`;
|
|
166
|
-
await _execa.$`docker cp ${containerId}:${containerBackupPath} ${backupDirPath}`;
|
|
167
|
-
await _execa.$`docker exec ${containerId} rm -rf ${containerBackupPath}`;
|
|
168
|
-
const prettyContent = _utils.prettyPrint.call(void 0, jsonMeta.contents);
|
|
169
|
-
if (prettyContent) {
|
|
170
|
-
await _promises.writeFile.call(void 0, backupJsonPath, prettyContent);
|
|
171
|
-
}
|
|
172
|
-
logger.info(`\u2705 Database backup created at: ${backupDirPath}`);
|
|
173
|
-
await cleanupOldBackups(project);
|
|
174
|
-
return {
|
|
175
|
-
existing: false,
|
|
176
|
-
meta: backupJsonPath,
|
|
177
|
-
path: backupDirPath
|
|
178
|
-
};
|
|
179
|
-
} catch (error) {
|
|
180
|
-
logger.error({ error }, "\n\u274C Error during backup:");
|
|
181
|
-
return null;
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
async function getBackupFiles(project) {
|
|
185
|
-
const backupDir = await getBackupDir(project);
|
|
186
|
-
const files = await _promises.readdir.call(void 0, backupDir);
|
|
187
|
-
return files.filter(isBackupFile).map((file) => {
|
|
188
|
-
const parts = file.replace("backup_", "").split("_");
|
|
189
|
-
const timestamp = parts.length >= 2 ? `${parts.at(0)}_${parts.at(1)}` : parts.at(0) || "";
|
|
190
|
-
return {
|
|
191
|
-
meta: _path.join.call(void 0, backupDir, `${file}.json`),
|
|
192
|
-
name: file,
|
|
193
|
-
path: _path.join.call(void 0, backupDir, file),
|
|
194
|
-
timestamp
|
|
195
|
-
};
|
|
196
|
-
}).sort((a, b) => b.timestamp.localeCompare(a.timestamp));
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// src/db/generate.ts
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
var _zod = require('zod');
|
|
203
|
-
var MIN_MIGRATION_NAME_LENGTH = 3;
|
|
204
|
-
var MAX_MIGRATION_NAME_LENGTH = 50;
|
|
205
|
-
var migrationNameSchema = _zod.z.string().min(
|
|
206
|
-
MIN_MIGRATION_NAME_LENGTH,
|
|
207
|
-
`Migration name must be at least ${MIN_MIGRATION_NAME_LENGTH} characters long`
|
|
208
|
-
).max(
|
|
209
|
-
MAX_MIGRATION_NAME_LENGTH,
|
|
210
|
-
`Migration name must be at most ${MAX_MIGRATION_NAME_LENGTH} characters long`
|
|
211
|
-
).regex(
|
|
212
|
-
/^[a-z0-9_-]+$/,
|
|
213
|
-
"Migration name must only contain lowercase letters, numbers, hyphens, and underscores"
|
|
214
|
-
);
|
|
215
|
-
async function generateMigration(configPath) {
|
|
216
|
-
try {
|
|
217
|
-
const migrationName = await _prompts.input.call(void 0, {
|
|
218
|
-
message: "Enter a name for this migration:",
|
|
219
|
-
validate: (value) => {
|
|
220
|
-
try {
|
|
221
|
-
migrationNameSchema.parse(value);
|
|
222
|
-
return true;
|
|
223
|
-
} catch (error) {
|
|
224
|
-
if (error instanceof _zod.z.ZodError) {
|
|
225
|
-
return error.message || "Invalid migration name";
|
|
226
|
-
}
|
|
227
|
-
return "Invalid migration name";
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
});
|
|
231
|
-
migrationNameSchema.parse(migrationName);
|
|
232
|
-
logger.info(`
|
|
233
|
-
\u{1F4DD} Generating migration: "${migrationName}"...`);
|
|
234
|
-
await _execa.$`drizzle-kit generate --config ${configPath} --name ${migrationName}`;
|
|
235
|
-
return true;
|
|
236
|
-
} catch (error) {
|
|
237
|
-
logger.error({ error }, "\n\u274C Error generating migration:");
|
|
238
|
-
return false;
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
// src/db/migrate.ts
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
async function runMigrations(configPath, project) {
|
|
247
|
-
try {
|
|
248
|
-
const shouldProceed = await _prompts.confirm.call(void 0, {
|
|
249
|
-
default: false,
|
|
250
|
-
message: "Are you sure you want to run migrations? A backup will be created in the backups directory."
|
|
251
|
-
});
|
|
252
|
-
if (!shouldProceed) {
|
|
253
|
-
logger.info("\n\u274C Migration cancelled by user");
|
|
254
|
-
return false;
|
|
255
|
-
}
|
|
256
|
-
const backupPath = await createDatabaseBackup(configPath, project);
|
|
257
|
-
if (!backupPath) {
|
|
258
|
-
logger.error("\n\u274C Database backup failed, aborting migration");
|
|
259
|
-
return false;
|
|
260
|
-
}
|
|
261
|
-
logger.info("\n\u{1F680} Running database migrations...");
|
|
262
|
-
await _execa.$`drizzle-kit migrate --config ${configPath}`;
|
|
263
|
-
logger.info("\n\u2705 Database migrations completed successfully!");
|
|
264
|
-
logger.info(`\u{1F4BE} Backup file is available at: ${_utils.prettyPrint.call(void 0, backupPath)}`);
|
|
265
|
-
logger.info(
|
|
266
|
-
"To restore the database in case of issues, you can use the backup file."
|
|
267
|
-
);
|
|
268
|
-
return true;
|
|
269
|
-
} catch (error) {
|
|
270
|
-
logger.error({ error }, "\n\u274C Error during migration:");
|
|
271
|
-
return false;
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
// src/db/restore.ts
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
function getOptimalWorkerCount2() {
|
|
279
|
-
const cpuCount = _os.cpus.call(void 0, ).length;
|
|
280
|
-
const maxWorkers = 8;
|
|
281
|
-
const cpuUtilization = 0.75;
|
|
282
|
-
return Math.max(
|
|
283
|
-
1,
|
|
284
|
-
Math.min(maxWorkers, Math.floor(cpuCount * cpuUtilization))
|
|
285
|
-
);
|
|
286
|
-
}
|
|
287
|
-
async function restoreDatabase(backupFilePath, {
|
|
288
|
-
silent = false
|
|
289
|
-
} = {}) {
|
|
290
|
-
const databaseUrl = process.env.DATABASE_URL;
|
|
291
|
-
if (!databaseUrl) {
|
|
292
|
-
throw new Error("DATABASE_URL is not set");
|
|
293
|
-
}
|
|
294
|
-
const dbUrl = new URL(databaseUrl);
|
|
295
|
-
const dbName = dbUrl.pathname.substring(1);
|
|
296
|
-
const dbUser = dbUrl.username;
|
|
297
|
-
if (!silent) {
|
|
298
|
-
logger.info(`
|
|
299
|
-
\u{1F504} Restoring database from backup: ${backupFilePath}`);
|
|
300
|
-
}
|
|
301
|
-
const { stdout: containerIdOutput } = await _execa.$`docker ps --filter name=postgres --format {{.ID}}`;
|
|
302
|
-
const containerId = containerIdOutput.trim();
|
|
303
|
-
if (!containerId) {
|
|
304
|
-
throw new Error("PostgreSQL container not found. Is Docker running?");
|
|
305
|
-
}
|
|
306
|
-
const containerTempPath = "/tmp/db_restore";
|
|
307
|
-
await _execa.$`docker cp ${backupFilePath} ${containerId}:${containerTempPath}`;
|
|
308
|
-
if (!silent) {
|
|
309
|
-
logger.info("\u{1F4CB} Copied backup directory to container");
|
|
310
|
-
}
|
|
311
|
-
if (!silent) {
|
|
312
|
-
logger.info("\u{1F50C} Terminating all connections to the database...");
|
|
313
|
-
}
|
|
314
|
-
try {
|
|
315
|
-
await _execa.$`docker exec ${containerId} sh -c ${`psql -U ${dbUser} -d postgres -c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '${dbName}' AND pid <> pg_backend_pid();"`}`;
|
|
316
|
-
} catch (error) {
|
|
317
|
-
logger.warn(`Warning when terminating connections: ${error}`);
|
|
318
|
-
}
|
|
319
|
-
if (!silent) {
|
|
320
|
-
logger.info("\u{1F5D1}\uFE0F Dropping database...");
|
|
321
|
-
}
|
|
322
|
-
await _execa.$`docker exec ${containerId} sh -c ${`dropdb -U ${dbUser} --if-exists ${dbName}`}`;
|
|
323
|
-
if (!silent) {
|
|
324
|
-
logger.info("\u{1F195} Recreating database...");
|
|
325
|
-
}
|
|
326
|
-
await _execa.$`docker exec ${containerId} sh -c ${`createdb -U ${dbUser} ${dbName}`}`;
|
|
327
|
-
if (!silent) {
|
|
328
|
-
logger.info("\u{1F4E5} Restoring database from backup...");
|
|
329
|
-
}
|
|
330
|
-
const workerCount = getOptimalWorkerCount2();
|
|
331
|
-
if (!silent) {
|
|
332
|
-
logger.info(`\u{1F527} Using ${workerCount} workers for restore`);
|
|
333
|
-
}
|
|
334
|
-
await _execa.$`docker exec ${containerId} sh -c ${`pg_restore -U ${dbUser} -d ${dbName} -j ${workerCount} --no-owner --no-acl ${containerTempPath}`}`;
|
|
335
|
-
await _execa.$`docker exec ${containerId} rm -rf ${containerTempPath}`;
|
|
336
|
-
if (!silent) {
|
|
337
|
-
logger.info("\u2705 Database restore completed successfully");
|
|
338
|
-
}
|
|
339
|
-
return true;
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
// src/db/index.ts
|
|
343
|
-
var NODE_PROCESS_ARGS_OFFSET = 2;
|
|
344
|
-
var TIME_REGEX = /(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/;
|
|
345
|
-
function parseConfigPath() {
|
|
346
|
-
const args = process.argv.slice(NODE_PROCESS_ARGS_OFFSET);
|
|
347
|
-
const configFlagIndex = args.indexOf("--config");
|
|
348
|
-
if (configFlagIndex !== -1 && args.length > configFlagIndex + 1) {
|
|
349
|
-
const configValue = args[configFlagIndex + 1];
|
|
350
|
-
if (configValue !== void 0) {
|
|
351
|
-
return configValue;
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
throw new Error(
|
|
355
|
-
"Missing required --config flag with path to drizzle.ts config file"
|
|
356
|
-
);
|
|
357
|
-
}
|
|
358
|
-
function parseProjectFlag() {
|
|
359
|
-
const args = process.argv.slice(NODE_PROCESS_ARGS_OFFSET);
|
|
360
|
-
const projectFlagIndex = args.indexOf("--project");
|
|
361
|
-
if (projectFlagIndex !== -1 && args.length > projectFlagIndex + 1) {
|
|
362
|
-
const projectValue = args[projectFlagIndex + 1];
|
|
363
|
-
if (projectValue !== void 0) {
|
|
364
|
-
if (projectValue === "api" || projectValue === "indexer" || projectValue === "forum") {
|
|
365
|
-
return projectValue;
|
|
366
|
-
}
|
|
367
|
-
throw new Error(
|
|
368
|
-
'Invalid project value. Must be "api", "indexer", or "forum"'
|
|
369
|
-
);
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
throw new Error(
|
|
373
|
-
'Missing required --project flag. Must be "api", "indexer", or "forum"'
|
|
374
|
-
);
|
|
375
|
-
}
|
|
376
|
-
async function handleBackup(configPath, project) {
|
|
377
|
-
logger.info("\n\u{1F4E6} Database Backup");
|
|
378
|
-
logger.info("----------------");
|
|
379
|
-
try {
|
|
380
|
-
const confirmBackup = await _prompts.select.call(void 0, {
|
|
381
|
-
choices: [
|
|
382
|
-
{ name: "Yes, create backup", value: "yes" },
|
|
383
|
-
{ name: "No, cancel", value: "no" }
|
|
384
|
-
],
|
|
385
|
-
message: "Are you sure you want to create a database backup?"
|
|
386
|
-
});
|
|
387
|
-
if (confirmBackup === "no") {
|
|
388
|
-
logger.info("\n\u274C Backup cancelled");
|
|
389
|
-
return;
|
|
390
|
-
}
|
|
391
|
-
const backup = await createDatabaseBackup(configPath, project);
|
|
392
|
-
if (backup) {
|
|
393
|
-
if (backup.existing) {
|
|
394
|
-
logger.info(
|
|
395
|
-
"\n\u2705 Database backup completed successfully! (Skipping duplicate)"
|
|
396
|
-
);
|
|
397
|
-
} else {
|
|
398
|
-
logger.info("\n\u2705 Database backup completed successfully!");
|
|
399
|
-
}
|
|
400
|
-
logger.info(`\u{1F4BE} Backup file is available at: ${backup.path}`);
|
|
401
|
-
logger.info(`\u{1F4BE} Backup metadata is available at: ${backup.meta}`);
|
|
402
|
-
} else {
|
|
403
|
-
logger.error("\n\u274C Database backup failed");
|
|
404
|
-
process.exit(1);
|
|
405
|
-
}
|
|
406
|
-
} catch (error) {
|
|
407
|
-
logger.error({ error }, "\n\u274C Error during backup:");
|
|
408
|
-
process.exit(1);
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
async function handleRestore(project) {
|
|
412
|
-
logger.info("\n\u{1F504} Database Restore");
|
|
413
|
-
logger.info("-----------------");
|
|
414
|
-
try {
|
|
415
|
-
const backups = await getBackupFiles(project);
|
|
416
|
-
if (backups.length === 0) {
|
|
417
|
-
logger.info("\n\u274C No backup files found in the backups directory");
|
|
418
|
-
return;
|
|
419
|
-
}
|
|
420
|
-
const choices = backups.map((backup) => {
|
|
421
|
-
const timestamp = backup.timestamp.replace("_", " ").replace(TIME_REGEX, "$1-$2-$3 $4:$5:$6");
|
|
422
|
-
return {
|
|
423
|
-
name: `${timestamp} - ${backup.name}`,
|
|
424
|
-
value: backup.path
|
|
425
|
-
};
|
|
426
|
-
});
|
|
427
|
-
choices.push({
|
|
428
|
-
name: "Cancel restore operation",
|
|
429
|
-
value: "cancel"
|
|
430
|
-
});
|
|
431
|
-
const selectedBackup = await _prompts.select.call(void 0, {
|
|
432
|
-
choices,
|
|
433
|
-
message: "Select a backup to restore:"
|
|
434
|
-
});
|
|
435
|
-
if (selectedBackup === "cancel") {
|
|
436
|
-
logger.info("\n\u274C Restore cancelled");
|
|
437
|
-
return;
|
|
438
|
-
}
|
|
439
|
-
logger.info(
|
|
440
|
-
"\n\u26A0\uFE0F WARNING: This will overwrite your current database with the selected backup."
|
|
441
|
-
);
|
|
442
|
-
logger.info("\u26A0\uFE0F Make sure you have a recent backup before proceeding.");
|
|
443
|
-
const confirmRestore = await _prompts.select.call(void 0, {
|
|
444
|
-
choices: [
|
|
445
|
-
{ name: "Yes, restore the database", value: "yes" },
|
|
446
|
-
{ name: "No, cancel the operation", value: "no" }
|
|
447
|
-
],
|
|
448
|
-
message: "Are you sure you want to proceed with the restore?"
|
|
449
|
-
});
|
|
450
|
-
if (confirmRestore === "no") {
|
|
451
|
-
logger.info("\n\u274C Restore cancelled");
|
|
452
|
-
return;
|
|
453
|
-
}
|
|
454
|
-
const success = await restoreDatabase(selectedBackup);
|
|
455
|
-
if (success) {
|
|
456
|
-
logger.info("\n\u2705 Database restore completed successfully!");
|
|
457
|
-
} else {
|
|
458
|
-
logger.error("\n\u274C Database restore failed");
|
|
459
|
-
process.exit(1);
|
|
460
|
-
}
|
|
461
|
-
} catch (error) {
|
|
462
|
-
logger.error({ error }, "\n\u274C Error during restore:");
|
|
463
|
-
process.exit(1);
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
async function handleMigrate(configPath, project) {
|
|
467
|
-
logger.info("\n\u{1F504} Database Migration");
|
|
468
|
-
logger.info("------------------");
|
|
469
|
-
try {
|
|
470
|
-
const confirmMigrate = await _prompts.select.call(void 0, {
|
|
471
|
-
choices: [
|
|
472
|
-
{ name: "Yes, run migrations", value: "yes" },
|
|
473
|
-
{ name: "No, cancel", value: "no" }
|
|
474
|
-
],
|
|
475
|
-
message: "Are you sure you want to run database migrations?"
|
|
476
|
-
});
|
|
477
|
-
if (confirmMigrate === "no") {
|
|
478
|
-
logger.info("\n\u274C Migration cancelled");
|
|
479
|
-
return;
|
|
480
|
-
}
|
|
481
|
-
logger.info("Running migrations...");
|
|
482
|
-
const success = await runMigrations(configPath, project);
|
|
483
|
-
if (success) {
|
|
484
|
-
logger.info("\n\u2705 Database migrations completed successfully!");
|
|
485
|
-
} else {
|
|
486
|
-
logger.error("\n\u274C Database migrations failed");
|
|
487
|
-
process.exit(1);
|
|
488
|
-
}
|
|
489
|
-
} catch (error) {
|
|
490
|
-
logger.error({ error }, "\n\u274C Error during migration:");
|
|
491
|
-
process.exit(1);
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
async function handleGenerate(configPath) {
|
|
495
|
-
logger.info("\n\u{1F504} Generate Migration");
|
|
496
|
-
logger.info("-------------------");
|
|
497
|
-
try {
|
|
498
|
-
const confirmGenerate = await _prompts.select.call(void 0, {
|
|
499
|
-
choices: [
|
|
500
|
-
{ name: "Yes, generate migration", value: "yes" },
|
|
501
|
-
{ name: "No, cancel", value: "no" }
|
|
502
|
-
],
|
|
503
|
-
message: "Are you sure you want to generate a migration?"
|
|
504
|
-
});
|
|
505
|
-
if (confirmGenerate === "no") {
|
|
506
|
-
logger.info("\n\u274C Generation cancelled");
|
|
507
|
-
return;
|
|
508
|
-
}
|
|
509
|
-
logger.info("Generating migration...");
|
|
510
|
-
const success = await generateMigration(configPath);
|
|
511
|
-
if (success) {
|
|
512
|
-
logger.info("\n\u2705 Migration generation completed successfully!");
|
|
513
|
-
} else {
|
|
514
|
-
logger.error("\n\u274C Migration generation failed");
|
|
515
|
-
process.exit(1);
|
|
516
|
-
}
|
|
517
|
-
} catch (error) {
|
|
518
|
-
logger.error({ error }, "\n\u274C Error during migration generation:");
|
|
519
|
-
process.exit(1);
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
function handleStudio(configPath) {
|
|
523
|
-
logger.info("\n\u{1F50D} Launching Drizzle Studio");
|
|
524
|
-
logger.info("------------------------");
|
|
525
|
-
try {
|
|
526
|
-
logger.info("Starting Drizzle Studio...");
|
|
527
|
-
logger.info(
|
|
528
|
-
"This will open a web interface to explore and manage your database."
|
|
529
|
-
);
|
|
530
|
-
logger.info("Press Ctrl+C to stop the server when you're done.");
|
|
531
|
-
const studio = new Promise((resolve, reject) => {
|
|
532
|
-
void _execa.execa.call(void 0, "pnpm", ["drizzle-kit", "studio", "--config", configPath], {
|
|
533
|
-
stdio: "inherit"
|
|
534
|
-
}).on("exit", () => {
|
|
535
|
-
logger.info("\n\u2705 Drizzle Studio session ended");
|
|
536
|
-
resolve(void 0);
|
|
537
|
-
}).on("error", (error) => {
|
|
538
|
-
reject(error);
|
|
539
|
-
});
|
|
540
|
-
});
|
|
541
|
-
process.on("SIGINT", () => {
|
|
542
|
-
void studio.then(() => {
|
|
543
|
-
process.exit(0);
|
|
544
|
-
});
|
|
545
|
-
});
|
|
546
|
-
} catch (error) {
|
|
547
|
-
logger.error({ error }, "\n\u274C Error launching Drizzle Studio:");
|
|
548
|
-
process.exit(1);
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
|
-
async function commandLine() {
|
|
552
|
-
const configPath = parseConfigPath();
|
|
553
|
-
const project = parseProjectFlag();
|
|
554
|
-
const resolvedConfigPath = _path2.default.isAbsolute(configPath) ? configPath : _path2.default.resolve(process.cwd(), configPath);
|
|
555
|
-
logger.info("\u{1F6E0}\uFE0F Database Management CLI");
|
|
556
|
-
logger.info("==========================");
|
|
557
|
-
logger.info(`Using config: ${resolvedConfigPath}`);
|
|
558
|
-
logger.info(`Project: ${project}`);
|
|
559
|
-
const operations = [
|
|
560
|
-
{
|
|
561
|
-
name: "Backup - Create a database backup",
|
|
562
|
-
value: "backup"
|
|
563
|
-
},
|
|
564
|
-
{
|
|
565
|
-
name: "Restore - Restore database from a backup",
|
|
566
|
-
value: "restore"
|
|
567
|
-
},
|
|
568
|
-
{
|
|
569
|
-
name: "Migrate - Run database migrations",
|
|
570
|
-
value: "migrate"
|
|
571
|
-
},
|
|
572
|
-
{
|
|
573
|
-
name: "Generate - Generate migration",
|
|
574
|
-
value: "generate"
|
|
575
|
-
},
|
|
576
|
-
{
|
|
577
|
-
name: "Studio - Launch Drizzle Studio",
|
|
578
|
-
value: "studio"
|
|
579
|
-
},
|
|
580
|
-
{
|
|
581
|
-
name: "Exit",
|
|
582
|
-
value: "exit"
|
|
583
|
-
}
|
|
584
|
-
];
|
|
585
|
-
const selectedOperation = await _prompts.select.call(void 0, {
|
|
586
|
-
choices: operations,
|
|
587
|
-
message: "Select an operation:"
|
|
588
|
-
});
|
|
589
|
-
switch (selectedOperation) {
|
|
590
|
-
case "backup":
|
|
591
|
-
await handleBackup(resolvedConfigPath, project);
|
|
592
|
-
break;
|
|
593
|
-
case "restore":
|
|
594
|
-
await handleRestore(project);
|
|
595
|
-
break;
|
|
596
|
-
case "migrate":
|
|
597
|
-
await handleMigrate(resolvedConfigPath, project);
|
|
598
|
-
break;
|
|
599
|
-
case "generate":
|
|
600
|
-
await handleGenerate(resolvedConfigPath);
|
|
601
|
-
break;
|
|
602
|
-
case "studio":
|
|
603
|
-
handleStudio(resolvedConfigPath);
|
|
604
|
-
break;
|
|
605
|
-
case "exit":
|
|
606
|
-
logger.info("\n\u{1F44B} Exiting...");
|
|
607
|
-
return process.exit(0);
|
|
608
|
-
default:
|
|
609
|
-
logger.error("\n\u274C Invalid operation");
|
|
610
|
-
return process.exit(1);
|
|
611
|
-
}
|
|
612
|
-
return process.exit(0);
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
exports.default = commandLine;
|
package/dist/db/index.d.cts
DELETED
package/dist/db/index.d.ts
DELETED
package/dist/db/index.js
DELETED
|
@@ -1,616 +0,0 @@
|
|
|
1
|
-
// src/db/index.ts
|
|
2
|
-
import path from "path";
|
|
3
|
-
import { select } from "@inquirer/prompts";
|
|
4
|
-
import { execa } from "execa";
|
|
5
|
-
|
|
6
|
-
// src/db/dump.ts
|
|
7
|
-
import { createHash } from "crypto";
|
|
8
|
-
import { readdir, readFile, rm, writeFile } from "fs/promises";
|
|
9
|
-
import { cpus } from "os";
|
|
10
|
-
import { dirname, join } from "path";
|
|
11
|
-
import { dayjs } from "@wlf/common/dayjs";
|
|
12
|
-
import { prettyPrint } from "@wlf/common/utils";
|
|
13
|
-
import { $ } from "execa";
|
|
14
|
-
|
|
15
|
-
// src/logger/index.ts
|
|
16
|
-
import { createLogger } from "@wlf/logger";
|
|
17
|
-
var logger = createLogger({
|
|
18
|
-
stage: "local"
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
// src/db/dump.ts
|
|
22
|
-
var MAX_BACKUP_FILES = 10;
|
|
23
|
-
function isBackupFile(file) {
|
|
24
|
-
return file.startsWith("backup_") && !file.endsWith(".json");
|
|
25
|
-
}
|
|
26
|
-
async function getLatestAppliedMigration(configPath) {
|
|
27
|
-
const databaseUrl = process.env.DATABASE_URL;
|
|
28
|
-
if (!databaseUrl) {
|
|
29
|
-
throw new Error("DATABASE_URL is not set");
|
|
30
|
-
}
|
|
31
|
-
const dbUrl = new URL(databaseUrl);
|
|
32
|
-
const dbName = dbUrl.pathname.substring(1);
|
|
33
|
-
const dbUser = dbUrl.username;
|
|
34
|
-
const { stdout: containerIdOutput } = await $`docker ps --filter name=postgres --format {{.ID}}`;
|
|
35
|
-
const containerId = containerIdOutput.trim();
|
|
36
|
-
if (!containerId) {
|
|
37
|
-
throw new Error("PostgreSQL container not found. Is Docker running?");
|
|
38
|
-
}
|
|
39
|
-
const { stdout: queryOutput } = await $`docker exec ${containerId} sh -c ${`psql -U ${dbUser} -d ${dbName} -t -c "SELECT hash FROM drizzle.__drizzle_migrations ORDER BY created_at DESC LIMIT 1;"`}`;
|
|
40
|
-
const dbHash = queryOutput.trim();
|
|
41
|
-
if (!dbHash) {
|
|
42
|
-
throw new Error("No migrations found in the database");
|
|
43
|
-
}
|
|
44
|
-
const configDir = dirname(configPath);
|
|
45
|
-
const migrationsPath = join(configDir, "migrations");
|
|
46
|
-
const journalPath = join(migrationsPath, "meta/_journal.json");
|
|
47
|
-
const journalContent = await readFile(journalPath, "utf-8");
|
|
48
|
-
const journal = JSON.parse(journalContent);
|
|
49
|
-
const entries = journal.entries.sort(
|
|
50
|
-
(a, b) => b.idx - a.idx
|
|
51
|
-
);
|
|
52
|
-
for (const entry of entries) {
|
|
53
|
-
const text = await readFile(
|
|
54
|
-
join(migrationsPath, `${entry.tag}.sql`),
|
|
55
|
-
"utf-8"
|
|
56
|
-
);
|
|
57
|
-
const hash = createHash("sha256").update(text, "utf-8").digest("hex");
|
|
58
|
-
if (hash === dbHash) {
|
|
59
|
-
return { hash, name: entry.tag };
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
throw new Error("No matching migration found");
|
|
63
|
-
}
|
|
64
|
-
function getOptimalWorkerCount() {
|
|
65
|
-
const cpuCount = cpus().length;
|
|
66
|
-
const maxWorkers = 8;
|
|
67
|
-
const cpuUtilization = 0.75;
|
|
68
|
-
return Math.max(
|
|
69
|
-
1,
|
|
70
|
-
Math.min(maxWorkers, Math.floor(cpuCount * cpuUtilization))
|
|
71
|
-
);
|
|
72
|
-
}
|
|
73
|
-
async function getBackupDir(project) {
|
|
74
|
-
const baseBackupDir = join(process.cwd(), "backups");
|
|
75
|
-
const backupDir = project ? join(baseBackupDir, project) : baseBackupDir;
|
|
76
|
-
try {
|
|
77
|
-
await $`mkdir -p ${backupDir}`;
|
|
78
|
-
} catch (error) {
|
|
79
|
-
logger.error({ error }, "Error creating backup directory:");
|
|
80
|
-
}
|
|
81
|
-
return backupDir;
|
|
82
|
-
}
|
|
83
|
-
async function cleanupOldBackups(project) {
|
|
84
|
-
try {
|
|
85
|
-
const backupFiles = await getBackupFiles(project);
|
|
86
|
-
if (backupFiles.length > MAX_BACKUP_FILES) {
|
|
87
|
-
logger.info(
|
|
88
|
-
"\n\u{1F9F9} Cleaning up old backups, keeping the 10 most recent..."
|
|
89
|
-
);
|
|
90
|
-
for (let i = MAX_BACKUP_FILES; i < backupFiles.length; i++) {
|
|
91
|
-
const backupFile = backupFiles[i];
|
|
92
|
-
if (backupFile?.path) {
|
|
93
|
-
await Promise.all([rm(backupFile.path), rm(backupFile.meta)]);
|
|
94
|
-
logger.info(` Deleted: ${backupFile.name}`);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
} catch (error) {
|
|
99
|
-
logger.error({ error }, "Error cleaning up old backups:");
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
async function findBackupWithHash(hash, project) {
|
|
103
|
-
try {
|
|
104
|
-
const backupFiles = await getBackupFiles(project);
|
|
105
|
-
for (const backupFile of backupFiles) {
|
|
106
|
-
try {
|
|
107
|
-
const metaFileContent = await readFile(backupFile.meta, "utf-8");
|
|
108
|
-
const metaContent = JSON.parse(metaFileContent);
|
|
109
|
-
if (metaContent && metaContent.hash === hash) {
|
|
110
|
-
return backupFile;
|
|
111
|
-
}
|
|
112
|
-
} catch (error) {
|
|
113
|
-
logger.warn(
|
|
114
|
-
{ error },
|
|
115
|
-
`Could not read metadata for ${backupFile.name}:`
|
|
116
|
-
);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
return null;
|
|
120
|
-
} catch (error) {
|
|
121
|
-
logger.error({ error }, "Error checking for existing backups:");
|
|
122
|
-
return null;
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
async function createDatabaseBackup(configPath, project) {
|
|
126
|
-
try {
|
|
127
|
-
const backupDir = await getBackupDir(project);
|
|
128
|
-
const databaseUrl = process.env.DATABASE_URL;
|
|
129
|
-
if (!databaseUrl) {
|
|
130
|
-
throw new Error("DATABASE_URL is not set");
|
|
131
|
-
}
|
|
132
|
-
const dbUrl = new URL(databaseUrl);
|
|
133
|
-
const dbName = dbUrl.pathname.substring(1);
|
|
134
|
-
const dbUser = dbUrl.username;
|
|
135
|
-
const { name, hash } = await getLatestAppliedMigration(configPath);
|
|
136
|
-
const existingBackup = await findBackupWithHash(hash, project);
|
|
137
|
-
if (existingBackup) {
|
|
138
|
-
logger.info(
|
|
139
|
-
"\n\u2705 Skipping backup: Database state unchanged since last backup"
|
|
140
|
-
);
|
|
141
|
-
logger.info(` Existing backup: ${existingBackup.name}`);
|
|
142
|
-
return {
|
|
143
|
-
existing: true,
|
|
144
|
-
meta: existingBackup.meta,
|
|
145
|
-
path: existingBackup.path
|
|
146
|
-
};
|
|
147
|
-
}
|
|
148
|
-
const timestamp = dayjs().format("YYYYMMDD_HHmmss");
|
|
149
|
-
const backupDirName = `backup_${timestamp}_${name}`;
|
|
150
|
-
const jsonMeta = {
|
|
151
|
-
contents: { hash },
|
|
152
|
-
name: `backup_${timestamp}_${name}.json`
|
|
153
|
-
};
|
|
154
|
-
const backupDirPath = join(backupDir, backupDirName);
|
|
155
|
-
const backupJsonPath = join(backupDir, jsonMeta.name);
|
|
156
|
-
logger.info("\n\u{1F4E6} Creating database backup...");
|
|
157
|
-
const { stdout: containerIdOutput } = await $`docker ps --filter name=postgres --format {{.ID}}`;
|
|
158
|
-
const containerId = containerIdOutput.trim();
|
|
159
|
-
if (!containerId) {
|
|
160
|
-
throw new Error("PostgreSQL container not found. Is Docker running?");
|
|
161
|
-
}
|
|
162
|
-
const workerCount = getOptimalWorkerCount();
|
|
163
|
-
logger.info(`\u{1F527} Using ${workerCount} workers for backup`);
|
|
164
|
-
const containerBackupPath = "/tmp/db_backup";
|
|
165
|
-
await $`docker exec ${containerId} sh -c ${`pg_dump -U ${dbUser} -d ${dbName} -F d -j ${workerCount} --no-owner --no-acl -f ${containerBackupPath}`}`;
|
|
166
|
-
await $`docker cp ${containerId}:${containerBackupPath} ${backupDirPath}`;
|
|
167
|
-
await $`docker exec ${containerId} rm -rf ${containerBackupPath}`;
|
|
168
|
-
const prettyContent = prettyPrint(jsonMeta.contents);
|
|
169
|
-
if (prettyContent) {
|
|
170
|
-
await writeFile(backupJsonPath, prettyContent);
|
|
171
|
-
}
|
|
172
|
-
logger.info(`\u2705 Database backup created at: ${backupDirPath}`);
|
|
173
|
-
await cleanupOldBackups(project);
|
|
174
|
-
return {
|
|
175
|
-
existing: false,
|
|
176
|
-
meta: backupJsonPath,
|
|
177
|
-
path: backupDirPath
|
|
178
|
-
};
|
|
179
|
-
} catch (error) {
|
|
180
|
-
logger.error({ error }, "\n\u274C Error during backup:");
|
|
181
|
-
return null;
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
async function getBackupFiles(project) {
|
|
185
|
-
const backupDir = await getBackupDir(project);
|
|
186
|
-
const files = await readdir(backupDir);
|
|
187
|
-
return files.filter(isBackupFile).map((file) => {
|
|
188
|
-
const parts = file.replace("backup_", "").split("_");
|
|
189
|
-
const timestamp = parts.length >= 2 ? `${parts.at(0)}_${parts.at(1)}` : parts.at(0) || "";
|
|
190
|
-
return {
|
|
191
|
-
meta: join(backupDir, `${file}.json`),
|
|
192
|
-
name: file,
|
|
193
|
-
path: join(backupDir, file),
|
|
194
|
-
timestamp
|
|
195
|
-
};
|
|
196
|
-
}).sort((a, b) => b.timestamp.localeCompare(a.timestamp));
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// src/db/generate.ts
|
|
200
|
-
import { input } from "@inquirer/prompts";
|
|
201
|
-
import { $ as $2 } from "execa";
|
|
202
|
-
import { z } from "zod";
|
|
203
|
-
var MIN_MIGRATION_NAME_LENGTH = 3;
|
|
204
|
-
var MAX_MIGRATION_NAME_LENGTH = 50;
|
|
205
|
-
var migrationNameSchema = z.string().min(
|
|
206
|
-
MIN_MIGRATION_NAME_LENGTH,
|
|
207
|
-
`Migration name must be at least ${MIN_MIGRATION_NAME_LENGTH} characters long`
|
|
208
|
-
).max(
|
|
209
|
-
MAX_MIGRATION_NAME_LENGTH,
|
|
210
|
-
`Migration name must be at most ${MAX_MIGRATION_NAME_LENGTH} characters long`
|
|
211
|
-
).regex(
|
|
212
|
-
/^[a-z0-9_-]+$/,
|
|
213
|
-
"Migration name must only contain lowercase letters, numbers, hyphens, and underscores"
|
|
214
|
-
);
|
|
215
|
-
async function generateMigration(configPath) {
|
|
216
|
-
try {
|
|
217
|
-
const migrationName = await input({
|
|
218
|
-
message: "Enter a name for this migration:",
|
|
219
|
-
validate: (value) => {
|
|
220
|
-
try {
|
|
221
|
-
migrationNameSchema.parse(value);
|
|
222
|
-
return true;
|
|
223
|
-
} catch (error) {
|
|
224
|
-
if (error instanceof z.ZodError) {
|
|
225
|
-
return error.message || "Invalid migration name";
|
|
226
|
-
}
|
|
227
|
-
return "Invalid migration name";
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
});
|
|
231
|
-
migrationNameSchema.parse(migrationName);
|
|
232
|
-
logger.info(`
|
|
233
|
-
\u{1F4DD} Generating migration: "${migrationName}"...`);
|
|
234
|
-
await $2`drizzle-kit generate --config ${configPath} --name ${migrationName}`;
|
|
235
|
-
return true;
|
|
236
|
-
} catch (error) {
|
|
237
|
-
logger.error({ error }, "\n\u274C Error generating migration:");
|
|
238
|
-
return false;
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
// src/db/migrate.ts
|
|
243
|
-
import { confirm } from "@inquirer/prompts";
|
|
244
|
-
import { prettyPrint as prettyPrint2 } from "@wlf/common/utils";
|
|
245
|
-
import { $ as $3 } from "execa";
|
|
246
|
-
async function runMigrations(configPath, project) {
|
|
247
|
-
try {
|
|
248
|
-
const shouldProceed = await confirm({
|
|
249
|
-
default: false,
|
|
250
|
-
message: "Are you sure you want to run migrations? A backup will be created in the backups directory."
|
|
251
|
-
});
|
|
252
|
-
if (!shouldProceed) {
|
|
253
|
-
logger.info("\n\u274C Migration cancelled by user");
|
|
254
|
-
return false;
|
|
255
|
-
}
|
|
256
|
-
const backupPath = await createDatabaseBackup(configPath, project);
|
|
257
|
-
if (!backupPath) {
|
|
258
|
-
logger.error("\n\u274C Database backup failed, aborting migration");
|
|
259
|
-
return false;
|
|
260
|
-
}
|
|
261
|
-
logger.info("\n\u{1F680} Running database migrations...");
|
|
262
|
-
await $3`drizzle-kit migrate --config ${configPath}`;
|
|
263
|
-
logger.info("\n\u2705 Database migrations completed successfully!");
|
|
264
|
-
logger.info(`\u{1F4BE} Backup file is available at: ${prettyPrint2(backupPath)}`);
|
|
265
|
-
logger.info(
|
|
266
|
-
"To restore the database in case of issues, you can use the backup file."
|
|
267
|
-
);
|
|
268
|
-
return true;
|
|
269
|
-
} catch (error) {
|
|
270
|
-
logger.error({ error }, "\n\u274C Error during migration:");
|
|
271
|
-
return false;
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
// src/db/restore.ts
|
|
276
|
-
import { cpus as cpus2 } from "os";
|
|
277
|
-
import { $ as $4 } from "execa";
|
|
278
|
-
function getOptimalWorkerCount2() {
|
|
279
|
-
const cpuCount = cpus2().length;
|
|
280
|
-
const maxWorkers = 8;
|
|
281
|
-
const cpuUtilization = 0.75;
|
|
282
|
-
return Math.max(
|
|
283
|
-
1,
|
|
284
|
-
Math.min(maxWorkers, Math.floor(cpuCount * cpuUtilization))
|
|
285
|
-
);
|
|
286
|
-
}
|
|
287
|
-
async function restoreDatabase(backupFilePath, {
|
|
288
|
-
silent = false
|
|
289
|
-
} = {}) {
|
|
290
|
-
const databaseUrl = process.env.DATABASE_URL;
|
|
291
|
-
if (!databaseUrl) {
|
|
292
|
-
throw new Error("DATABASE_URL is not set");
|
|
293
|
-
}
|
|
294
|
-
const dbUrl = new URL(databaseUrl);
|
|
295
|
-
const dbName = dbUrl.pathname.substring(1);
|
|
296
|
-
const dbUser = dbUrl.username;
|
|
297
|
-
if (!silent) {
|
|
298
|
-
logger.info(`
|
|
299
|
-
\u{1F504} Restoring database from backup: ${backupFilePath}`);
|
|
300
|
-
}
|
|
301
|
-
const { stdout: containerIdOutput } = await $4`docker ps --filter name=postgres --format {{.ID}}`;
|
|
302
|
-
const containerId = containerIdOutput.trim();
|
|
303
|
-
if (!containerId) {
|
|
304
|
-
throw new Error("PostgreSQL container not found. Is Docker running?");
|
|
305
|
-
}
|
|
306
|
-
const containerTempPath = "/tmp/db_restore";
|
|
307
|
-
await $4`docker cp ${backupFilePath} ${containerId}:${containerTempPath}`;
|
|
308
|
-
if (!silent) {
|
|
309
|
-
logger.info("\u{1F4CB} Copied backup directory to container");
|
|
310
|
-
}
|
|
311
|
-
if (!silent) {
|
|
312
|
-
logger.info("\u{1F50C} Terminating all connections to the database...");
|
|
313
|
-
}
|
|
314
|
-
try {
|
|
315
|
-
await $4`docker exec ${containerId} sh -c ${`psql -U ${dbUser} -d postgres -c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '${dbName}' AND pid <> pg_backend_pid();"`}`;
|
|
316
|
-
} catch (error) {
|
|
317
|
-
logger.warn(`Warning when terminating connections: ${error}`);
|
|
318
|
-
}
|
|
319
|
-
if (!silent) {
|
|
320
|
-
logger.info("\u{1F5D1}\uFE0F Dropping database...");
|
|
321
|
-
}
|
|
322
|
-
await $4`docker exec ${containerId} sh -c ${`dropdb -U ${dbUser} --if-exists ${dbName}`}`;
|
|
323
|
-
if (!silent) {
|
|
324
|
-
logger.info("\u{1F195} Recreating database...");
|
|
325
|
-
}
|
|
326
|
-
await $4`docker exec ${containerId} sh -c ${`createdb -U ${dbUser} ${dbName}`}`;
|
|
327
|
-
if (!silent) {
|
|
328
|
-
logger.info("\u{1F4E5} Restoring database from backup...");
|
|
329
|
-
}
|
|
330
|
-
const workerCount = getOptimalWorkerCount2();
|
|
331
|
-
if (!silent) {
|
|
332
|
-
logger.info(`\u{1F527} Using ${workerCount} workers for restore`);
|
|
333
|
-
}
|
|
334
|
-
await $4`docker exec ${containerId} sh -c ${`pg_restore -U ${dbUser} -d ${dbName} -j ${workerCount} --no-owner --no-acl ${containerTempPath}`}`;
|
|
335
|
-
await $4`docker exec ${containerId} rm -rf ${containerTempPath}`;
|
|
336
|
-
if (!silent) {
|
|
337
|
-
logger.info("\u2705 Database restore completed successfully");
|
|
338
|
-
}
|
|
339
|
-
return true;
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
// src/db/index.ts
|
|
343
|
-
var NODE_PROCESS_ARGS_OFFSET = 2;
|
|
344
|
-
var TIME_REGEX = /(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/;
|
|
345
|
-
function parseConfigPath() {
|
|
346
|
-
const args = process.argv.slice(NODE_PROCESS_ARGS_OFFSET);
|
|
347
|
-
const configFlagIndex = args.indexOf("--config");
|
|
348
|
-
if (configFlagIndex !== -1 && args.length > configFlagIndex + 1) {
|
|
349
|
-
const configValue = args[configFlagIndex + 1];
|
|
350
|
-
if (configValue !== void 0) {
|
|
351
|
-
return configValue;
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
throw new Error(
|
|
355
|
-
"Missing required --config flag with path to drizzle.ts config file"
|
|
356
|
-
);
|
|
357
|
-
}
|
|
358
|
-
function parseProjectFlag() {
|
|
359
|
-
const args = process.argv.slice(NODE_PROCESS_ARGS_OFFSET);
|
|
360
|
-
const projectFlagIndex = args.indexOf("--project");
|
|
361
|
-
if (projectFlagIndex !== -1 && args.length > projectFlagIndex + 1) {
|
|
362
|
-
const projectValue = args[projectFlagIndex + 1];
|
|
363
|
-
if (projectValue !== void 0) {
|
|
364
|
-
if (projectValue === "api" || projectValue === "indexer" || projectValue === "forum") {
|
|
365
|
-
return projectValue;
|
|
366
|
-
}
|
|
367
|
-
throw new Error(
|
|
368
|
-
'Invalid project value. Must be "api", "indexer", or "forum"'
|
|
369
|
-
);
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
throw new Error(
|
|
373
|
-
'Missing required --project flag. Must be "api", "indexer", or "forum"'
|
|
374
|
-
);
|
|
375
|
-
}
|
|
376
|
-
async function handleBackup(configPath, project) {
|
|
377
|
-
logger.info("\n\u{1F4E6} Database Backup");
|
|
378
|
-
logger.info("----------------");
|
|
379
|
-
try {
|
|
380
|
-
const confirmBackup = await select({
|
|
381
|
-
choices: [
|
|
382
|
-
{ name: "Yes, create backup", value: "yes" },
|
|
383
|
-
{ name: "No, cancel", value: "no" }
|
|
384
|
-
],
|
|
385
|
-
message: "Are you sure you want to create a database backup?"
|
|
386
|
-
});
|
|
387
|
-
if (confirmBackup === "no") {
|
|
388
|
-
logger.info("\n\u274C Backup cancelled");
|
|
389
|
-
return;
|
|
390
|
-
}
|
|
391
|
-
const backup = await createDatabaseBackup(configPath, project);
|
|
392
|
-
if (backup) {
|
|
393
|
-
if (backup.existing) {
|
|
394
|
-
logger.info(
|
|
395
|
-
"\n\u2705 Database backup completed successfully! (Skipping duplicate)"
|
|
396
|
-
);
|
|
397
|
-
} else {
|
|
398
|
-
logger.info("\n\u2705 Database backup completed successfully!");
|
|
399
|
-
}
|
|
400
|
-
logger.info(`\u{1F4BE} Backup file is available at: ${backup.path}`);
|
|
401
|
-
logger.info(`\u{1F4BE} Backup metadata is available at: ${backup.meta}`);
|
|
402
|
-
} else {
|
|
403
|
-
logger.error("\n\u274C Database backup failed");
|
|
404
|
-
process.exit(1);
|
|
405
|
-
}
|
|
406
|
-
} catch (error) {
|
|
407
|
-
logger.error({ error }, "\n\u274C Error during backup:");
|
|
408
|
-
process.exit(1);
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
async function handleRestore(project) {
|
|
412
|
-
logger.info("\n\u{1F504} Database Restore");
|
|
413
|
-
logger.info("-----------------");
|
|
414
|
-
try {
|
|
415
|
-
const backups = await getBackupFiles(project);
|
|
416
|
-
if (backups.length === 0) {
|
|
417
|
-
logger.info("\n\u274C No backup files found in the backups directory");
|
|
418
|
-
return;
|
|
419
|
-
}
|
|
420
|
-
const choices = backups.map((backup) => {
|
|
421
|
-
const timestamp = backup.timestamp.replace("_", " ").replace(TIME_REGEX, "$1-$2-$3 $4:$5:$6");
|
|
422
|
-
return {
|
|
423
|
-
name: `${timestamp} - ${backup.name}`,
|
|
424
|
-
value: backup.path
|
|
425
|
-
};
|
|
426
|
-
});
|
|
427
|
-
choices.push({
|
|
428
|
-
name: "Cancel restore operation",
|
|
429
|
-
value: "cancel"
|
|
430
|
-
});
|
|
431
|
-
const selectedBackup = await select({
|
|
432
|
-
choices,
|
|
433
|
-
message: "Select a backup to restore:"
|
|
434
|
-
});
|
|
435
|
-
if (selectedBackup === "cancel") {
|
|
436
|
-
logger.info("\n\u274C Restore cancelled");
|
|
437
|
-
return;
|
|
438
|
-
}
|
|
439
|
-
logger.info(
|
|
440
|
-
"\n\u26A0\uFE0F WARNING: This will overwrite your current database with the selected backup."
|
|
441
|
-
);
|
|
442
|
-
logger.info("\u26A0\uFE0F Make sure you have a recent backup before proceeding.");
|
|
443
|
-
const confirmRestore = await select({
|
|
444
|
-
choices: [
|
|
445
|
-
{ name: "Yes, restore the database", value: "yes" },
|
|
446
|
-
{ name: "No, cancel the operation", value: "no" }
|
|
447
|
-
],
|
|
448
|
-
message: "Are you sure you want to proceed with the restore?"
|
|
449
|
-
});
|
|
450
|
-
if (confirmRestore === "no") {
|
|
451
|
-
logger.info("\n\u274C Restore cancelled");
|
|
452
|
-
return;
|
|
453
|
-
}
|
|
454
|
-
const success = await restoreDatabase(selectedBackup);
|
|
455
|
-
if (success) {
|
|
456
|
-
logger.info("\n\u2705 Database restore completed successfully!");
|
|
457
|
-
} else {
|
|
458
|
-
logger.error("\n\u274C Database restore failed");
|
|
459
|
-
process.exit(1);
|
|
460
|
-
}
|
|
461
|
-
} catch (error) {
|
|
462
|
-
logger.error({ error }, "\n\u274C Error during restore:");
|
|
463
|
-
process.exit(1);
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
async function handleMigrate(configPath, project) {
|
|
467
|
-
logger.info("\n\u{1F504} Database Migration");
|
|
468
|
-
logger.info("------------------");
|
|
469
|
-
try {
|
|
470
|
-
const confirmMigrate = await select({
|
|
471
|
-
choices: [
|
|
472
|
-
{ name: "Yes, run migrations", value: "yes" },
|
|
473
|
-
{ name: "No, cancel", value: "no" }
|
|
474
|
-
],
|
|
475
|
-
message: "Are you sure you want to run database migrations?"
|
|
476
|
-
});
|
|
477
|
-
if (confirmMigrate === "no") {
|
|
478
|
-
logger.info("\n\u274C Migration cancelled");
|
|
479
|
-
return;
|
|
480
|
-
}
|
|
481
|
-
logger.info("Running migrations...");
|
|
482
|
-
const success = await runMigrations(configPath, project);
|
|
483
|
-
if (success) {
|
|
484
|
-
logger.info("\n\u2705 Database migrations completed successfully!");
|
|
485
|
-
} else {
|
|
486
|
-
logger.error("\n\u274C Database migrations failed");
|
|
487
|
-
process.exit(1);
|
|
488
|
-
}
|
|
489
|
-
} catch (error) {
|
|
490
|
-
logger.error({ error }, "\n\u274C Error during migration:");
|
|
491
|
-
process.exit(1);
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
async function handleGenerate(configPath) {
|
|
495
|
-
logger.info("\n\u{1F504} Generate Migration");
|
|
496
|
-
logger.info("-------------------");
|
|
497
|
-
try {
|
|
498
|
-
const confirmGenerate = await select({
|
|
499
|
-
choices: [
|
|
500
|
-
{ name: "Yes, generate migration", value: "yes" },
|
|
501
|
-
{ name: "No, cancel", value: "no" }
|
|
502
|
-
],
|
|
503
|
-
message: "Are you sure you want to generate a migration?"
|
|
504
|
-
});
|
|
505
|
-
if (confirmGenerate === "no") {
|
|
506
|
-
logger.info("\n\u274C Generation cancelled");
|
|
507
|
-
return;
|
|
508
|
-
}
|
|
509
|
-
logger.info("Generating migration...");
|
|
510
|
-
const success = await generateMigration(configPath);
|
|
511
|
-
if (success) {
|
|
512
|
-
logger.info("\n\u2705 Migration generation completed successfully!");
|
|
513
|
-
} else {
|
|
514
|
-
logger.error("\n\u274C Migration generation failed");
|
|
515
|
-
process.exit(1);
|
|
516
|
-
}
|
|
517
|
-
} catch (error) {
|
|
518
|
-
logger.error({ error }, "\n\u274C Error during migration generation:");
|
|
519
|
-
process.exit(1);
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
function handleStudio(configPath) {
|
|
523
|
-
logger.info("\n\u{1F50D} Launching Drizzle Studio");
|
|
524
|
-
logger.info("------------------------");
|
|
525
|
-
try {
|
|
526
|
-
logger.info("Starting Drizzle Studio...");
|
|
527
|
-
logger.info(
|
|
528
|
-
"This will open a web interface to explore and manage your database."
|
|
529
|
-
);
|
|
530
|
-
logger.info("Press Ctrl+C to stop the server when you're done.");
|
|
531
|
-
const studio = new Promise((resolve, reject) => {
|
|
532
|
-
void execa("pnpm", ["drizzle-kit", "studio", "--config", configPath], {
|
|
533
|
-
stdio: "inherit"
|
|
534
|
-
}).on("exit", () => {
|
|
535
|
-
logger.info("\n\u2705 Drizzle Studio session ended");
|
|
536
|
-
resolve(void 0);
|
|
537
|
-
}).on("error", (error) => {
|
|
538
|
-
reject(error);
|
|
539
|
-
});
|
|
540
|
-
});
|
|
541
|
-
process.on("SIGINT", () => {
|
|
542
|
-
void studio.then(() => {
|
|
543
|
-
process.exit(0);
|
|
544
|
-
});
|
|
545
|
-
});
|
|
546
|
-
} catch (error) {
|
|
547
|
-
logger.error({ error }, "\n\u274C Error launching Drizzle Studio:");
|
|
548
|
-
process.exit(1);
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
|
-
async function commandLine() {
|
|
552
|
-
const configPath = parseConfigPath();
|
|
553
|
-
const project = parseProjectFlag();
|
|
554
|
-
const resolvedConfigPath = path.isAbsolute(configPath) ? configPath : path.resolve(process.cwd(), configPath);
|
|
555
|
-
logger.info("\u{1F6E0}\uFE0F Database Management CLI");
|
|
556
|
-
logger.info("==========================");
|
|
557
|
-
logger.info(`Using config: ${resolvedConfigPath}`);
|
|
558
|
-
logger.info(`Project: ${project}`);
|
|
559
|
-
const operations = [
|
|
560
|
-
{
|
|
561
|
-
name: "Backup - Create a database backup",
|
|
562
|
-
value: "backup"
|
|
563
|
-
},
|
|
564
|
-
{
|
|
565
|
-
name: "Restore - Restore database from a backup",
|
|
566
|
-
value: "restore"
|
|
567
|
-
},
|
|
568
|
-
{
|
|
569
|
-
name: "Migrate - Run database migrations",
|
|
570
|
-
value: "migrate"
|
|
571
|
-
},
|
|
572
|
-
{
|
|
573
|
-
name: "Generate - Generate migration",
|
|
574
|
-
value: "generate"
|
|
575
|
-
},
|
|
576
|
-
{
|
|
577
|
-
name: "Studio - Launch Drizzle Studio",
|
|
578
|
-
value: "studio"
|
|
579
|
-
},
|
|
580
|
-
{
|
|
581
|
-
name: "Exit",
|
|
582
|
-
value: "exit"
|
|
583
|
-
}
|
|
584
|
-
];
|
|
585
|
-
const selectedOperation = await select({
|
|
586
|
-
choices: operations,
|
|
587
|
-
message: "Select an operation:"
|
|
588
|
-
});
|
|
589
|
-
switch (selectedOperation) {
|
|
590
|
-
case "backup":
|
|
591
|
-
await handleBackup(resolvedConfigPath, project);
|
|
592
|
-
break;
|
|
593
|
-
case "restore":
|
|
594
|
-
await handleRestore(project);
|
|
595
|
-
break;
|
|
596
|
-
case "migrate":
|
|
597
|
-
await handleMigrate(resolvedConfigPath, project);
|
|
598
|
-
break;
|
|
599
|
-
case "generate":
|
|
600
|
-
await handleGenerate(resolvedConfigPath);
|
|
601
|
-
break;
|
|
602
|
-
case "studio":
|
|
603
|
-
handleStudio(resolvedConfigPath);
|
|
604
|
-
break;
|
|
605
|
-
case "exit":
|
|
606
|
-
logger.info("\n\u{1F44B} Exiting...");
|
|
607
|
-
return process.exit(0);
|
|
608
|
-
default:
|
|
609
|
-
logger.error("\n\u274C Invalid operation");
|
|
610
|
-
return process.exit(1);
|
|
611
|
-
}
|
|
612
|
-
return process.exit(0);
|
|
613
|
-
}
|
|
614
|
-
export {
|
|
615
|
-
commandLine as default
|
|
616
|
-
};
|