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 +129 -5
- package/dist/cli/migrate.js +182 -66
- package/dist/cli/studio.js +600 -59
- package/dist/cli/test.js +27 -21
- package/dist/cli/update.js +249 -0
- 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 +5 -3
package/README.md
CHANGED
|
@@ -168,8 +168,8 @@ Add to your `package.json`:
|
|
|
168
168
|
```json
|
|
169
169
|
{
|
|
170
170
|
"scripts": {
|
|
171
|
-
"db:
|
|
172
|
-
"
|
|
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
|
-
|
|
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
|
|
205
|
-
|
|
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,
|
package/dist/cli/migrate.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
67
|
-
|
|
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
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
93
|
-
results.push({ shardId: shard.id, success: true, output });
|
|
204
|
+
loader.fail("Failed");
|
|
94
205
|
}
|
|
95
206
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
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(
|
|
231
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
116
232
|
process.exit(1);
|
|
117
233
|
});
|