prisma-sharding 0.0.4 → 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
@@ -168,8 +168,8 @@ Add to your `package.json`:
168
168
  ```json
169
169
  {
170
170
  "scripts": {
171
- "db:studio:all": "prisma-sharding-studio",
172
- "migrate:shards": "prisma-sharding-migrate",
171
+ "db:update": "prisma-sharding-update",
172
+ "db:studio": "prisma-sharding-studio",
173
173
  "test:shards": "prisma-sharding-test"
174
174
  }
175
175
  }
@@ -184,25 +184,146 @@ 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
190
196
 
197
+ #### `prisma-sharding-update` (Recommended)
198
+
199
+ The "All-in-One" command. It generates Prisma Client types and migrates all shards.
200
+ **Use this whenever you change `schema.prisma`.**
201
+
202
+ 1. Runs `prisma generate` (Updates TypeScript types)
203
+ 2. Runs `prisma db push` on all shards (Updates Databases)
204
+
205
+ ```bash
206
+ yarn db:update
207
+
208
+ ```
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
+
227
+ **With Flags:**
228
+ You can pass flags like `--force-reset` if you need to wipe data due to schema conflicts.
229
+
230
+ ```bash
231
+ yarn db:update --force-reset
232
+
233
+ ```
234
+
191
235
  #### `prisma-sharding-migrate`
192
236
 
193
- Push schema to all shards using `prisma db push`.
237
+ Only pushes schema to all shards (skips type generation). Useful for production deployment pipelines.
194
238
 
195
239
  ```bash
196
240
  yarn migrate:shards
241
+
197
242
  ```
198
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
+
199
247
  #### `prisma-sharding-studio`
200
248
 
201
249
  Start Prisma Studio for all shards on sequential ports.
202
250
 
203
251
  ```bash
204
- yarn db:studio:all
205
- # Opens shard_1 on :51212, shard_2 on :51213, etc.
252
+ yarn db:studio
253
+ ```
254
+
255
+ By default, ports are assigned from `SHARD_STUDIO_BASE_PORT`:
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
206
327
  ```
207
328
 
208
329
  #### `prisma-sharding-test`
@@ -314,6 +435,9 @@ try {
314
435
 
315
436
  ## Custom Logger
316
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
+
317
441
  ```typescript
318
442
  const sharding = new PrismaSharding({
319
443
  // ...config,
@@ -25,93 +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 prisma = (0, import_child_process.spawn)("npx", ["prisma", ...command, "--url", shardUrl], {
47
- env: process.env,
48
- cwd: import_path.default.resolve(process.cwd()),
49
- 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"]
50
103
  });
51
- let output = "";
52
- let errorOutput = "";
53
- prisma.stdout.on("data", (data) => {
54
- output += data.toString();
55
- });
56
- prisma.stderr.on("data", (data) => {
57
- 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
+ }
58
116
  });
59
- prisma.on("close", (code) => {
60
- if (code === 0) {
61
- resolve({ output });
62
- } else {
63
- resolve({ 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);
64
122
  }
65
123
  });
66
- prisma.on("error", (err) => {
67
- resolve({ 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
+ });
68
140
  });
69
141
  });
70
142
  };
71
- var migrateAllShards = async () => {
72
- const shards = getShardConfigs();
73
- console.log("\u{1F504} prisma-sharding: Starting migrations...\n");
74
- console.log(`\u{1F4CA} Total shards to migrate: ${shards.length}
75
- `);
76
- if (shards.length === 0) {
77
- console.error(
78
- "\u274C No shards configured. Set SHARD_COUNT and SHARD_N_URL environment variables."
79
- );
80
- process.exit(1);
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
+ }
81
158
  }
159
+ if (shards.length === 0 && env.DATABASE_URL) {
160
+ shards.push({ id: "shard_1", index: 0, url: env.DATABASE_URL });
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) => {
82
181
  const results = [];
83
182
  for (const shard of shards) {
84
- console.log(`
85
- \u{1F4E6} Migrating ${shard.id}...`);
86
- console.log(` URL: ${shard.url.replace(/:[^:@]+@/, ":***@")}`);
87
- const { output, error } = await runPrismaCommand(shard.url, ["db", "push"]);
88
- if (error && !output.includes("Your database is now in sync")) {
89
- console.error(` \u274C Failed: ${error.split("\n")[0]}`);
90
- 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");
91
203
  } else {
92
- console.log(` \u2705 Success`);
93
- results.push({ shardId: shard.id, success: true, output });
204
+ loader.fail("Failed");
94
205
  }
95
206
  }
96
- console.log("\n" + "=".repeat(50));
97
- console.log("\u{1F4CB} Migration Summary\n");
98
- const successful = results.filter((r) => r.success).length;
99
- const failed = results.filter((r) => !r.success).length;
100
- results.forEach((result) => {
101
- const status = result.success ? "\u2705" : "\u274C";
102
- console.log(
103
- ` ${status} ${result.shardId}${result.error ? ` - ${result.error.split("\n")[0]}` : ""}`
104
- );
105
- });
106
- console.log(`
107
- 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;
108
223
  if (failed > 0) {
109
- console.log("\n\u26A0\uFE0F Some migrations failed. Please review the errors above.");
224
+ if (!verbose) {
225
+ printVerboseHint();
226
+ }
110
227
  process.exit(1);
111
228
  }
112
- console.log("\n\u2705 All shard migrations completed successfully!");
113
229
  };
114
230
  migrateAllShards().catch((error) => {
115
- console.error("Migration script failed:", error);
231
+ console.error(error instanceof Error ? error.message : String(error));
116
232
  process.exit(1);
117
233
  });