prisma-sharding 0.0.2 → 0.0.4

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
@@ -1,6 +1,6 @@
1
1
  # Prisma Sharding
2
2
 
3
- Lightweight database sharding library for Prisma with connection pooling, health monitoring, and circuit breaker support.
3
+ Lightweight database sharding library for Prisma with connection pooling, health monitoring, and CLI tools.
4
4
 
5
5
  ## Installation
6
6
 
@@ -10,9 +10,13 @@ yarn add prisma-sharding
10
10
  npm install prisma-sharding
11
11
  ```
12
12
 
13
- ## Quick Start
13
+ > Don't forget to follow me on [GitHub](https://github.com/safdar-azeem)!
14
+
15
+ ## Step 1: Create Sharding Connection
14
16
 
15
17
  ```typescript
18
+ // src/config/prisma.ts
19
+
16
20
  import { PrismaSharding } from 'prisma-sharding';
17
21
  import { PrismaClient } from '@/generated/prisma';
18
22
  import { PrismaPg } from '@prisma/adapter-pg';
@@ -21,7 +25,6 @@ const sharding = new PrismaSharding<PrismaClient>({
21
25
  shards: [
22
26
  { id: 'shard_1', url: process.env.SHARD_1_URL! },
23
27
  { id: 'shard_2', url: process.env.SHARD_2_URL! },
24
- { id: 'shard_3', url: process.env.SHARD_3_URL! },
25
28
  ],
26
29
  strategy: 'modulo', // 'modulo' | 'consistent-hash'
27
30
  createClient: (url) => {
@@ -30,32 +33,78 @@ const sharding = new PrismaSharding<PrismaClient>({
30
33
  },
31
34
  });
32
35
 
33
- // Initialize connections
34
36
  await sharding.connect();
35
37
  ```
36
38
 
37
- ## Usage
39
+ ## API
38
40
 
39
- ### Get Shard by Key
41
+ | Method | Description |
42
+ | ---------------------------- | --------------------------------------------- |
43
+ | `getShard(key)` | Get Prisma client for a given key |
44
+ | `getShardById(shardId)` | Get Prisma client by shard ID |
45
+ | `getRandomShard()` | Get random shard (for new records) |
46
+ | `findFirst(fn)` | Search across all shards, return first result |
47
+ | `runOnAll(fn)` | Execute on all shards |
48
+ | `getHealth()` | Get health status of all shards |
49
+ | `connect()` / `disconnect()` | Lifecycle methods |
50
+
51
+ ### Step 2: Create a User (Assign to a Shard)
52
+
53
+ New records should be created on a random shard for even distribution.
54
+
55
+ ```ts
56
+ import { sharding } from '@/config/prisma';
57
+
58
+ const client = sharding.getRandomShard();
59
+
60
+ const user = await client.user.create({
61
+ data: {
62
+ email: 'user@example.com',
63
+ username: 'new_user',
64
+ },
65
+ });
66
+ ```
67
+
68
+ ## Step 3: Access User by ID (Shard Routing)
69
+
70
+ When you have a user ID, Prisma Sharding routes you to the correct shard automatically.
71
+
72
+ ```ts
73
+ const userId = 'abc123';
40
74
 
41
- ```typescript
42
- // Get client for existing user (routed by user ID)
43
75
  const client = sharding.getShard(userId);
44
- const user = await client.user.findUnique({ where: { id: userId } });
45
76
 
46
- // Get shard with metadata
47
- const { client, shardId } = sharding.getShardWithInfo(userId);
77
+ const user = await client.user.findUnique({
78
+ where: { id: userId },
79
+ });
48
80
  ```
49
81
 
50
- ### Random Shard (New Records)
82
+ `Important rule:`
51
83
 
52
- ```typescript
53
- // Get random shard for creating new user (ensures even distribution)
54
- const client = sharding.getRandomShard();
55
- const newUser = await client.user.create({ data: { email, username } });
84
+ Once you get the shard client using a user ID, **all future operations for that user must use this same client**.
85
+
86
+ That includes:
87
+
88
+ - Reading user data
89
+ - Updating user data
90
+ - Creating related records (profiles, posts, settings, etc)
91
+
92
+ Every user belongs to exactly one shard. Their entire data lives on that shard only.
93
+
94
+ Do **not** switch shards or use a random shard for user related actions.
95
+
96
+ Always do this:
97
+
98
+ ```ts
99
+ const client = sharding.getShard(userId);
56
100
  ```
57
101
 
58
- ### Cross-Shard Search
102
+ This guarantees all user data stays on the correct shard and avoids cross shard bugs.
103
+
104
+ ### Step 4: Find User Without ID (Cross Shard Search)
105
+
106
+ If you do not have the user ID, search all shards in parallel.
107
+ Use this only when necessary.
59
108
 
60
109
  ```typescript
61
110
  // Find user by email across ALL shards (parallel execution)
@@ -72,7 +121,7 @@ if (user && client) {
72
121
  }
73
122
  ```
74
123
 
75
- ### Execute on All Shards
124
+ ## Step 5: Run on All Shards (Admin or Analytics)
76
125
 
77
126
  ```typescript
78
127
  // Get counts from all shards
@@ -108,16 +157,117 @@ if (sharding.isConnected()) {
108
157
  }
109
158
  ```
110
159
 
160
+ ## CLI Tools
161
+
162
+ The package includes CLI tools for common sharding operations. No need to write custom scripts!
163
+
164
+ ### Setup
165
+
166
+ Add to your `package.json`:
167
+
168
+ ```json
169
+ {
170
+ "scripts": {
171
+ "db:studio:all": "prisma-sharding-studio",
172
+ "migrate:shards": "prisma-sharding-migrate",
173
+ "test:shards": "prisma-sharding-test"
174
+ }
175
+ }
176
+ ```
177
+
178
+ ### Environment Variables
179
+
180
+ ```bash
181
+ SHARD_COUNT=3
182
+ SHARD_1_URL=postgresql://user:pass@host:5432/db1
183
+ SHARD_2_URL=postgresql://user:pass@host:5432/db2
184
+ SHARD_3_URL=postgresql://user:pass@host:5432/db3
185
+ SHARD_ROUTING_STRATEGY=modulo # or consistent-hash
186
+ SHARD_STUDIO_BASE_PORT=51212 # optional, for studio
187
+ ```
188
+
189
+ ### Commands
190
+
191
+ #### `prisma-sharding-migrate`
192
+
193
+ Push schema to all shards using `prisma db push`.
194
+
195
+ ```bash
196
+ yarn migrate:shards
197
+ ```
198
+
199
+ #### `prisma-sharding-studio`
200
+
201
+ Start Prisma Studio for all shards on sequential ports.
202
+
203
+ ```bash
204
+ yarn db:studio:all
205
+ # Opens shard_1 on :51212, shard_2 on :51213, etc.
206
+ ```
207
+
208
+ #### `prisma-sharding-test`
209
+
210
+ Test connections to all shards.
211
+
212
+ ```bash
213
+ yarn test:shards
214
+ ```
215
+
216
+ ```
217
+ ================================
218
+ 📋 User Distribution Test
219
+ ================================
220
+ Creating 24 test users across 3 shards...
221
+
222
+ User 1/24: "testuser_0" → shard_3
223
+ User 2/24: "testuser_1" → shard_1
224
+ User 3/24: "testuser_2" → shard_2
225
+ User 4/24: "testuser_3" → shard_3
226
+ User 5/24: "testuser_4" → shard_1
227
+ User 6/24: "testuser_5" → shard_2
228
+ User 7/24: "testuser_6" → shard_3
229
+ User 8/24: "testuser_7" → shard_1
230
+ User 9/24: "testuser_8" → shard_2
231
+ User 10/24: "testuser_9" → shard_3
232
+ User 11/24: "testuser_10" → shard_2
233
+ User 12/24: "testuser_11" → shard_1
234
+ User 13/24: "testuser_12" → shard_3
235
+ User 14/24: "testuser_13" → shard_2
236
+ User 15/24: "testuser_14" → shard_1
237
+ User 16/24: "testuser_15" → shard_3
238
+ User 17/24: "testuser_16" → shard_2
239
+ User 18/24: "testuser_17" → shard_1
240
+ User 19/24: "testuser_18" → shard_3
241
+ User 20/24: "testuser_19" → shard_2
242
+ User 21/24: "testuser_20" → shard_1
243
+ User 22/24: "testuser_21" → shard_3
244
+ User 23/24: "testuser_22" → shard_2
245
+ User 24/24: "testuser_23" → shard_1
246
+ ✅ Created 24/24 test users
247
+ ```
248
+
249
+ ```
250
+ ================================
251
+ 📋 Read Verification
252
+ ================================
253
+ ✓ User "test_user_1770289330292_0" found on shard_3
254
+ ✓ User "test_user_1770289330292_1" found on shard_1
255
+ ✓ User "test_user_1770289330292_2" found on shard_2
256
+ ✓ User "test_user_1770289330292_3" found on shard_3
257
+ ✓ User "test_user_1770289330292_4" found on shard_1
258
+ Verified 5/5 users on correct shards
259
+ ✅ Verify users exist on correct shards (136ms)
260
+ ```
261
+
111
262
  ## Configuration
112
263
 
113
- | Option | Type | Default | Description |
114
- | ------------------------- | ------------------------------- | ---------- | ----------------------------------------- |
115
- | `shards` | `ShardConfig[]` | Required | Array of shard configurations |
116
- | `strategy` | `'modulo' \| 'consistent-hash'` | `'modulo'` | Routing algorithm |
117
- | `createClient` | `(url, shardId) => TClient` | Required | Factory function to create Prisma clients |
118
- | `healthCheckIntervalMs` | `number` | `30000` | Health check frequency |
119
- | `circuitBreakerThreshold` | `number` | `3` | Failures before marking unhealthy |
120
- | `logger` | `ShardingLogger` | Console | Custom logger |
264
+ | Option | Type | Default | Description |
265
+ | ------------------------- | ------------------------------- | ---------- | --------------------------------- |
266
+ | `shards` | `ShardConfig[]` | Required | Array of shard configurations |
267
+ | `strategy` | `'modulo' \| 'consistent-hash'` | `'modulo'` | Routing algorithm |
268
+ | `createClient` | `(url, shardId) => TClient` | Required | Factory to create Prisma clients |
269
+ | `healthCheckIntervalMs` | `number` | `30000` | Health check frequency |
270
+ | `circuitBreakerThreshold` | `number` | `3` | Failures before marking unhealthy |
121
271
 
122
272
  ### Shard Config
123
273
 
@@ -151,7 +301,7 @@ strategy: 'consistent-hash';
151
301
  ## Error Handling
152
302
 
153
303
  ```typescript
154
- import { ShardingError, ConfigError, ConnectionError, RoutingError } from 'prisma-sharding';
304
+ import { ShardingError, ConfigError, ConnectionError } from 'prisma-sharding';
155
305
 
156
306
  try {
157
307
  const client = sharding.getShard(userId);
@@ -175,6 +325,52 @@ const sharding = new PrismaSharding({
175
325
  });
176
326
  ```
177
327
 
328
+ ---
329
+
330
+ ### `getAllClients()`
331
+
332
+ Get all Prisma client instances.
333
+
334
+ ```typescript
335
+ const clients = sharding.getAllClients();
336
+
337
+ console.log(`Managing ${clients.length} shard clients`);
338
+ ```
339
+
340
+ **Returns:** `PrismaClient[]`
341
+
342
+ ---
343
+
344
+ ### `getShardCount()`
345
+
346
+ Get total number of configured shards.
347
+
348
+ ```typescript
349
+ const count = sharding.getShardCount();
350
+ console.log(`Running on ${count} shards`);
351
+ // Output: Running on 3 shards
352
+ ```
353
+
354
+ ---
355
+
356
+ ### `getShardIds()`
357
+
358
+ Get array of all shard IDs.
359
+
360
+ ```typescript
361
+ const shardIds = sharding.getShardIds();
362
+ console.log(shardIds);
363
+ // Output: ['shard_1', 'shard_2', 'shard_3']
364
+ ```
365
+
366
+ **Returns:** `string[]`
367
+
368
+ ---
369
+
370
+ ## Author
371
+
372
+ [safdar-azeem](https://github.com/safdar-azeem)
373
+
178
374
  ## License
179
375
 
180
376
  MIT
@@ -0,0 +1,117 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
18
+ // If the importer is in node compatibility mode or this is not an ESM
19
+ // file that has been converted to a CommonJS file using a Babel-
20
+ // compatible transform (i.e. "__esModule" has not been set), then set
21
+ // "default" to the CommonJS "module.exports" for node compatibility.
22
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
23
+ mod
24
+ ));
25
+
26
+ // src/cli/migrate.ts
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
+ }
38
+ }
39
+ if (shards.length === 0 && process.env.DATABASE_URL) {
40
+ shards.push({ id: "shard_1", url: process.env.DATABASE_URL });
41
+ }
42
+ return shards;
43
+ };
44
+ var runPrismaCommand = (shardUrl, command) => {
45
+ 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
50
+ });
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();
58
+ });
59
+ prisma.on("close", (code) => {
60
+ if (code === 0) {
61
+ resolve({ output });
62
+ } else {
63
+ resolve({ output, error: errorOutput || `Exit code: ${code}` });
64
+ }
65
+ });
66
+ prisma.on("error", (err) => {
67
+ resolve({ output, error: err.message });
68
+ });
69
+ });
70
+ };
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);
81
+ }
82
+ const results = [];
83
+ 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 });
91
+ } else {
92
+ console.log(` \u2705 Success`);
93
+ results.push({ shardId: shard.id, success: true, output });
94
+ }
95
+ }
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}`);
108
+ if (failed > 0) {
109
+ console.log("\n\u26A0\uFE0F Some migrations failed. Please review the errors above.");
110
+ process.exit(1);
111
+ }
112
+ console.log("\n\u2705 All shard migrations completed successfully!");
113
+ };
114
+ migrateAllShards().catch((error) => {
115
+ console.error("Migration script failed:", error);
116
+ process.exit(1);
117
+ });
@@ -0,0 +1,112 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ // src/cli/studio.ts
5
+ var import_config = require("dotenv/config");
6
+ var import_child_process = require("child_process");
7
+ var instances = [];
8
+ var BASE_PORT = parseInt(process.env.SHARD_STUDIO_BASE_PORT || "51212", 10);
9
+ var getShardConfigs = () => {
10
+ const shards = [];
11
+ const shardCount = parseInt(process.env.SHARD_COUNT || "0", 10);
12
+ for (let i = 1; i <= shardCount; i++) {
13
+ const url = process.env[`SHARD_${i}_URL`];
14
+ if (url) {
15
+ shards.push({ id: `shard_${i}`, url });
16
+ }
17
+ }
18
+ if (shards.length === 0 && process.env.DATABASE_URL) {
19
+ shards.push({ id: "shard_1", url: process.env.DATABASE_URL });
20
+ }
21
+ return shards;
22
+ };
23
+ var startStudio = (shard, index) => {
24
+ return new Promise((resolve, reject) => {
25
+ const port = BASE_PORT + index;
26
+ const shardId = shard.id;
27
+ console.log(`
28
+ \u{1F680} Starting Prisma Studio for ${shardId} on port ${port}...`);
29
+ console.log(` URL: ${shard.url.replace(/:[^:@]+@/, ":***@")}`);
30
+ const studioProcess = (0, import_child_process.spawn)(
31
+ "npx",
32
+ ["prisma", "studio", "--port", port.toString(), "--browser", "none"],
33
+ {
34
+ env: {
35
+ ...process.env,
36
+ DATABASE_URL: shard.url
37
+ },
38
+ shell: true,
39
+ stdio: "pipe"
40
+ }
41
+ );
42
+ studioProcess.stdout?.on("data", (data) => {
43
+ const output = data.toString();
44
+ if (output.includes("Prisma Studio is running")) {
45
+ console.log(` \u2705 ${shardId} ready at http://localhost:${port}`);
46
+ }
47
+ });
48
+ studioProcess.stderr?.on("data", (data) => {
49
+ const output = data.toString();
50
+ if (!output.includes("warn") && !output.includes("Loaded")) {
51
+ console.error(` [${shardId}] ${output}`);
52
+ }
53
+ });
54
+ studioProcess.on("error", (err) => {
55
+ console.error(` \u274C Failed to start ${shardId}:`, err.message);
56
+ reject(err);
57
+ });
58
+ const instance = {
59
+ shardId,
60
+ port,
61
+ process: studioProcess
62
+ };
63
+ instances.push(instance);
64
+ setTimeout(() => resolve(instance), 2e3);
65
+ });
66
+ };
67
+ var startAllStudios = async () => {
68
+ const shards = getShardConfigs();
69
+ console.log("=".repeat(60));
70
+ console.log("\u{1F5C4}\uFE0F prisma-sharding: Multi-Shard Studio Viewer");
71
+ console.log("=".repeat(60));
72
+ console.log(`
73
+ \u{1F4CA} Starting ${shards.length} Prisma Studio instance(s)...
74
+ `);
75
+ if (shards.length === 0) {
76
+ console.error(
77
+ "\u274C No shards configured. Set SHARD_COUNT and SHARD_N_URL environment variables."
78
+ );
79
+ process.exit(1);
80
+ }
81
+ for (let i = 0; i < shards.length; i++) {
82
+ try {
83
+ await startStudio(shards[i], i);
84
+ } catch (error) {
85
+ console.error(`Failed to start studio for ${shards[i].id}:`, error);
86
+ }
87
+ }
88
+ console.log("\n" + "=".repeat(60));
89
+ console.log("\u{1F4CB} All Studios Running:");
90
+ console.log("=".repeat(60));
91
+ instances.forEach((instance) => {
92
+ console.log(` \u2022 ${instance.shardId}: http://localhost:${instance.port}`);
93
+ });
94
+ console.log("\n Press Ctrl+C to stop all instances\n");
95
+ };
96
+ var gracefulShutdown = () => {
97
+ console.log("\n\n\u{1F6D1} Shutting down all Prisma Studio instances...\n");
98
+ instances.forEach((instance) => {
99
+ console.log(` Stopping ${instance.shardId}...`);
100
+ instance.process.kill("SIGTERM");
101
+ });
102
+ setTimeout(() => {
103
+ console.log("\n\u2705 All instances stopped\n");
104
+ process.exit(0);
105
+ }, 1e3);
106
+ };
107
+ process.on("SIGINT", gracefulShutdown);
108
+ process.on("SIGTERM", gracefulShutdown);
109
+ startAllStudios().catch((error) => {
110
+ console.error("Failed to start studios:", error);
111
+ process.exit(1);
112
+ });