prisma-sharding 0.0.5 → 0.0.6

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 CHANGED
@@ -184,6 +184,12 @@ SHARD_2_URL=postgresql://user:pass@host:5432/db2
184
184
  SHARD_3_URL=postgresql://user:pass@host:5432/db3
185
185
  SHARD_ROUTING_STRATEGY=modulo # or consistent-hash
186
186
  SHARD_STUDIO_BASE_PORT=51212 # optional, for studio
187
+ SHARD_STUDIO_REUSE_EXISTING=true # optional, defaults to true
188
+ SHARD_STUDIO_STRICT_PORT_CHECK=false # optional, defaults to false
189
+ SHARD_STUDIO_START_TIMEOUT_MS=15000 # optional, defaults to 15000
190
+ SHARD_STUDIO_VERBOSE=false # optional, defaults to false
191
+ SHARD_CLI_VERBOSE=false # optional, verbose update/migrate output
192
+ PRISMA_SHARDING_VERBOSE=false # optional, library lifecycle logs
187
193
  ```
188
194
 
189
195
  ### Commands
@@ -201,6 +207,23 @@ yarn db:update
201
207
 
202
208
  ```
203
209
 
210
+ Default output stays compact:
211
+
212
+ ```text
213
+ 🔄 Prisma Sharding Update
214
+
215
+ ✅ client Generated
216
+ ✅ shard_1 Synced
217
+ ✅ shard_2 Synced
218
+ ✅ shard_3 Synced
219
+ ```
220
+
221
+ Interactive terminals show a single inline loader while Prisma Client generation and each shard
222
+ sync are running. The loader is replaced by the completed row and is disabled for piped or CI logs.
223
+
224
+ Set `SHARD_CLI_VERBOSE=true` or `SHARD_UPDATE_VERBOSE=true` to include Prisma command
225
+ output, masked database URLs, and detailed diagnostics.
226
+
204
227
  **With Flags:**
205
228
  You can pass flags like `--force-reset` if you need to wipe data due to schema conflicts.
206
229
 
@@ -218,14 +241,89 @@ yarn migrate:shards
218
241
 
219
242
  ```
220
243
 
244
+ This command uses the same compact shard rows as `db:update`. Set
245
+ `SHARD_CLI_VERBOSE=true` or `SHARD_MIGRATE_VERBOSE=true` for Prisma command output.
246
+
221
247
  #### `prisma-sharding-studio`
222
248
 
223
249
  Start Prisma Studio for all shards on sequential ports.
224
250
 
225
251
  ```bash
226
252
  yarn db:studio
227
- # Opens shard_1 on :51212, shard_2 on :51213, etc.
253
+ ```
254
+
255
+ By default, ports are assigned from `SHARD_STUDIO_BASE_PORT`:
228
256
 
257
+ ```text
258
+ shard_1 -> http://localhost:51212
259
+ shard_2 -> http://localhost:51213
260
+ shard_3 -> http://localhost:51214
261
+ ```
262
+
263
+ Set `SHARD_STUDIO_BASE_PORT` to move the whole range:
264
+
265
+ ```bash
266
+ SHARD_STUDIO_BASE_PORT=52000 yarn db:studio
267
+ # shard_1 -> :52000, shard_2 -> :52001, etc.
268
+ ```
269
+
270
+ Studio startup is safe to run from multiple local APIs. Before starting a shard Studio,
271
+ the CLI checks whether the target port is already active. If it finds an existing Prisma
272
+ Studio on that port, it reuses it instead of spawning another process:
273
+
274
+ ```text
275
+ 🗄️ Prisma Sharding Studio
276
+
277
+ ♻️ shard_1 http://localhost:51212
278
+ ♻️ shard_2 http://localhost:51213
279
+ ♻️ shard_3 http://localhost:51214
280
+ ```
281
+
282
+ If a port is occupied by another process that does not look like Prisma Studio, the shard
283
+ is marked with a warning and the CLI continues with the remaining shards. It will not kill,
284
+ restart, or claim ownership of processes it did not start.
285
+
286
+ Default output is intentionally compact:
287
+
288
+ ```text
289
+ 🗄️ Prisma Sharding Studio
290
+
291
+ ✅ shard_1 http://localhost:51212
292
+ ✅ shard_2 http://localhost:51213
293
+ ✅ shard_3 http://localhost:51214
294
+ ```
295
+
296
+ Run with `SHARD_STUDIO_VERBOSE=true` to print port checks, masked database URLs, Prisma
297
+ Studio child-process output, startup timings, and detailed failure diagnostics.
298
+
299
+ Useful Studio environment variables:
300
+
301
+ - `SHARD_STUDIO_BASE_PORT`: first port in the shard Studio range. Defaults to `51212`.
302
+ - `SHARD_STUDIO_REUSE_EXISTING`: reuse already-running Prisma Studio ports. Defaults to `true`.
303
+ - `SHARD_STUDIO_STRICT_PORT_CHECK`: when `true`, any failed shard makes the command exit
304
+ non-zero after stopping Studio processes started by that run. Defaults to `false`.
305
+ - `SHARD_STUDIO_START_TIMEOUT_MS`: maximum time to wait for a newly spawned Studio to become
306
+ reachable. Defaults to `15000`.
307
+ - `SHARD_STUDIO_STABILITY_MS`: short window a newly-ready Studio process must survive before
308
+ it is reported as started. Defaults to `500`.
309
+ - `SHARD_STUDIO_SHUTDOWN_TIMEOUT_MS`: time to wait for owned Studio processes to close during
310
+ shutdown before sending a force-stop signal. Defaults to `5000`.
311
+ - `SHARD_STUDIO_VERBOSE`: print detailed Studio startup diagnostics. Defaults to `false`.
312
+ - `SHARD_STUDIO_DEBUG`: alias for `SHARD_STUDIO_VERBOSE`.
313
+
314
+ When multiple APIs use the same shard configuration locally, the first API starts the Studio
315
+ processes and later APIs reuse the existing Studio ports. Reused-only commands stay quietly
316
+ attached, preventing process supervisors from printing a normal child-exit message. Pressing
317
+ Ctrl+C only stops Studio processes started by the current CLI run; reused processes are left running.
318
+
319
+ If you run Studio beside `nodemon`, prefer an explicit watch scope for the API process. Prisma
320
+ Studio does not need to write to your app source, but broad nodemon defaults can restart on
321
+ generated TypeScript or JSON files produced by other dev tooling:
322
+
323
+ ```bash
324
+ nodemon --watch src --ext ts,json \
325
+ --ignore 'src/types/*.generated.ts' \
326
+ --exec tsx --env-file=.env --no-warnings src/server.ts
229
327
  ```
230
328
 
231
329
  #### `prisma-sharding-test`
@@ -337,6 +435,9 @@ try {
337
435
 
338
436
  ## Custom Logger
339
437
 
438
+ The default logger prints warnings and errors only. Set `PRISMA_SHARDING_VERBOSE=true` to include
439
+ initialization, shard connection, and shutdown lifecycle messages.
440
+
340
441
  ```typescript
341
442
  const sharding = new PrismaSharding({
342
443
  // ...config,
@@ -25,103 +25,209 @@ 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
- var import_child_process = require("child_process");
29
- 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 });
37
- }
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;
38
34
  }
39
- if (shards.length === 0 && process.env.DATABASE_URL) {
40
- shards.push({ id: "shard_1", url: process.env.DATABASE_URL });
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);
41
71
  }
42
- return shards;
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
+ };
43
87
  };
44
- var runPrismaCommand = (shardUrl, command) => {
88
+
89
+ // src/cli/utils/command.ts
90
+ var import_child_process = require("child_process");
91
+ var import_path = __toESM(require("path"));
92
+ var getNpxCommand = () => process.platform === "win32" ? "npx.cmd" : "npx";
93
+ var runCommand = (command, args, options = {}) => {
45
94
  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], {
51
- env,
52
- cwd: import_path.default.resolve(process.cwd()),
53
- shell: true
95
+ let stdout = "";
96
+ let stderr = "";
97
+ let settled = false;
98
+ const child = (0, import_child_process.spawn)(command, args, {
99
+ env: options.env || process.env,
100
+ cwd: import_path.default.resolve(options.cwd || process.cwd()),
101
+ shell: false,
102
+ stdio: ["ignore", "pipe", "pipe"]
54
103
  });
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();
104
+ const settle = (result) => {
105
+ if (!settled) {
106
+ settled = true;
107
+ resolve(result);
108
+ }
109
+ };
110
+ child.stdout?.on("data", (data) => {
111
+ const output = data.toString();
112
+ stdout += output;
113
+ if (options.verbose) {
114
+ process.stdout.write(output);
115
+ }
62
116
  });
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}` });
117
+ child.stderr?.on("data", (data) => {
118
+ const output = data.toString();
119
+ stderr += output;
120
+ if (options.verbose) {
121
+ process.stderr.write(output);
68
122
  }
69
123
  });
70
- prisma.on("error", (err) => {
71
- resolve({ success: false, output, error: err.message });
124
+ child.once("error", (error) => {
125
+ settle({
126
+ success: false,
127
+ stdout,
128
+ stderr,
129
+ error: error.message
130
+ });
131
+ });
132
+ child.once("close", (exitCode) => {
133
+ settle({
134
+ success: exitCode === 0,
135
+ stdout,
136
+ stderr,
137
+ exitCode,
138
+ error: exitCode === 0 ? void 0 : stderr.trim() || stdout.trim() || `Command exited with code ${exitCode}`
139
+ });
72
140
  });
73
141
  });
74
142
  };
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(" ")}`);
143
+ var runPrismaCommand = (args, options = {}) => runCommand(getNpxCommand(), ["prisma", ...args], options);
144
+
145
+ // src/cli/utils/shards.ts
146
+ var NO_SHARDS_CONFIGURED_MESSAGE = "No shards configured. Set SHARD_COUNT and SHARD_N_URL environment variables.";
147
+ var getShardConfigResult = (env = process.env) => {
148
+ const shards = [];
149
+ const missingShardIds = [];
150
+ const shardCount = parseInt(env.SHARD_COUNT || "0", 10);
151
+ for (let i = 1; i <= shardCount; i++) {
152
+ const url = env[`SHARD_${i}_URL`];
153
+ if (url) {
154
+ shards.push({ id: `shard_${i}`, index: i - 1, url });
155
+ } else {
156
+ missingShardIds.push(`shard_${i}`);
157
+ }
82
158
  }
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."
87
- );
88
- process.exit(1);
159
+ if (shards.length === 0 && env.DATABASE_URL) {
160
+ shards.push({ id: "shard_1", index: 0, url: env.DATABASE_URL });
89
161
  }
162
+ return { shards, missingShardIds };
163
+ };
164
+ var getShardConfigs = (env = process.env) => {
165
+ return getShardConfigResult(env).shards;
166
+ };
167
+ var maskShardUrl = (url) => {
168
+ try {
169
+ const parsed = new URL(url);
170
+ if (parsed.password) {
171
+ parsed.password = "***";
172
+ }
173
+ return parsed.toString();
174
+ } catch {
175
+ return url.replace(/:[^:@]+@/, ":***@");
176
+ }
177
+ };
178
+
179
+ // src/cli/utils/prisma.ts
180
+ var syncShardSchemas = async (shards, extraArgs, verbose) => {
90
181
  const results = [];
91
182
  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 });
183
+ const loader = createCliLoader(shard.id, "Syncing", !verbose);
184
+ if (verbose) {
185
+ console.log(`
186
+ Syncing ${shard.id}...`);
187
+ console.log(`Database: ${maskShardUrl(shard.url)}
188
+ `);
189
+ }
190
+ const result = await runPrismaCommand(
191
+ ["db", "push", "--accept-data-loss", ...extraArgs],
192
+ {
193
+ env: { ...process.env, DATABASE_URL: shard.url },
194
+ verbose
195
+ }
196
+ );
197
+ if (verbose && result.error && !result.stderr.trim() && !result.stdout.trim()) {
198
+ console.error(result.error);
199
+ }
200
+ results.push({ shardId: shard.id, success: result.success });
201
+ if (result.success) {
202
+ loader.succeed("Synced");
101
203
  } else {
102
- console.log(` \u2705 Success`);
103
- results.push({ shardId: shard.id, success: true, output });
204
+ loader.fail("Failed");
104
205
  }
105
206
  }
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}`);
207
+ return results;
208
+ };
209
+
210
+ // src/cli/migrate.ts
211
+ var migrateAllShards = async () => {
212
+ const verbose = isVerboseEnv(["SHARD_MIGRATE_VERBOSE", "SHARD_CLI_VERBOSE"]);
213
+ const shards = getShardConfigs();
214
+ const extraArgs = process.argv.slice(2);
215
+ printCliHeader("\u{1F504}", "Prisma Sharding Migrate");
216
+ if (shards.length === 0) {
217
+ printCliRow("\u274C", "config", NO_SHARDS_CONFIGURED_MESSAGE);
218
+ process.exit(1);
219
+ }
220
+ const results = await syncShardSchemas(shards, extraArgs, verbose);
221
+ const successful = results.filter((result) => result.success).length;
222
+ const failed = results.length - successful;
118
223
  if (failed > 0) {
119
- console.log("\n\u26A0\uFE0F Some migrations failed. Run with --force-reset to clear incompatible data.");
224
+ if (!verbose) {
225
+ printVerboseHint();
226
+ }
120
227
  process.exit(1);
121
228
  }
122
- console.log("\n\u2705 All shard migrations completed successfully!");
123
229
  };
124
230
  migrateAllShards().catch((error) => {
125
- console.error("Migration script failed:", error);
231
+ console.error(error instanceof Error ? error.message : String(error));
126
232
  process.exit(1);
127
233
  });