prisma-sharding 0.0.5 → 0.0.7
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 +237 -59
- package/dist/cli/migrate.js +327 -70
- package/dist/cli/studio.js +668 -59
- package/dist/cli/test.js +169 -59
- package/dist/cli/update.js +340 -75
- package/dist/cli/utils/command.js +232 -0
- package/dist/cli/utils/postgres.js +55 -0
- package/dist/index.d.mts +4 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +414 -90
- package/dist/index.mjs +414 -90
- package/package.json +4 -3
package/dist/cli/test.js
CHANGED
|
@@ -25,6 +25,114 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
25
25
|
|
|
26
26
|
// src/cli/test.ts
|
|
27
27
|
var import_config = require("dotenv/config");
|
|
28
|
+
|
|
29
|
+
// src/constants/internal.ts
|
|
30
|
+
var INTERNAL_DEFAULTS = {
|
|
31
|
+
HEALTH_CHECK_TIMEOUT_MS: 5e3,
|
|
32
|
+
CROSS_SHARD_CONCURRENCY: 4,
|
|
33
|
+
CROSS_SHARD_TIMEOUT_MS: 1e4,
|
|
34
|
+
CLI_COMMAND_TIMEOUT_MS: 12e4,
|
|
35
|
+
CLI_FORCE_KILL_GRACE_MS: 5e3,
|
|
36
|
+
CLI_MAX_OUTPUT_LENGTH: 1e6,
|
|
37
|
+
CLI_TEST_TCP_TIMEOUT_MS: 5e3,
|
|
38
|
+
CLI_TEST_COMMAND_TIMEOUT_MS: 15e3,
|
|
39
|
+
STUDIO_BASE_PORT: 51212,
|
|
40
|
+
STUDIO_REUSE_EXISTING: true,
|
|
41
|
+
STUDIO_STRICT_PORT_CHECK: false,
|
|
42
|
+
STUDIO_START_TIMEOUT_MS: 15e3,
|
|
43
|
+
STUDIO_STABILITY_MS: 500,
|
|
44
|
+
STUDIO_SHUTDOWN_TIMEOUT_MS: 5e3
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// src/utils/sanitize.ts
|
|
48
|
+
var escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
49
|
+
var maskDatabaseUrl = (url) => {
|
|
50
|
+
try {
|
|
51
|
+
const parsed = new URL(url);
|
|
52
|
+
if (parsed.password) {
|
|
53
|
+
parsed.password = "***";
|
|
54
|
+
}
|
|
55
|
+
return parsed.toString();
|
|
56
|
+
} catch {
|
|
57
|
+
return url.replace(/:[^:@]+@/, ":***@");
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
var sanitizeDatabaseText = (output, databaseUrls) => {
|
|
61
|
+
let sanitized = output;
|
|
62
|
+
for (const url of databaseUrls) {
|
|
63
|
+
sanitized = sanitized.replace(new RegExp(escapeRegExp(url), "g"), maskDatabaseUrl(url));
|
|
64
|
+
try {
|
|
65
|
+
const password = new URL(url).password;
|
|
66
|
+
if (password.length >= 4) {
|
|
67
|
+
sanitized = sanitized.replace(
|
|
68
|
+
new RegExp(escapeRegExp(decodeURIComponent(password)), "g"),
|
|
69
|
+
"***"
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
} catch {
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return sanitized.replace(
|
|
76
|
+
/(postgres(?:ql)?:\/\/[^:/\s]+:)[^@\s]+(@)/gi,
|
|
77
|
+
"$1***$2"
|
|
78
|
+
);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// src/cli/utils/command.ts
|
|
82
|
+
var sanitizeCommandOutput = (output, env = process.env) => {
|
|
83
|
+
const databaseUrls = Object.entries(env).filter(
|
|
84
|
+
([name, value]) => Boolean(value) && (name === "DATABASE_URL" || /^SHARD_\d+_URL$/.test(name))
|
|
85
|
+
).map(([, value]) => value);
|
|
86
|
+
return sanitizeDatabaseText(output, databaseUrls).replace(/(\bpassword\s*[=:]\s*)[^\s,;]+/gi, "$1***");
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// src/cli/utils/postgres.ts
|
|
90
|
+
var parsePostgresUrl = (url) => {
|
|
91
|
+
try {
|
|
92
|
+
const parsed = new URL(url);
|
|
93
|
+
if (parsed.protocol !== "postgresql:" && parsed.protocol !== "postgres:") {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
const database = decodeURIComponent(parsed.pathname.replace(/^\/+/, ""));
|
|
97
|
+
const port = parsed.port ? Number(parsed.port) : 5432;
|
|
98
|
+
const queryHost = parsed.searchParams.get("host");
|
|
99
|
+
const host = parsed.hostname.replace(/^\[(.*)\]$/, "$1");
|
|
100
|
+
if (!database || !host || !Number.isInteger(port) || port <= 0 || port > 65535) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
host,
|
|
105
|
+
port,
|
|
106
|
+
database,
|
|
107
|
+
socketPath: queryHost?.startsWith("/") ? queryHost : void 0
|
|
108
|
+
};
|
|
109
|
+
} catch {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
var postgresEndpoint = (info) => info.socketPath || `${info.host}:${info.port}`;
|
|
114
|
+
|
|
115
|
+
// src/cli/utils/shards.ts
|
|
116
|
+
var NO_SHARDS_CONFIGURED_MESSAGE = "No shards configured. Set SHARD_COUNT and SHARD_N_URL environment variables.";
|
|
117
|
+
var getShardConfigResult = (env = process.env) => {
|
|
118
|
+
const shards = [];
|
|
119
|
+
const missingShardIds = [];
|
|
120
|
+
const shardCount = parseInt(env.SHARD_COUNT || "0", 10);
|
|
121
|
+
for (let i = 1; i <= shardCount; i++) {
|
|
122
|
+
const url = env[`SHARD_${i}_URL`];
|
|
123
|
+
if (url) {
|
|
124
|
+
shards.push({ id: `shard_${i}`, index: i - 1, url });
|
|
125
|
+
} else {
|
|
126
|
+
missingShardIds.push(`shard_${i}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (shards.length === 0 && env.DATABASE_URL) {
|
|
130
|
+
shards.push({ id: "shard_1", index: 0, url: env.DATABASE_URL });
|
|
131
|
+
}
|
|
132
|
+
return { shards, missingShardIds };
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
// src/cli/test.ts
|
|
28
136
|
var results = [];
|
|
29
137
|
var testUsers = [];
|
|
30
138
|
var TEST_USER_COUNT = 24;
|
|
@@ -39,20 +147,6 @@ ${"=".repeat(60)}
|
|
|
39
147
|
${"=".repeat(60)}`),
|
|
40
148
|
detail: (msg) => console.log(` ${msg}`)
|
|
41
149
|
};
|
|
42
|
-
var getShardConfigs = () => {
|
|
43
|
-
const shards = [];
|
|
44
|
-
const shardCount = parseInt(process.env.SHARD_COUNT || "0", 10);
|
|
45
|
-
for (let i = 1; i <= shardCount; i++) {
|
|
46
|
-
const url = process.env[`SHARD_${i}_URL`];
|
|
47
|
-
if (url) {
|
|
48
|
-
shards.push({ id: `shard_${i}`, url });
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
if (shards.length === 0 && process.env.DATABASE_URL) {
|
|
52
|
-
shards.push({ id: "shard_1", url: process.env.DATABASE_URL });
|
|
53
|
-
}
|
|
54
|
-
return shards;
|
|
55
|
-
};
|
|
56
150
|
var runTest = async (name, testFn) => {
|
|
57
151
|
const start = Date.now();
|
|
58
152
|
try {
|
|
@@ -67,28 +161,11 @@ var runTest = async (name, testFn) => {
|
|
|
67
161
|
log.error(`${name}: ${message}`);
|
|
68
162
|
}
|
|
69
163
|
};
|
|
70
|
-
var
|
|
71
|
-
try {
|
|
72
|
-
const match = url.match(/postgresql:\/\/([^:]+):([^@]+)@([^:]+):(\d+)\/([^?]+)/);
|
|
73
|
-
if (match) {
|
|
74
|
-
return {
|
|
75
|
-
user: match[1],
|
|
76
|
-
password: match[2],
|
|
77
|
-
host: match[3],
|
|
78
|
-
port: parseInt(match[4], 10),
|
|
79
|
-
database: match[5]
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
return null;
|
|
83
|
-
} catch {
|
|
84
|
-
return null;
|
|
85
|
-
}
|
|
86
|
-
};
|
|
87
|
-
var testTcpConnection = (host, port) => {
|
|
164
|
+
var testTcpConnection = (urlInfo) => {
|
|
88
165
|
return new Promise((resolve) => {
|
|
89
166
|
import("net").then(({ default: net }) => {
|
|
90
167
|
const socket = new net.Socket();
|
|
91
|
-
socket.setTimeout(
|
|
168
|
+
socket.setTimeout(INTERNAL_DEFAULTS.CLI_TEST_TCP_TIMEOUT_MS);
|
|
92
169
|
socket.on("connect", () => {
|
|
93
170
|
socket.destroy();
|
|
94
171
|
resolve(true);
|
|
@@ -101,7 +178,11 @@ var testTcpConnection = (host, port) => {
|
|
|
101
178
|
socket.destroy();
|
|
102
179
|
resolve(false);
|
|
103
180
|
});
|
|
104
|
-
|
|
181
|
+
if (urlInfo.socketPath) {
|
|
182
|
+
socket.connect(`${urlInfo.socketPath}/.s.PGSQL.${urlInfo.port}`);
|
|
183
|
+
} else {
|
|
184
|
+
socket.connect(urlInfo.port, urlInfo.host);
|
|
185
|
+
}
|
|
105
186
|
}).catch(() => resolve(false));
|
|
106
187
|
});
|
|
107
188
|
};
|
|
@@ -113,15 +194,19 @@ var getPgClient = async () => {
|
|
|
113
194
|
return null;
|
|
114
195
|
}
|
|
115
196
|
};
|
|
116
|
-
var executeSql = async (url,
|
|
197
|
+
var executeSql = async (url, statement) => {
|
|
117
198
|
const Client = await getPgClient();
|
|
199
|
+
const normalized = typeof statement === "string" ? { text: statement } : statement;
|
|
118
200
|
if (!Client) {
|
|
119
|
-
|
|
201
|
+
if (normalized.values && !normalized.psqlText) {
|
|
202
|
+
return { success: false, error: "Parameterized SQL has no safe psql fallback" };
|
|
203
|
+
}
|
|
204
|
+
return executeSqlWithPsql(url, normalized.psqlText ?? normalized.text);
|
|
120
205
|
}
|
|
121
206
|
const client = new Client({ connectionString: url });
|
|
122
207
|
try {
|
|
123
208
|
await client.connect();
|
|
124
|
-
const result = await client.query(
|
|
209
|
+
const result = await client.query(normalized.text, normalized.values);
|
|
125
210
|
await client.end();
|
|
126
211
|
return { success: true, rows: result.rows };
|
|
127
212
|
} catch (error) {
|
|
@@ -129,35 +214,42 @@ var executeSql = async (url, sql) => {
|
|
|
129
214
|
await client.end();
|
|
130
215
|
} catch {
|
|
131
216
|
}
|
|
132
|
-
|
|
217
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
218
|
+
return {
|
|
219
|
+
success: false,
|
|
220
|
+
error: sanitizeCommandOutput(message, { DATABASE_URL: url })
|
|
221
|
+
};
|
|
133
222
|
}
|
|
134
223
|
};
|
|
135
224
|
var executeSqlWithPsql = (url, sql) => {
|
|
136
225
|
return new Promise((resolve) => {
|
|
137
226
|
import("child_process").then(({ spawn }) => {
|
|
138
227
|
const psql = spawn("psql", [url, "-c", sql], {
|
|
139
|
-
shell:
|
|
228
|
+
shell: false,
|
|
140
229
|
stdio: ["pipe", "pipe", "pipe"]
|
|
141
230
|
});
|
|
142
|
-
let stdout = "";
|
|
143
231
|
let stderr = "";
|
|
144
|
-
psql.stdout?.on("data", (data) => {
|
|
145
|
-
stdout += data.toString();
|
|
146
|
-
});
|
|
147
232
|
psql.stderr?.on("data", (data) => {
|
|
148
233
|
stderr += data.toString();
|
|
149
234
|
});
|
|
150
235
|
const timeout = setTimeout(() => {
|
|
151
236
|
psql.kill();
|
|
152
237
|
resolve({ success: false, error: "Command timeout" });
|
|
153
|
-
},
|
|
238
|
+
}, INTERNAL_DEFAULTS.CLI_TEST_COMMAND_TIMEOUT_MS);
|
|
239
|
+
timeout.unref?.();
|
|
154
240
|
psql.on("close", (code) => {
|
|
155
241
|
clearTimeout(timeout);
|
|
156
|
-
resolve({
|
|
242
|
+
resolve({
|
|
243
|
+
success: code === 0,
|
|
244
|
+
error: code !== 0 ? sanitizeCommandOutput(stderr, { DATABASE_URL: url }) : void 0
|
|
245
|
+
});
|
|
157
246
|
});
|
|
158
247
|
psql.on("error", (err) => {
|
|
159
248
|
clearTimeout(timeout);
|
|
160
|
-
resolve({
|
|
249
|
+
resolve({
|
|
250
|
+
success: false,
|
|
251
|
+
error: sanitizeCommandOutput(err.message, { DATABASE_URL: url })
|
|
252
|
+
});
|
|
161
253
|
});
|
|
162
254
|
});
|
|
163
255
|
});
|
|
@@ -175,7 +267,7 @@ var getShardIndex = (key, shardCount) => {
|
|
|
175
267
|
return hashString(key) % shardCount;
|
|
176
268
|
};
|
|
177
269
|
var runTests = async () => {
|
|
178
|
-
const shards =
|
|
270
|
+
const { shards, missingShardIds } = getShardConfigResult();
|
|
179
271
|
const timestamp = Date.now();
|
|
180
272
|
const testTableName = `_prisma_sharding_test_${timestamp}`;
|
|
181
273
|
console.log("\n" + "\u2550".repeat(60));
|
|
@@ -187,10 +279,14 @@ var runTests = async () => {
|
|
|
187
279
|
console.log(` \u2022 Routing Strategy: ${process.env.SHARD_ROUTING_STRATEGY || "modulo"}`);
|
|
188
280
|
console.log(` \u2022 Test Users: ${TEST_USER_COUNT}`);
|
|
189
281
|
console.log(` \u2022 Test Table: ${testTableName}`);
|
|
282
|
+
if (missingShardIds.length > 0) {
|
|
283
|
+
console.error(`
|
|
284
|
+
\u274C Missing shard URLs: ${missingShardIds.join(", ")}`);
|
|
285
|
+
process.exit(1);
|
|
286
|
+
}
|
|
190
287
|
if (shards.length === 0) {
|
|
191
|
-
console.error(
|
|
192
|
-
|
|
193
|
-
);
|
|
288
|
+
console.error(`
|
|
289
|
+
\u274C ${NO_SHARDS_CONFIGURED_MESSAGE}`);
|
|
194
290
|
process.exit(1);
|
|
195
291
|
}
|
|
196
292
|
const pgAvailable = await getPgClient();
|
|
@@ -214,7 +310,7 @@ var runTests = async () => {
|
|
|
214
310
|
if (!urlInfo) {
|
|
215
311
|
throw new Error(`Invalid URL format for ${shard.id}`);
|
|
216
312
|
}
|
|
217
|
-
log.detail(`${shard.id} \u2192 ${urlInfo
|
|
313
|
+
log.detail(`${shard.id} \u2192 ${postgresEndpoint(urlInfo)}/${urlInfo.database}`);
|
|
218
314
|
}
|
|
219
315
|
});
|
|
220
316
|
log.section("Test 2: Network Connectivity");
|
|
@@ -222,11 +318,11 @@ var runTests = async () => {
|
|
|
222
318
|
await runTest(`TCP connection to ${shard.id}`, async () => {
|
|
223
319
|
const urlInfo = parsePostgresUrl(shard.url);
|
|
224
320
|
if (!urlInfo) throw new Error("Invalid URL");
|
|
225
|
-
const connected = await testTcpConnection(urlInfo
|
|
321
|
+
const connected = await testTcpConnection(urlInfo);
|
|
226
322
|
if (!connected) {
|
|
227
|
-
throw new Error(`Cannot reach ${urlInfo
|
|
323
|
+
throw new Error(`Cannot reach ${postgresEndpoint(urlInfo)}`);
|
|
228
324
|
}
|
|
229
|
-
log.detail(`${urlInfo
|
|
325
|
+
log.detail(`${postgresEndpoint(urlInfo)} is reachable`);
|
|
230
326
|
});
|
|
231
327
|
}
|
|
232
328
|
log.section("Test 3: Database Connection");
|
|
@@ -269,10 +365,17 @@ var runTests = async () => {
|
|
|
269
365
|
const username = `testuser_${i}`;
|
|
270
366
|
const shardIndex = getShardIndex(userId, shards.length);
|
|
271
367
|
const targetShard = shards[shardIndex];
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
368
|
+
const controlledValue = (value) => {
|
|
369
|
+
if (!/^[A-Za-z0-9_@.-]+$/.test(value)) {
|
|
370
|
+
throw new Error("Generated test value contains unsupported characters");
|
|
371
|
+
}
|
|
372
|
+
return `'${value}'`;
|
|
373
|
+
};
|
|
374
|
+
const insertSql = {
|
|
375
|
+
text: `INSERT INTO "${testTableName}" (id, email, username) VALUES ($1, $2, $3);`,
|
|
376
|
+
values: [userId, email, username],
|
|
377
|
+
psqlText: `INSERT INTO "${testTableName}" (id, email, username) VALUES (${controlledValue(userId)}, ${controlledValue(email)}, ${controlledValue(username)});`
|
|
378
|
+
};
|
|
276
379
|
const result = await executeSql(targetShard.url, insertSql);
|
|
277
380
|
if (result.success) {
|
|
278
381
|
testUsers.push({ id: userId, email, shardId: targetShard.id });
|
|
@@ -323,7 +426,14 @@ var runTests = async () => {
|
|
|
323
426
|
const user = testUsers[i];
|
|
324
427
|
const shard = shards.find((s) => s.id === user.shardId);
|
|
325
428
|
if (!shard) continue;
|
|
326
|
-
|
|
429
|
+
if (!/^[A-Za-z0-9_@.-]+$/.test(user.id)) {
|
|
430
|
+
throw new Error("Generated test ID contains unsupported characters");
|
|
431
|
+
}
|
|
432
|
+
const selectSql = {
|
|
433
|
+
text: `SELECT id, email FROM "${testTableName}" WHERE id = $1;`,
|
|
434
|
+
values: [user.id],
|
|
435
|
+
psqlText: `SELECT id, email FROM "${testTableName}" WHERE id = '${user.id}';`
|
|
436
|
+
};
|
|
327
437
|
const result = await executeSql(shard.url, selectSql);
|
|
328
438
|
if (result.success && result.rows && result.rows.length > 0) {
|
|
329
439
|
verified++;
|