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/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 parsePostgresUrl = (url) => {
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(5e3);
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
- 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
+ }
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, sql) => {
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
- 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);
120
205
  }
121
206
  const client = new Client({ connectionString: url });
122
207
  try {
123
208
  await client.connect();
124
- const result = await client.query(sql);
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
- 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
+ };
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: true,
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
- }, 15e3);
238
+ }, INTERNAL_DEFAULTS.CLI_TEST_COMMAND_TIMEOUT_MS);
239
+ timeout.unref?.();
154
240
  psql.on("close", (code) => {
155
241
  clearTimeout(timeout);
156
- 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
+ });
157
246
  });
158
247
  psql.on("error", (err) => {
159
248
  clearTimeout(timeout);
160
- resolve({ success: false, error: err.message });
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 = getShardConfigs();
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
- "\n\u274C No shards configured. Set SHARD_COUNT and SHARD_N_URL environment variables."
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.host}:${urlInfo.port}/${urlInfo.database}`);
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.host, urlInfo.port);
321
+ const connected = await testTcpConnection(urlInfo);
226
322
  if (!connected) {
227
- throw new Error(`Cannot reach ${urlInfo.host}:${urlInfo.port}`);
323
+ throw new Error(`Cannot reach ${postgresEndpoint(urlInfo)}`);
228
324
  }
229
- log.detail(`${urlInfo.host}:${urlInfo.port} is reachable`);
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 insertSql = `
273
- INSERT INTO "${testTableName}" (id, email, username)
274
- VALUES ('${userId}', '${email}', '${username}');
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
- 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
+ };
327
437
  const result = await executeSql(shard.url, selectSql);
328
438
  if (result.success && result.rows && result.rows.length > 0) {
329
439
  verified++;