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 +102 -1
- package/dist/cli/migrate.js +181 -75
- package/dist/cli/studio.js +600 -59
- package/dist/cli/test.js +27 -21
- package/dist/cli/update.js +196 -80
- package/dist/index.d.mts +4 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +19 -1
- package/dist/index.mjs +19 -1
- package/package.json +2 -1
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
|
-
|
|
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,
|
package/dist/cli/migrate.js
CHANGED
|
@@ -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
|
-
|
|
29
|
-
|
|
30
|
-
var
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
40
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
71
|
-
|
|
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
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
84
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
103
|
-
results.push({ shardId: shard.id, success: true, output });
|
|
204
|
+
loader.fail("Failed");
|
|
104
205
|
}
|
|
105
206
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
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(
|
|
231
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
126
232
|
process.exit(1);
|
|
127
233
|
});
|