@habityzer/db-sync-tool 1.0.3
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 +119 -0
- package/bin/dbsync.js +14 -0
- package/package.json +54 -0
- package/src/adapters/mysql.js +116 -0
- package/src/adapters/postgres.js +148 -0
- package/src/commands/clean.js +68 -0
- package/src/commands/export.js +195 -0
- package/src/commands/info.js +31 -0
- package/src/commands/list.js +63 -0
- package/src/commands/restore.js +147 -0
- package/src/commands/test.js +29 -0
- package/src/index.js +118 -0
- package/src/utils/backup-scanner.js +101 -0
- package/src/utils/backup-validate.js +78 -0
- package/src/utils/compression.js +25 -0
- package/src/utils/config-loader.js +92 -0
- package/src/utils/env-loader.js +45 -0
- package/src/utils/errors.js +45 -0
- package/src/utils/process.js +79 -0
- package/src/utils/progress.js +115 -0
- package/src/utils/prompt.js +22 -0
- package/src/utils/retention.js +32 -0
- package/src/utils/ui.js +63 -0
- package/src/utils/url-parser.js +190 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { ConfigError } from './errors.js';
|
|
2
|
+
|
|
3
|
+
const STRIP_PARAMS = new Set(['serverVersion', 'charset', 'driver']);
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @typedef {'postgres' | 'mysql'} DbType
|
|
7
|
+
* @typedef {object} ParsedDatabaseUrl
|
|
8
|
+
* @property {DbType} type
|
|
9
|
+
* @property {string} protocol
|
|
10
|
+
* @property {string} host
|
|
11
|
+
* @property {number} [port]
|
|
12
|
+
* @property {string} user
|
|
13
|
+
* @property {string} password
|
|
14
|
+
* @property {string} database
|
|
15
|
+
* @property {string} hrefForTools - URL without stripped params, for tools that accept URI
|
|
16
|
+
* @property {URL} url
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Strip doctrine/non-essential query params from a database URL string.
|
|
21
|
+
* @param {string} input
|
|
22
|
+
*/
|
|
23
|
+
export function stripNonEssentialQueryParams(input) {
|
|
24
|
+
let s = String(input).trim();
|
|
25
|
+
const hashIdx = s.indexOf('#');
|
|
26
|
+
const hash = hashIdx >= 0 ? s.slice(hashIdx) : '';
|
|
27
|
+
const noHash = hashIdx >= 0 ? s.slice(0, hashIdx) : s;
|
|
28
|
+
const qIdx = noHash.indexOf('?');
|
|
29
|
+
if (qIdx < 0) return s;
|
|
30
|
+
const base = noHash.slice(0, qIdx);
|
|
31
|
+
const qs = noHrefSearchParams(noHash.slice(qIdx + 1));
|
|
32
|
+
const sp = new URLSearchParams(qs);
|
|
33
|
+
for (const k of STRIP_PARAMS) {
|
|
34
|
+
sp.delete(k);
|
|
35
|
+
}
|
|
36
|
+
const rest = sp.toString();
|
|
37
|
+
return base + (rest ? `?${rest}` : '') + hash;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function noHrefSearchParams(qs) {
|
|
41
|
+
return qs.replace(/^\+/, '');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Fix postgresql:// -> postgres://
|
|
46
|
+
* @param {string} input
|
|
47
|
+
*/
|
|
48
|
+
export function fixPostgresProtocol(input) {
|
|
49
|
+
return String(input).trim().replace(/^postgresql:/i, 'postgres:');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Parse user:password from userinfo (password may contain ':').
|
|
54
|
+
* @param {string} userinfo
|
|
55
|
+
*/
|
|
56
|
+
function splitUserInfo(userinfo) {
|
|
57
|
+
const idx = userinfo.indexOf(':');
|
|
58
|
+
if (idx < 0) {
|
|
59
|
+
return { user: decodeURIComponent(userinfo), password: '' };
|
|
60
|
+
}
|
|
61
|
+
const user = decodeURIComponent(userinfo.slice(0, idx));
|
|
62
|
+
const password = decodeURIComponent(userinfo.slice(idx + 1).replace(/\+/g, ' '));
|
|
63
|
+
return { user, password };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Manual parse when URL constructor fails (e.g. rare edge cases).
|
|
68
|
+
* @param {string} s - postgres:// or mysql:// without fragment
|
|
69
|
+
*/
|
|
70
|
+
function parseManual(s) {
|
|
71
|
+
const isMysql = /^mysql:\/\//i.test(s);
|
|
72
|
+
const isPostgres = /^postgres:\/\//i.test(s);
|
|
73
|
+
if (!isMysql && !isPostgres) {
|
|
74
|
+
throw new ConfigError(
|
|
75
|
+
'Unsupported database URL protocol',
|
|
76
|
+
'Use postgresql://, postgres://, or mysql://'
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
const type = isMysql ? 'mysql' : 'postgres';
|
|
80
|
+
const scheme = isMysql ? 'mysql:' : 'postgres:';
|
|
81
|
+
const rest = s.replace(/^mysql:\/\//i, '').replace(/^postgres:\/\//i, '');
|
|
82
|
+
const at = rest.lastIndexOf('@');
|
|
83
|
+
if (at < 0) {
|
|
84
|
+
throw new ConfigError('Invalid database URL', 'Expected user@host in DATABASE_URL');
|
|
85
|
+
}
|
|
86
|
+
const userinfo = rest.slice(0, at);
|
|
87
|
+
const afterAt = rest.slice(at + 1);
|
|
88
|
+
const { user, password } = splitUserInfo(userinfo);
|
|
89
|
+
|
|
90
|
+
const slash = afterAt.indexOf('/');
|
|
91
|
+
const hostPort = slash >= 0 ? afterAt.slice(0, slash) : afterAt;
|
|
92
|
+
const pathAndQuery = slash >= 0 ? afterAt.slice(slash) : '/';
|
|
93
|
+
|
|
94
|
+
let host = hostPort;
|
|
95
|
+
let port;
|
|
96
|
+
if (hostPort.startsWith('[')) {
|
|
97
|
+
const end = hostPort.indexOf(']');
|
|
98
|
+
host = hostPort.slice(1, end);
|
|
99
|
+
const colon = hostPort.indexOf(':', end);
|
|
100
|
+
if (colon >= 0) port = Number(hostPort.slice(colon + 1));
|
|
101
|
+
} else {
|
|
102
|
+
const colon = hostPort.lastIndexOf(':');
|
|
103
|
+
if (colon > 0 && !hostPort.includes(':', colon - 1)) {
|
|
104
|
+
host = hostPort.slice(0, colon);
|
|
105
|
+
port = Number(hostPort.slice(colon + 1));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const pathMatch = pathAndQuery.match(/^\/([^?]*)(\?.*)?$/);
|
|
110
|
+
const dbName = pathMatch ? decodeURIComponent(pathMatch[1] || '') : '';
|
|
111
|
+
const search = pathMatch?.[2] || '';
|
|
112
|
+
|
|
113
|
+
const built = `${scheme}//${encodeURIComponent(user)}:${encodeURIComponent(password)}@${hostPort}${pathAndQuery}`;
|
|
114
|
+
let url;
|
|
115
|
+
try {
|
|
116
|
+
url = new URL(built);
|
|
117
|
+
} catch {
|
|
118
|
+
url = new URL(`${scheme}//${hostPort}${pathAndQuery}`);
|
|
119
|
+
url.username = user;
|
|
120
|
+
url.password = password;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return { type, user, password, database: dbName, host, port, url };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* @param {string} rawUrl
|
|
128
|
+
* @returns {ParsedDatabaseUrl}
|
|
129
|
+
*/
|
|
130
|
+
export function parseDatabaseUrl(rawUrl) {
|
|
131
|
+
if (!rawUrl || typeof rawUrl !== 'string' || !String(rawUrl).trim()) {
|
|
132
|
+
throw new ConfigError(
|
|
133
|
+
'DATABASE_URL is missing or empty',
|
|
134
|
+
'Set DATABASE_URL in .env or use --env-var'
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
let s = fixPostgresProtocol(rawUrl);
|
|
139
|
+
s = stripNonEssentialQueryParams(s);
|
|
140
|
+
|
|
141
|
+
let type;
|
|
142
|
+
let url;
|
|
143
|
+
let user = '';
|
|
144
|
+
let password = '';
|
|
145
|
+
let database = '';
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
url = new URL(s);
|
|
149
|
+
const proto = url.protocol.replace(/:$/, '').toLowerCase();
|
|
150
|
+
if (proto === 'postgres') {
|
|
151
|
+
type = 'postgres';
|
|
152
|
+
} else if (proto === 'mysql') {
|
|
153
|
+
type = 'mysql';
|
|
154
|
+
} else {
|
|
155
|
+
throw new ConfigError(
|
|
156
|
+
'Unsupported database URL protocol',
|
|
157
|
+
'Use postgresql://, postgres://, or mysql://'
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
user = decodeURIComponent(url.username || '');
|
|
161
|
+
password = decodeURIComponent(url.password || '');
|
|
162
|
+
database = decodeURIComponent((url.pathname || '/').replace(/^\//, '') || '');
|
|
163
|
+
} catch {
|
|
164
|
+
const manual = parseManual(s);
|
|
165
|
+
type = manual.type;
|
|
166
|
+
url = manual.url;
|
|
167
|
+
user = manual.user;
|
|
168
|
+
password = manual.password;
|
|
169
|
+
database = manual.database;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
for (const k of STRIP_PARAMS) {
|
|
173
|
+
url.searchParams.delete(k);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const host = url.hostname || 'localhost';
|
|
177
|
+
const port = url.port ? Number(url.port) : undefined;
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
type,
|
|
181
|
+
protocol: type === 'mysql' ? 'mysql:' : 'postgres:',
|
|
182
|
+
host,
|
|
183
|
+
port,
|
|
184
|
+
user,
|
|
185
|
+
password,
|
|
186
|
+
database,
|
|
187
|
+
hrefForTools: url.toString(),
|
|
188
|
+
url,
|
|
189
|
+
};
|
|
190
|
+
}
|