@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.
@@ -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
+ }