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.
@@ -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/utils/env.ts
31
- var parseBooleanEnv = (name, defaultValue, env = process.env) => {
32
- const value = env[name]?.trim();
33
- if (value === void 0 || value === "") {
34
- return defaultValue;
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 parsePositiveIntegerEnv = (name, defaultValue, env = process.env) => {
39
- const value = env[name]?.trim();
40
- if (!value) {
41
- return defaultValue;
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
- const parsed = parseInt(value, 10);
44
- return Number.isFinite(parsed) && parsed > 0 ? parsed : defaultValue;
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 = (url) => {
274
- try {
275
- const parsed = new URL(url);
276
- if (parsed.password) {
277
- parsed.password = "***";
278
- }
279
- return parsed.toString();
280
- } catch {
281
- return url.replace(/:[^:@]+@/, ":***@");
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 parsePostgresUrl = (url) => {
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(5e3);
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
- socket.connect(port, host);
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, sql) => {
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
- return executeSqlWithPsql(url, sql);
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(sql);
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
- return { success: false, error: error instanceof Error ? error.message : String(error) };
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: true,
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
- }, 15e3);
238
+ }, INTERNAL_DEFAULTS.CLI_TEST_COMMAND_TIMEOUT_MS);
239
+ timeout.unref?.();
161
240
  psql.on("close", (code) => {
162
241
  clearTimeout(timeout);
163
- resolve({ success: code === 0, error: code !== 0 ? stderr : void 0 });
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({ success: false, error: err.message });
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 = getShardConfigs();
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.host}:${urlInfo.port}/${urlInfo.database}`);
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.host, urlInfo.port);
321
+ const connected = await testTcpConnection(urlInfo);
232
322
  if (!connected) {
233
- throw new Error(`Cannot reach ${urlInfo.host}:${urlInfo.port}`);
323
+ throw new Error(`Cannot reach ${postgresEndpoint(urlInfo)}`);
234
324
  }
235
- log.detail(`${urlInfo.host}:${urlInfo.port} is reachable`);
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 insertSql = `
279
- INSERT INTO "${testTableName}" (id, email, username)
280
- VALUES ('${userId}', '${email}', '${username}');
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
- const selectSql = `SELECT id, email FROM "${testTableName}" WHERE id = '${user.id}';`;
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++;