prisma-sharding 0.0.6 → 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 +135 -58
- package/dist/cli/migrate.js +198 -47
- package/dist/cli/studio.js +152 -84
- package/dist/cli/test.js +145 -41
- package/dist/cli/update.js +195 -46
- package/dist/cli/utils/command.js +232 -0
- package/dist/cli/utils/postgres.js +55 -0
- package/dist/index.js +395 -89
- package/dist/index.mjs +395 -89
- package/package.json +3 -3
package/dist/cli/studio.js
CHANGED
|
@@ -25,28 +25,116 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
25
25
|
|
|
26
26
|
// src/cli/studio.ts
|
|
27
27
|
var import_config = require("dotenv/config");
|
|
28
|
+
|
|
29
|
+
// src/cli/utils/studio-runner.ts
|
|
28
30
|
var import_child_process = require("child_process");
|
|
29
31
|
|
|
30
|
-
// src/
|
|
31
|
-
var
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
// src/constants/internal.ts
|
|
33
|
+
var INTERNAL_DEFAULTS = {
|
|
34
|
+
HEALTH_CHECK_TIMEOUT_MS: 5e3,
|
|
35
|
+
CROSS_SHARD_CONCURRENCY: 4,
|
|
36
|
+
CROSS_SHARD_TIMEOUT_MS: 1e4,
|
|
37
|
+
CLI_COMMAND_TIMEOUT_MS: 12e4,
|
|
38
|
+
CLI_FORCE_KILL_GRACE_MS: 5e3,
|
|
39
|
+
CLI_MAX_OUTPUT_LENGTH: 1e6,
|
|
40
|
+
CLI_TEST_TCP_TIMEOUT_MS: 5e3,
|
|
41
|
+
CLI_TEST_COMMAND_TIMEOUT_MS: 15e3,
|
|
42
|
+
STUDIO_BASE_PORT: 51212,
|
|
43
|
+
STUDIO_REUSE_EXISTING: true,
|
|
44
|
+
STUDIO_STRICT_PORT_CHECK: false,
|
|
45
|
+
STUDIO_START_TIMEOUT_MS: 15e3,
|
|
46
|
+
STUDIO_STABILITY_MS: 500,
|
|
47
|
+
STUDIO_SHUTDOWN_TIMEOUT_MS: 5e3
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// src/utils/sanitize.ts
|
|
51
|
+
var escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
52
|
+
var maskDatabaseUrl = (url) => {
|
|
53
|
+
try {
|
|
54
|
+
const parsed = new URL(url);
|
|
55
|
+
if (parsed.password) {
|
|
56
|
+
parsed.password = "***";
|
|
57
|
+
}
|
|
58
|
+
return parsed.toString();
|
|
59
|
+
} catch {
|
|
60
|
+
return url.replace(/:[^:@]+@/, ":***@");
|
|
35
61
|
}
|
|
36
|
-
return ["1", "true", "yes", "on"].includes(value.toLowerCase());
|
|
37
62
|
};
|
|
38
|
-
var
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
63
|
+
var sanitizeDatabaseText = (output, databaseUrls) => {
|
|
64
|
+
let sanitized = output;
|
|
65
|
+
for (const url of databaseUrls) {
|
|
66
|
+
sanitized = sanitized.replace(new RegExp(escapeRegExp(url), "g"), maskDatabaseUrl(url));
|
|
67
|
+
try {
|
|
68
|
+
const password = new URL(url).password;
|
|
69
|
+
if (password.length >= 4) {
|
|
70
|
+
sanitized = sanitized.replace(
|
|
71
|
+
new RegExp(escapeRegExp(decodeURIComponent(password)), "g"),
|
|
72
|
+
"***"
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
} catch {
|
|
76
|
+
}
|
|
42
77
|
}
|
|
43
|
-
|
|
44
|
-
|
|
78
|
+
return sanitized.replace(
|
|
79
|
+
/(postgres(?:ql)?:\/\/[^:/\s]+:)[^@\s]+(@)/gi,
|
|
80
|
+
"$1***$2"
|
|
81
|
+
);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
// src/cli/utils/process.ts
|
|
85
|
+
var isChildProcessRunning = (childProcess) => {
|
|
86
|
+
return childProcess.exitCode === null && childProcess.signalCode === null;
|
|
87
|
+
};
|
|
88
|
+
var terminateChildProcess = (childProcess, options = {}) => {
|
|
89
|
+
if (!isChildProcessRunning(childProcess)) {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
const signal = options.signal || "SIGTERM";
|
|
93
|
+
try {
|
|
94
|
+
if (options.processGroup && childProcess.pid && process.platform !== "win32") {
|
|
95
|
+
try {
|
|
96
|
+
process.kill(-childProcess.pid, signal);
|
|
97
|
+
return true;
|
|
98
|
+
} catch {
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return childProcess.kill(signal);
|
|
102
|
+
} catch {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
var waitForChildProcessClose = (childProcess, timeoutMs) => {
|
|
107
|
+
return new Promise((resolve) => {
|
|
108
|
+
if (!isChildProcessRunning(childProcess)) {
|
|
109
|
+
resolve(true);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const timeout = setTimeout(() => {
|
|
113
|
+
cleanup();
|
|
114
|
+
resolve(false);
|
|
115
|
+
}, timeoutMs);
|
|
116
|
+
const onClose = () => {
|
|
117
|
+
clearTimeout(timeout);
|
|
118
|
+
cleanup();
|
|
119
|
+
resolve(true);
|
|
120
|
+
};
|
|
121
|
+
const cleanup = () => {
|
|
122
|
+
childProcess.off("close", onClose);
|
|
123
|
+
childProcess.off("exit", onClose);
|
|
124
|
+
};
|
|
125
|
+
childProcess.once("close", onClose);
|
|
126
|
+
childProcess.once("exit", onClose);
|
|
127
|
+
});
|
|
45
128
|
};
|
|
46
|
-
var isVerboseEnv = (names, env = process.env) => names.some((name) => parseBooleanEnv(name, false, env));
|
|
47
129
|
|
|
48
130
|
// src/cli/utils/command.ts
|
|
49
131
|
var getNpxCommand = () => process.platform === "win32" ? "npx.cmd" : "npx";
|
|
132
|
+
var sanitizeCommandOutput = (output, env = process.env) => {
|
|
133
|
+
const databaseUrls = Object.entries(env).filter(
|
|
134
|
+
([name, value]) => Boolean(value) && (name === "DATABASE_URL" || /^SHARD_\d+_URL$/.test(name))
|
|
135
|
+
).map(([, value]) => value);
|
|
136
|
+
return sanitizeDatabaseText(output, databaseUrls).replace(/(\bpassword\s*[=:]\s*)[^\s,;]+/gi, "$1***");
|
|
137
|
+
};
|
|
50
138
|
|
|
51
139
|
// src/cli/utils/ports.ts
|
|
52
140
|
var import_http = __toESM(require("http"));
|
|
@@ -205,52 +293,6 @@ var waitForPrismaStudio = async (port, timeoutMs, intervalMs = 400, shouldContin
|
|
|
205
293
|
return lastProbe;
|
|
206
294
|
};
|
|
207
295
|
|
|
208
|
-
// src/cli/utils/process.ts
|
|
209
|
-
var isChildProcessRunning = (childProcess) => {
|
|
210
|
-
return childProcess.exitCode === null && childProcess.signalCode === null && !childProcess.killed;
|
|
211
|
-
};
|
|
212
|
-
var terminateChildProcess = (childProcess, options = {}) => {
|
|
213
|
-
if (!isChildProcessRunning(childProcess)) {
|
|
214
|
-
return false;
|
|
215
|
-
}
|
|
216
|
-
const signal = options.signal || "SIGTERM";
|
|
217
|
-
try {
|
|
218
|
-
if (options.processGroup && childProcess.pid && process.platform !== "win32") {
|
|
219
|
-
try {
|
|
220
|
-
process.kill(-childProcess.pid, signal);
|
|
221
|
-
return true;
|
|
222
|
-
} catch {
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
return childProcess.kill(signal);
|
|
226
|
-
} catch {
|
|
227
|
-
return false;
|
|
228
|
-
}
|
|
229
|
-
};
|
|
230
|
-
var waitForChildProcessClose = (childProcess, timeoutMs) => {
|
|
231
|
-
return new Promise((resolve) => {
|
|
232
|
-
if (!isChildProcessRunning(childProcess)) {
|
|
233
|
-
resolve(true);
|
|
234
|
-
return;
|
|
235
|
-
}
|
|
236
|
-
const timeout = setTimeout(() => {
|
|
237
|
-
cleanup();
|
|
238
|
-
resolve(false);
|
|
239
|
-
}, timeoutMs);
|
|
240
|
-
const onClose = () => {
|
|
241
|
-
clearTimeout(timeout);
|
|
242
|
-
cleanup();
|
|
243
|
-
resolve(true);
|
|
244
|
-
};
|
|
245
|
-
const cleanup = () => {
|
|
246
|
-
childProcess.off("close", onClose);
|
|
247
|
-
childProcess.off("exit", onClose);
|
|
248
|
-
};
|
|
249
|
-
childProcess.once("close", onClose);
|
|
250
|
-
childProcess.once("exit", onClose);
|
|
251
|
-
});
|
|
252
|
-
};
|
|
253
|
-
|
|
254
296
|
// src/cli/utils/shards.ts
|
|
255
297
|
var NO_SHARDS_CONFIGURED_MESSAGE = "No shards configured. Set SHARD_COUNT and SHARD_N_URL environment variables.";
|
|
256
298
|
var getShardConfigResult = (env = process.env) => {
|
|
@@ -270,34 +312,60 @@ var getShardConfigResult = (env = process.env) => {
|
|
|
270
312
|
}
|
|
271
313
|
return { shards, missingShardIds };
|
|
272
314
|
};
|
|
273
|
-
var maskShardUrl =
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
return
|
|
280
|
-
}
|
|
281
|
-
|
|
315
|
+
var maskShardUrl = maskDatabaseUrl;
|
|
316
|
+
|
|
317
|
+
// src/utils/env.ts
|
|
318
|
+
var parseBooleanEnv = (name, defaultValue, env = process.env) => {
|
|
319
|
+
const value = env[name]?.trim();
|
|
320
|
+
if (value === void 0 || value === "") {
|
|
321
|
+
return defaultValue;
|
|
322
|
+
}
|
|
323
|
+
return ["1", "true", "yes", "on"].includes(value.toLowerCase());
|
|
324
|
+
};
|
|
325
|
+
var parsePositiveIntegerEnv = (name, defaultValue, env = process.env) => {
|
|
326
|
+
const value = env[name]?.trim();
|
|
327
|
+
if (!value) {
|
|
328
|
+
return defaultValue;
|
|
282
329
|
}
|
|
330
|
+
const parsed = parseInt(value, 10);
|
|
331
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : defaultValue;
|
|
283
332
|
};
|
|
333
|
+
var isVerboseEnv = (names, env = process.env) => names.some((name) => parseBooleanEnv(name, false, env));
|
|
284
334
|
|
|
285
|
-
// src/cli/studio.ts
|
|
335
|
+
// src/cli/utils/studio-options.ts
|
|
336
|
+
var getStudioOptions = () => ({
|
|
337
|
+
basePort: parsePositiveIntegerEnv(
|
|
338
|
+
"SHARD_STUDIO_BASE_PORT",
|
|
339
|
+
INTERNAL_DEFAULTS.STUDIO_BASE_PORT
|
|
340
|
+
),
|
|
341
|
+
reuseExisting: parseBooleanEnv(
|
|
342
|
+
"SHARD_STUDIO_REUSE_EXISTING",
|
|
343
|
+
INTERNAL_DEFAULTS.STUDIO_REUSE_EXISTING
|
|
344
|
+
),
|
|
345
|
+
startupTimeoutMs: parsePositiveIntegerEnv(
|
|
346
|
+
"SHARD_STUDIO_START_TIMEOUT_MS",
|
|
347
|
+
INTERNAL_DEFAULTS.STUDIO_START_TIMEOUT_MS
|
|
348
|
+
),
|
|
349
|
+
stabilityMs: parsePositiveIntegerEnv(
|
|
350
|
+
"SHARD_STUDIO_STABILITY_MS",
|
|
351
|
+
INTERNAL_DEFAULTS.STUDIO_STABILITY_MS
|
|
352
|
+
),
|
|
353
|
+
shutdownTimeoutMs: parsePositiveIntegerEnv(
|
|
354
|
+
"SHARD_STUDIO_SHUTDOWN_TIMEOUT_MS",
|
|
355
|
+
INTERNAL_DEFAULTS.STUDIO_SHUTDOWN_TIMEOUT_MS
|
|
356
|
+
),
|
|
357
|
+
strictPortCheck: parseBooleanEnv(
|
|
358
|
+
"SHARD_STUDIO_STRICT_PORT_CHECK",
|
|
359
|
+
INTERNAL_DEFAULTS.STUDIO_STRICT_PORT_CHECK
|
|
360
|
+
),
|
|
361
|
+
verbose: isVerboseEnv(["SHARD_STUDIO_VERBOSE", "SHARD_STUDIO_DEBUG"])
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
// src/cli/utils/studio-runner.ts
|
|
286
365
|
var instances = [];
|
|
287
366
|
var activeOptions;
|
|
288
367
|
var isShuttingDown = false;
|
|
289
368
|
var keepAliveTimer;
|
|
290
|
-
var getStudioOptions = () => {
|
|
291
|
-
return {
|
|
292
|
-
basePort: parsePositiveIntegerEnv("SHARD_STUDIO_BASE_PORT", 51212),
|
|
293
|
-
reuseExisting: parseBooleanEnv("SHARD_STUDIO_REUSE_EXISTING", true),
|
|
294
|
-
startupTimeoutMs: parsePositiveIntegerEnv("SHARD_STUDIO_START_TIMEOUT_MS", 15e3),
|
|
295
|
-
stabilityMs: parsePositiveIntegerEnv("SHARD_STUDIO_STABILITY_MS", 500),
|
|
296
|
-
shutdownTimeoutMs: parsePositiveIntegerEnv("SHARD_STUDIO_SHUTDOWN_TIMEOUT_MS", 5e3),
|
|
297
|
-
strictPortCheck: parseBooleanEnv("SHARD_STUDIO_STRICT_PORT_CHECK", false),
|
|
298
|
-
verbose: isVerboseEnv(["SHARD_STUDIO_VERBOSE", "SHARD_STUDIO_DEBUG"])
|
|
299
|
-
};
|
|
300
|
-
};
|
|
301
369
|
var studioUrl = (port) => `http://localhost:${port}`;
|
|
302
370
|
var isAddressInUseOutput = (output) => output.includes("EADDRINUSE");
|
|
303
371
|
var outputLooksReady = (output, port) => {
|
|
@@ -309,11 +377,11 @@ var logVerbose = (options, message, writer = console.log) => {
|
|
|
309
377
|
writer(message);
|
|
310
378
|
}
|
|
311
379
|
};
|
|
312
|
-
var writeVerboseOutput = (options, shardId, output, writer) => {
|
|
380
|
+
var writeVerboseOutput = (options, shardId, shardUrl, output, writer) => {
|
|
313
381
|
if (!options.verbose) {
|
|
314
382
|
return;
|
|
315
383
|
}
|
|
316
|
-
output.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).forEach((line) => writer(` [${shardId}] ${line}`));
|
|
384
|
+
sanitizeCommandOutput(output, { DATABASE_URL: shardUrl }).split(/\r?\n/).map((line) => line.trim()).filter(Boolean).forEach((line) => writer(` [${shardId}] ${line}`));
|
|
317
385
|
};
|
|
318
386
|
var failedInstance = (shard, port, message, details, severity = "error") => {
|
|
319
387
|
return {
|
|
@@ -429,7 +497,7 @@ var startSpawnedStudio = (shard, port, options) => {
|
|
|
429
497
|
if (outputLooksReady(output, port)) {
|
|
430
498
|
settleStartedAfterStability();
|
|
431
499
|
}
|
|
432
|
-
writeVerboseOutput(options, shardId, output, console.log);
|
|
500
|
+
writeVerboseOutput(options, shardId, shard.url, output, console.log);
|
|
433
501
|
});
|
|
434
502
|
studioProcess.stderr?.on("data", (data) => {
|
|
435
503
|
const output = data.toString();
|
|
@@ -438,7 +506,7 @@ var startSpawnedStudio = (shard, port, options) => {
|
|
|
438
506
|
addressInUse = true;
|
|
439
507
|
return;
|
|
440
508
|
}
|
|
441
|
-
writeVerboseOutput(options, shardId, output, console.error);
|
|
509
|
+
writeVerboseOutput(options, shardId, shard.url, output, console.error);
|
|
442
510
|
});
|
|
443
511
|
studioProcess.on("error", (err) => {
|
|
444
512
|
fail("Failed to start", err.message);
|
package/dist/cli/test.js
CHANGED
|
@@ -26,6 +26,92 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
26
26
|
// src/cli/test.ts
|
|
27
27
|
var import_config = require("dotenv/config");
|
|
28
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
|
+
|
|
29
115
|
// src/cli/utils/shards.ts
|
|
30
116
|
var NO_SHARDS_CONFIGURED_MESSAGE = "No shards configured. Set SHARD_COUNT and SHARD_N_URL environment variables.";
|
|
31
117
|
var getShardConfigResult = (env = process.env) => {
|
|
@@ -45,9 +131,6 @@ var getShardConfigResult = (env = process.env) => {
|
|
|
45
131
|
}
|
|
46
132
|
return { shards, missingShardIds };
|
|
47
133
|
};
|
|
48
|
-
var getShardConfigs = (env = process.env) => {
|
|
49
|
-
return getShardConfigResult(env).shards;
|
|
50
|
-
};
|
|
51
134
|
|
|
52
135
|
// src/cli/test.ts
|
|
53
136
|
var results = [];
|
|
@@ -78,28 +161,11 @@ var runTest = async (name, testFn) => {
|
|
|
78
161
|
log.error(`${name}: ${message}`);
|
|
79
162
|
}
|
|
80
163
|
};
|
|
81
|
-
var
|
|
82
|
-
try {
|
|
83
|
-
const match = url.match(/postgresql:\/\/([^:]+):([^@]+)@([^:]+):(\d+)\/([^?]+)/);
|
|
84
|
-
if (match) {
|
|
85
|
-
return {
|
|
86
|
-
user: match[1],
|
|
87
|
-
password: match[2],
|
|
88
|
-
host: match[3],
|
|
89
|
-
port: parseInt(match[4], 10),
|
|
90
|
-
database: match[5]
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
return null;
|
|
94
|
-
} catch {
|
|
95
|
-
return null;
|
|
96
|
-
}
|
|
97
|
-
};
|
|
98
|
-
var testTcpConnection = (host, port) => {
|
|
164
|
+
var testTcpConnection = (urlInfo) => {
|
|
99
165
|
return new Promise((resolve) => {
|
|
100
166
|
import("net").then(({ default: net }) => {
|
|
101
167
|
const socket = new net.Socket();
|
|
102
|
-
socket.setTimeout(
|
|
168
|
+
socket.setTimeout(INTERNAL_DEFAULTS.CLI_TEST_TCP_TIMEOUT_MS);
|
|
103
169
|
socket.on("connect", () => {
|
|
104
170
|
socket.destroy();
|
|
105
171
|
resolve(true);
|
|
@@ -112,7 +178,11 @@ var testTcpConnection = (host, port) => {
|
|
|
112
178
|
socket.destroy();
|
|
113
179
|
resolve(false);
|
|
114
180
|
});
|
|
115
|
-
|
|
181
|
+
if (urlInfo.socketPath) {
|
|
182
|
+
socket.connect(`${urlInfo.socketPath}/.s.PGSQL.${urlInfo.port}`);
|
|
183
|
+
} else {
|
|
184
|
+
socket.connect(urlInfo.port, urlInfo.host);
|
|
185
|
+
}
|
|
116
186
|
}).catch(() => resolve(false));
|
|
117
187
|
});
|
|
118
188
|
};
|
|
@@ -124,15 +194,19 @@ var getPgClient = async () => {
|
|
|
124
194
|
return null;
|
|
125
195
|
}
|
|
126
196
|
};
|
|
127
|
-
var executeSql = async (url,
|
|
197
|
+
var executeSql = async (url, statement) => {
|
|
128
198
|
const Client = await getPgClient();
|
|
199
|
+
const normalized = typeof statement === "string" ? { text: statement } : statement;
|
|
129
200
|
if (!Client) {
|
|
130
|
-
|
|
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);
|
|
131
205
|
}
|
|
132
206
|
const client = new Client({ connectionString: url });
|
|
133
207
|
try {
|
|
134
208
|
await client.connect();
|
|
135
|
-
const result = await client.query(
|
|
209
|
+
const result = await client.query(normalized.text, normalized.values);
|
|
136
210
|
await client.end();
|
|
137
211
|
return { success: true, rows: result.rows };
|
|
138
212
|
} catch (error) {
|
|
@@ -140,14 +214,18 @@ var executeSql = async (url, sql) => {
|
|
|
140
214
|
await client.end();
|
|
141
215
|
} catch {
|
|
142
216
|
}
|
|
143
|
-
|
|
217
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
218
|
+
return {
|
|
219
|
+
success: false,
|
|
220
|
+
error: sanitizeCommandOutput(message, { DATABASE_URL: url })
|
|
221
|
+
};
|
|
144
222
|
}
|
|
145
223
|
};
|
|
146
224
|
var executeSqlWithPsql = (url, sql) => {
|
|
147
225
|
return new Promise((resolve) => {
|
|
148
226
|
import("child_process").then(({ spawn }) => {
|
|
149
227
|
const psql = spawn("psql", [url, "-c", sql], {
|
|
150
|
-
shell:
|
|
228
|
+
shell: false,
|
|
151
229
|
stdio: ["pipe", "pipe", "pipe"]
|
|
152
230
|
});
|
|
153
231
|
let stderr = "";
|
|
@@ -157,14 +235,21 @@ var executeSqlWithPsql = (url, sql) => {
|
|
|
157
235
|
const timeout = setTimeout(() => {
|
|
158
236
|
psql.kill();
|
|
159
237
|
resolve({ success: false, error: "Command timeout" });
|
|
160
|
-
},
|
|
238
|
+
}, INTERNAL_DEFAULTS.CLI_TEST_COMMAND_TIMEOUT_MS);
|
|
239
|
+
timeout.unref?.();
|
|
161
240
|
psql.on("close", (code) => {
|
|
162
241
|
clearTimeout(timeout);
|
|
163
|
-
resolve({
|
|
242
|
+
resolve({
|
|
243
|
+
success: code === 0,
|
|
244
|
+
error: code !== 0 ? sanitizeCommandOutput(stderr, { DATABASE_URL: url }) : void 0
|
|
245
|
+
});
|
|
164
246
|
});
|
|
165
247
|
psql.on("error", (err) => {
|
|
166
248
|
clearTimeout(timeout);
|
|
167
|
-
resolve({
|
|
249
|
+
resolve({
|
|
250
|
+
success: false,
|
|
251
|
+
error: sanitizeCommandOutput(err.message, { DATABASE_URL: url })
|
|
252
|
+
});
|
|
168
253
|
});
|
|
169
254
|
});
|
|
170
255
|
});
|
|
@@ -182,7 +267,7 @@ var getShardIndex = (key, shardCount) => {
|
|
|
182
267
|
return hashString(key) % shardCount;
|
|
183
268
|
};
|
|
184
269
|
var runTests = async () => {
|
|
185
|
-
const shards =
|
|
270
|
+
const { shards, missingShardIds } = getShardConfigResult();
|
|
186
271
|
const timestamp = Date.now();
|
|
187
272
|
const testTableName = `_prisma_sharding_test_${timestamp}`;
|
|
188
273
|
console.log("\n" + "\u2550".repeat(60));
|
|
@@ -194,6 +279,11 @@ var runTests = async () => {
|
|
|
194
279
|
console.log(` \u2022 Routing Strategy: ${process.env.SHARD_ROUTING_STRATEGY || "modulo"}`);
|
|
195
280
|
console.log(` \u2022 Test Users: ${TEST_USER_COUNT}`);
|
|
196
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
|
+
}
|
|
197
287
|
if (shards.length === 0) {
|
|
198
288
|
console.error(`
|
|
199
289
|
\u274C ${NO_SHARDS_CONFIGURED_MESSAGE}`);
|
|
@@ -220,7 +310,7 @@ var runTests = async () => {
|
|
|
220
310
|
if (!urlInfo) {
|
|
221
311
|
throw new Error(`Invalid URL format for ${shard.id}`);
|
|
222
312
|
}
|
|
223
|
-
log.detail(`${shard.id} \u2192 ${urlInfo
|
|
313
|
+
log.detail(`${shard.id} \u2192 ${postgresEndpoint(urlInfo)}/${urlInfo.database}`);
|
|
224
314
|
}
|
|
225
315
|
});
|
|
226
316
|
log.section("Test 2: Network Connectivity");
|
|
@@ -228,11 +318,11 @@ var runTests = async () => {
|
|
|
228
318
|
await runTest(`TCP connection to ${shard.id}`, async () => {
|
|
229
319
|
const urlInfo = parsePostgresUrl(shard.url);
|
|
230
320
|
if (!urlInfo) throw new Error("Invalid URL");
|
|
231
|
-
const connected = await testTcpConnection(urlInfo
|
|
321
|
+
const connected = await testTcpConnection(urlInfo);
|
|
232
322
|
if (!connected) {
|
|
233
|
-
throw new Error(`Cannot reach ${urlInfo
|
|
323
|
+
throw new Error(`Cannot reach ${postgresEndpoint(urlInfo)}`);
|
|
234
324
|
}
|
|
235
|
-
log.detail(`${urlInfo
|
|
325
|
+
log.detail(`${postgresEndpoint(urlInfo)} is reachable`);
|
|
236
326
|
});
|
|
237
327
|
}
|
|
238
328
|
log.section("Test 3: Database Connection");
|
|
@@ -275,10 +365,17 @@ var runTests = async () => {
|
|
|
275
365
|
const username = `testuser_${i}`;
|
|
276
366
|
const shardIndex = getShardIndex(userId, shards.length);
|
|
277
367
|
const targetShard = shards[shardIndex];
|
|
278
|
-
const
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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
|
+
};
|
|
282
379
|
const result = await executeSql(targetShard.url, insertSql);
|
|
283
380
|
if (result.success) {
|
|
284
381
|
testUsers.push({ id: userId, email, shardId: targetShard.id });
|
|
@@ -329,7 +426,14 @@ var runTests = async () => {
|
|
|
329
426
|
const user = testUsers[i];
|
|
330
427
|
const shard = shards.find((s) => s.id === user.shardId);
|
|
331
428
|
if (!shard) continue;
|
|
332
|
-
|
|
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
|
+
};
|
|
333
437
|
const result = await executeSql(shard.url, selectSql);
|
|
334
438
|
if (result.success && result.rows && result.rows.length > 0) {
|
|
335
439
|
verified++;
|