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.
@@ -25,103 +25,360 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
25
25
 
26
26
  // src/cli/migrate.ts
27
27
  var import_config = require("dotenv/config");
28
+
29
+ // src/utils/env.ts
30
+ var parseBooleanEnv = (name, defaultValue, env = process.env) => {
31
+ const value = env[name]?.trim();
32
+ if (value === void 0 || value === "") {
33
+ return defaultValue;
34
+ }
35
+ return ["1", "true", "yes", "on"].includes(value.toLowerCase());
36
+ };
37
+ var isVerboseEnv = (names, env = process.env) => names.some((name) => parseBooleanEnv(name, false, env));
38
+
39
+ // src/cli/utils/output.ts
40
+ var import_readline = __toESM(require("readline"));
41
+ var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
42
+ var printCliHeader = (icon, title) => {
43
+ console.log(`${icon} ${title}
44
+ `);
45
+ };
46
+ var printCliRow = (icon, label, message) => {
47
+ console.log(`${icon} ${label} ${message}`);
48
+ };
49
+ var printVerboseHint = () => {
50
+ console.log("\nRun with SHARD_CLI_VERBOSE=true for details.");
51
+ };
52
+ var createCliLoader = (label, message, enabled = true) => {
53
+ const animated = enabled && Boolean(process.stdout.isTTY);
54
+ let frameIndex = 0;
55
+ let timer;
56
+ let finished = false;
57
+ const clear = () => {
58
+ if (animated) {
59
+ import_readline.default.clearLine(process.stdout, 0);
60
+ import_readline.default.cursorTo(process.stdout, 0);
61
+ }
62
+ };
63
+ const render = () => {
64
+ clear();
65
+ process.stdout.write(`${SPINNER_FRAMES[frameIndex]} ${label} ${message}`);
66
+ frameIndex = (frameIndex + 1) % SPINNER_FRAMES.length;
67
+ };
68
+ if (animated) {
69
+ render();
70
+ timer = setInterval(render, 80);
71
+ }
72
+ const finish = (icon, finalMessage) => {
73
+ if (finished) {
74
+ return;
75
+ }
76
+ finished = true;
77
+ if (timer) {
78
+ clearInterval(timer);
79
+ }
80
+ clear();
81
+ printCliRow(icon, label, finalMessage);
82
+ };
83
+ return {
84
+ succeed: (finalMessage) => finish("\u2705", finalMessage),
85
+ fail: (finalMessage) => finish("\u274C", finalMessage)
86
+ };
87
+ };
88
+
89
+ // src/cli/utils/command.ts
28
90
  var import_child_process = require("child_process");
29
91
  var import_path = __toESM(require("path"));
30
- var getShardConfigs = () => {
31
- const shards = [];
32
- const shardCount = parseInt(process.env.SHARD_COUNT || "0", 10);
33
- for (let i = 1; i <= shardCount; i++) {
34
- const url = process.env[`SHARD_${i}_URL`];
35
- if (url) {
36
- shards.push({ id: `shard_${i}`, url });
92
+
93
+ // src/constants/internal.ts
94
+ var INTERNAL_DEFAULTS = {
95
+ HEALTH_CHECK_TIMEOUT_MS: 5e3,
96
+ CROSS_SHARD_CONCURRENCY: 4,
97
+ CROSS_SHARD_TIMEOUT_MS: 1e4,
98
+ CLI_COMMAND_TIMEOUT_MS: 12e4,
99
+ CLI_FORCE_KILL_GRACE_MS: 5e3,
100
+ CLI_MAX_OUTPUT_LENGTH: 1e6,
101
+ CLI_TEST_TCP_TIMEOUT_MS: 5e3,
102
+ CLI_TEST_COMMAND_TIMEOUT_MS: 15e3,
103
+ STUDIO_BASE_PORT: 51212,
104
+ STUDIO_REUSE_EXISTING: true,
105
+ STUDIO_STRICT_PORT_CHECK: false,
106
+ STUDIO_START_TIMEOUT_MS: 15e3,
107
+ STUDIO_STABILITY_MS: 500,
108
+ STUDIO_SHUTDOWN_TIMEOUT_MS: 5e3
109
+ };
110
+
111
+ // src/utils/sanitize.ts
112
+ var escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
113
+ var maskDatabaseUrl = (url) => {
114
+ try {
115
+ const parsed = new URL(url);
116
+ if (parsed.password) {
117
+ parsed.password = "***";
37
118
  }
119
+ return parsed.toString();
120
+ } catch {
121
+ return url.replace(/:[^:@]+@/, ":***@");
38
122
  }
39
- if (shards.length === 0 && process.env.DATABASE_URL) {
40
- shards.push({ id: "shard_1", url: process.env.DATABASE_URL });
123
+ };
124
+ var sanitizeDatabaseText = (output, databaseUrls) => {
125
+ let sanitized = output;
126
+ for (const url of databaseUrls) {
127
+ sanitized = sanitized.replace(new RegExp(escapeRegExp(url), "g"), maskDatabaseUrl(url));
128
+ try {
129
+ const password = new URL(url).password;
130
+ if (password.length >= 4) {
131
+ sanitized = sanitized.replace(
132
+ new RegExp(escapeRegExp(decodeURIComponent(password)), "g"),
133
+ "***"
134
+ );
135
+ }
136
+ } catch {
137
+ }
138
+ }
139
+ return sanitized.replace(
140
+ /(postgres(?:ql)?:\/\/[^:/\s]+:)[^@\s]+(@)/gi,
141
+ "$1***$2"
142
+ );
143
+ };
144
+
145
+ // src/cli/utils/process.ts
146
+ var isChildProcessRunning = (childProcess) => {
147
+ return childProcess.exitCode === null && childProcess.signalCode === null;
148
+ };
149
+ var terminateChildProcess = (childProcess, options = {}) => {
150
+ if (!isChildProcessRunning(childProcess)) {
151
+ return false;
152
+ }
153
+ const signal = options.signal || "SIGTERM";
154
+ try {
155
+ if (options.processGroup && childProcess.pid && process.platform !== "win32") {
156
+ try {
157
+ process.kill(-childProcess.pid, signal);
158
+ return true;
159
+ } catch {
160
+ }
161
+ }
162
+ return childProcess.kill(signal);
163
+ } catch {
164
+ return false;
41
165
  }
42
- return shards;
43
166
  };
44
- var runPrismaCommand = (shardUrl, command) => {
167
+
168
+ // src/cli/utils/command.ts
169
+ var getNpxCommand = () => process.platform === "win32" ? "npx.cmd" : "npx";
170
+ var OUTPUT_TRUNCATED_MARKER = "\n[output truncated]\n";
171
+ var appendBoundedOutput = (current, chunk, limit) => {
172
+ const combined = current + chunk;
173
+ if (combined.length <= limit) {
174
+ return combined;
175
+ }
176
+ if (limit <= OUTPUT_TRUNCATED_MARKER.length) {
177
+ return combined.slice(-limit);
178
+ }
179
+ return OUTPUT_TRUNCATED_MARKER + combined.slice(-(limit - OUTPUT_TRUNCATED_MARKER.length));
180
+ };
181
+ var sanitizeCommandOutput = (output, env = process.env) => {
182
+ const databaseUrls = Object.entries(env).filter(
183
+ ([name, value]) => Boolean(value) && (name === "DATABASE_URL" || /^SHARD_\d+_URL$/.test(name))
184
+ ).map(([, value]) => value);
185
+ return sanitizeDatabaseText(output, databaseUrls).replace(/(\bpassword\s*[=:]\s*)[^\s,;]+/gi, "$1***");
186
+ };
187
+ var runCommand = (command, args, options = {}) => {
45
188
  return new Promise((resolve) => {
46
- const env = {
47
- ...process.env,
48
- DATABASE_URL: shardUrl
49
- };
50
- const prisma = (0, import_child_process.spawn)("npx", ["prisma", ...command], {
189
+ let stdout = "";
190
+ let stderr = "";
191
+ let settled = false;
192
+ let timedOut = false;
193
+ let forceKillTimeout;
194
+ const env = options.env || process.env;
195
+ const timeoutMs = options.timeoutMs ?? INTERNAL_DEFAULTS.CLI_COMMAND_TIMEOUT_MS;
196
+ const forceKillGraceMs = options.forceKillGraceMs ?? INTERNAL_DEFAULTS.CLI_FORCE_KILL_GRACE_MS;
197
+ const maxOutputLength = options.maxOutputLength ?? INTERNAL_DEFAULTS.CLI_MAX_OUTPUT_LENGTH;
198
+ const processGroup = process.platform !== "win32";
199
+ const child = (0, import_child_process.spawn)(command, args, {
51
200
  env,
52
- cwd: import_path.default.resolve(process.cwd()),
53
- shell: true
201
+ cwd: import_path.default.resolve(options.cwd || process.cwd()),
202
+ detached: processGroup,
203
+ shell: false,
204
+ stdio: ["ignore", "pipe", "pipe"]
54
205
  });
55
- let output = "";
56
- let errorOutput = "";
57
- prisma.stdout.on("data", (data) => {
58
- output += data.toString();
59
- });
60
- prisma.stderr.on("data", (data) => {
61
- errorOutput += data.toString();
206
+ const settle = (result) => {
207
+ if (!settled) {
208
+ settled = true;
209
+ clearTimeout(timeout);
210
+ if (forceKillTimeout) {
211
+ clearTimeout(forceKillTimeout);
212
+ }
213
+ const sanitizedResult = {
214
+ ...result,
215
+ stdout: sanitizeCommandOutput(result.stdout, env),
216
+ stderr: sanitizeCommandOutput(result.stderr, env),
217
+ error: result.error ? sanitizeCommandOutput(result.error, env) : void 0
218
+ };
219
+ if (options.verbose) {
220
+ if (sanitizedResult.stdout) {
221
+ process.stdout.write(sanitizedResult.stdout);
222
+ }
223
+ if (sanitizedResult.stderr) {
224
+ process.stderr.write(sanitizedResult.stderr);
225
+ }
226
+ }
227
+ resolve(sanitizedResult);
228
+ }
229
+ };
230
+ child.stdout?.on("data", (data) => {
231
+ if (!settled) {
232
+ stdout = appendBoundedOutput(stdout, data.toString(), maxOutputLength);
233
+ }
62
234
  });
63
- prisma.on("close", (code) => {
64
- if (code === 0) {
65
- resolve({ success: true, output });
66
- } else {
67
- resolve({ success: false, output, error: errorOutput || `Exit code: ${code}` });
235
+ child.stderr?.on("data", (data) => {
236
+ if (!settled) {
237
+ stderr = appendBoundedOutput(stderr, data.toString(), maxOutputLength);
68
238
  }
69
239
  });
70
- prisma.on("error", (err) => {
71
- resolve({ success: false, output, error: err.message });
240
+ child.once("error", (error) => {
241
+ settle({
242
+ success: false,
243
+ stdout,
244
+ stderr,
245
+ exitCode: null,
246
+ error: timedOut ? `Command timed out after ${timeoutMs}ms` : error.message
247
+ });
72
248
  });
249
+ child.once("close", (exitCode) => {
250
+ settle({
251
+ success: !timedOut && exitCode === 0,
252
+ stdout,
253
+ stderr,
254
+ exitCode,
255
+ error: timedOut ? `Command timed out after ${timeoutMs}ms` : exitCode === 0 ? void 0 : stderr.trim() || stdout.trim() || `Command exited with code ${exitCode}`
256
+ });
257
+ });
258
+ const timeout = setTimeout(() => {
259
+ timedOut = true;
260
+ terminateChildProcess(child, { signal: "SIGTERM", processGroup });
261
+ forceKillTimeout = setTimeout(() => {
262
+ terminateChildProcess(child, { signal: "SIGKILL", processGroup });
263
+ settle({
264
+ success: false,
265
+ stdout,
266
+ stderr,
267
+ exitCode: null,
268
+ error: `Command timed out after ${timeoutMs}ms`
269
+ });
270
+ }, forceKillGraceMs);
271
+ forceKillTimeout.unref?.();
272
+ }, timeoutMs);
273
+ timeout.unref?.();
73
274
  });
74
275
  };
75
- var migrateAllShards = async () => {
76
- const shards = getShardConfigs();
77
- const extraArgs = process.argv.slice(2);
78
- console.log("\u{1F504} prisma-sharding: Starting migrations...\n");
79
- console.log(`\u{1F4CA} Total shards to migrate: ${shards.length}`);
80
- if (extraArgs.length > 0) {
81
- console.log(`\u{1F527} Using flags: ${extraArgs.join(" ")}`);
276
+ var runPrismaCommand = (args, options = {}) => runCommand(getNpxCommand(), ["prisma", ...args], options);
277
+
278
+ // src/cli/utils/shards.ts
279
+ var NO_SHARDS_CONFIGURED_MESSAGE = "No shards configured. Set SHARD_COUNT and SHARD_N_URL environment variables.";
280
+ var getShardConfigResult = (env = process.env) => {
281
+ const shards = [];
282
+ const missingShardIds = [];
283
+ const shardCount = parseInt(env.SHARD_COUNT || "0", 10);
284
+ for (let i = 1; i <= shardCount; i++) {
285
+ const url = env[`SHARD_${i}_URL`];
286
+ if (url) {
287
+ shards.push({ id: `shard_${i}`, index: i - 1, url });
288
+ } else {
289
+ missingShardIds.push(`shard_${i}`);
290
+ }
82
291
  }
83
- console.log("");
84
- if (shards.length === 0) {
85
- console.error(
86
- "\u274C No shards configured. Set SHARD_COUNT and SHARD_N_URL environment variables."
292
+ if (shards.length === 0 && env.DATABASE_URL) {
293
+ shards.push({ id: "shard_1", index: 0, url: env.DATABASE_URL });
294
+ }
295
+ return { shards, missingShardIds };
296
+ };
297
+ var maskShardUrl = maskDatabaseUrl;
298
+
299
+ // src/cli/utils/prisma.ts
300
+ var executeShardCommand = async (shard, commandArgs, verbose, action) => {
301
+ const commandEnv = { ...process.env, DATABASE_URL: shard.url };
302
+ if (verbose) {
303
+ console.log(`
304
+ ${action} ${shard.id}...`);
305
+ console.log(`Database: ${maskShardUrl(shard.url)}
306
+ `);
307
+ console.log(
308
+ `Command: ${sanitizeCommandOutput(`prisma ${commandArgs.join(" ")}`, commandEnv)}`
87
309
  );
88
- process.exit(1);
89
310
  }
311
+ const result = await runPrismaCommand(commandArgs, {
312
+ env: commandEnv,
313
+ verbose
314
+ });
315
+ if (verbose) {
316
+ console.log(`Exit code: ${result.exitCode ?? "unavailable"}`);
317
+ }
318
+ if (verbose && result.error && !result.stderr.trim() && !result.stdout.trim()) {
319
+ console.error(result.error);
320
+ }
321
+ if (verbose && !result.success) {
322
+ console.error(`Next: verify ${shard.id} connectivity and migration state, then retry.`);
323
+ }
324
+ return result;
325
+ };
326
+ var deployShardMigrations = async (shards, extraArgs, verbose) => {
90
327
  const results = [];
91
328
  for (const shard of shards) {
92
- console.log(`
93
- \u{1F4E6} Migrating ${shard.id}...`);
94
- console.log(` URL: ${shard.url.replace(/:[^:@]+@/, ":***@")}`);
95
- const args = ["db", "push", "--accept-data-loss", ...extraArgs];
96
- const { success, output, error } = await runPrismaCommand(shard.url, args);
97
- if (!success) {
98
- console.error(` \u274C Failed:`);
99
- console.error(error?.replace(/Check logs above/g, "").trim());
100
- results.push({ shardId: shard.id, success: false, output, error });
329
+ const loader = createCliLoader(shard.id, "Migrating", !verbose);
330
+ const status = await executeShardCommand(
331
+ shard,
332
+ ["migrate", "status"],
333
+ verbose,
334
+ "Checking migration status for"
335
+ );
336
+ if (!status.success) {
337
+ results.push({ shardId: shard.id, success: false });
338
+ loader.fail("Failed");
339
+ continue;
340
+ }
341
+ const deploy = await executeShardCommand(
342
+ shard,
343
+ ["migrate", "deploy", ...extraArgs],
344
+ verbose,
345
+ "Migrating"
346
+ );
347
+ results.push({ shardId: shard.id, success: deploy.success });
348
+ if (deploy.success) {
349
+ loader.succeed("Synced");
101
350
  } else {
102
- console.log(` \u2705 Success`);
103
- results.push({ shardId: shard.id, success: true, output });
351
+ loader.fail("Failed");
104
352
  }
105
353
  }
106
- console.log("\n" + "=".repeat(50));
107
- console.log("\u{1F4CB} Migration Summary\n");
108
- const successful = results.filter((r) => r.success).length;
109
- const failed = results.filter((r) => !r.success).length;
110
- results.forEach((result) => {
111
- const status = result.success ? "\u2705" : "\u274C";
112
- console.log(
113
- ` ${status} ${result.shardId}`
114
- );
115
- });
116
- console.log(`
117
- Total: ${results.length} | Success: ${successful} | Failed: ${failed}`);
354
+ return results;
355
+ };
356
+
357
+ // src/cli/migrate.ts
358
+ var migrateAllShards = async () => {
359
+ const verbose = isVerboseEnv(["SHARD_MIGRATE_VERBOSE", "SHARD_CLI_VERBOSE"]);
360
+ const { shards, missingShardIds } = getShardConfigResult();
361
+ const extraArgs = process.argv.slice(2);
362
+ printCliHeader("\u{1F504}", "Prisma Sharding Migrate");
363
+ if (missingShardIds.length > 0) {
364
+ printCliRow("\u274C", "config", `Missing shard URLs: ${missingShardIds.join(", ")}`);
365
+ process.exit(1);
366
+ }
367
+ if (shards.length === 0) {
368
+ printCliRow("\u274C", "config", NO_SHARDS_CONFIGURED_MESSAGE);
369
+ process.exit(1);
370
+ }
371
+ const results = await deployShardMigrations(shards, extraArgs, verbose);
372
+ const successful = results.filter((result) => result.success).length;
373
+ const failed = results.length - successful;
118
374
  if (failed > 0) {
119
- console.log("\n\u26A0\uFE0F Some migrations failed. Run with --force-reset to clear incompatible data.");
375
+ if (!verbose) {
376
+ printVerboseHint();
377
+ }
120
378
  process.exit(1);
121
379
  }
122
- console.log("\n\u2705 All shard migrations completed successfully!");
123
380
  };
124
381
  migrateAllShards().catch((error) => {
125
- console.error("Migration script failed:", error);
382
+ console.error(error instanceof Error ? error.message : String(error));
126
383
  process.exit(1);
127
384
  });