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.
- package/README.md +237 -59
- package/dist/cli/migrate.js +327 -70
- package/dist/cli/studio.js +668 -59
- package/dist/cli/test.js +169 -59
- package/dist/cli/update.js +340 -75
- package/dist/cli/utils/command.js +232 -0
- package/dist/cli/utils/postgres.js +55 -0
- package/dist/index.d.mts +4 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +414 -90
- package/dist/index.mjs +414 -90
- package/package.json +4 -3
package/dist/cli/migrate.js
CHANGED
|
@@ -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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
40
|
-
|
|
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
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
201
|
+
cwd: import_path.default.resolve(options.cwd || process.cwd()),
|
|
202
|
+
detached: processGroup,
|
|
203
|
+
shell: false,
|
|
204
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
54
205
|
});
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
64
|
-
if (
|
|
65
|
-
|
|
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
|
-
|
|
71
|
-
|
|
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
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
results.push({ shardId: shard.id, success: false
|
|
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
|
-
|
|
103
|
-
results.push({ shardId: shard.id, success: true, output });
|
|
351
|
+
loader.fail("Failed");
|
|
104
352
|
}
|
|
105
353
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
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(
|
|
382
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
126
383
|
process.exit(1);
|
|
127
384
|
});
|