prisma-sharding 0.0.2 → 0.0.3

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,71 @@ 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
+
111
216
  ## Configuration
112
217
 
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 |
218
+ | Option | Type | Default | Description |
219
+ | ------------------------- | ------------------------------- | ---------- | --------------------------------- |
220
+ | `shards` | `ShardConfig[]` | Required | Array of shard configurations |
221
+ | `strategy` | `'modulo' \| 'consistent-hash'` | `'modulo'` | Routing algorithm |
222
+ | `createClient` | `(url, shardId) => TClient` | Required | Factory to create Prisma clients |
223
+ | `healthCheckIntervalMs` | `number` | `30000` | Health check frequency |
224
+ | `circuitBreakerThreshold` | `number` | `3` | Failures before marking unhealthy |
121
225
 
122
226
  ### Shard Config
123
227
 
@@ -151,7 +255,7 @@ strategy: 'consistent-hash';
151
255
  ## Error Handling
152
256
 
153
257
  ```typescript
154
- import { ShardingError, ConfigError, ConnectionError, RoutingError } from 'prisma-sharding';
258
+ import { ShardingError, ConfigError, ConnectionError } from 'prisma-sharding';
155
259
 
156
260
  try {
157
261
  const client = sharding.getShard(userId);
@@ -175,6 +279,52 @@ const sharding = new PrismaSharding({
175
279
  });
176
280
  ```
177
281
 
282
+ ---
283
+
284
+ ### `getAllClients()`
285
+
286
+ Get all Prisma client instances.
287
+
288
+ ```typescript
289
+ const clients = sharding.getAllClients();
290
+
291
+ console.log(`Managing ${clients.length} shard clients`);
292
+ ```
293
+
294
+ **Returns:** `PrismaClient[]`
295
+
296
+ ---
297
+
298
+ ### `getShardCount()`
299
+
300
+ Get total number of configured shards.
301
+
302
+ ```typescript
303
+ const count = sharding.getShardCount();
304
+ console.log(`Running on ${count} shards`);
305
+ // Output: Running on 3 shards
306
+ ```
307
+
308
+ ---
309
+
310
+ ### `getShardIds()`
311
+
312
+ Get array of all shard IDs.
313
+
314
+ ```typescript
315
+ const shardIds = sharding.getShardIds();
316
+ console.log(shardIds);
317
+ // Output: ['shard_1', 'shard_2', 'shard_3']
318
+ ```
319
+
320
+ **Returns:** `string[]`
321
+
322
+ ---
323
+
324
+ ## Author
325
+
326
+ [safdar-azeem](https://github.com/safdar-azeem)
327
+
178
328
  ## License
179
329
 
180
330
  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
+ });
@@ -0,0 +1,122 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ // src/cli/test.ts
5
+ var import_config = require("dotenv/config");
6
+ var import_child_process = require("child_process");
7
+ var results = [];
8
+ var log = {
9
+ info: (msg) => console.log(`\u2139\uFE0F ${msg}`),
10
+ success: (msg) => console.log(`\u2705 ${msg}`),
11
+ error: (msg) => console.log(`\u274C ${msg}`),
12
+ warn: (msg) => console.log(`\u26A0\uFE0F ${msg}`),
13
+ section: (msg) => console.log(`
14
+ ${"=".repeat(50)}
15
+ \u{1F4CB} ${msg}
16
+ ${"=".repeat(50)}`)
17
+ };
18
+ var getShardConfigs = () => {
19
+ const shards = [];
20
+ const shardCount = parseInt(process.env.SHARD_COUNT || "0", 10);
21
+ for (let i = 1; i <= shardCount; i++) {
22
+ const url = process.env[`SHARD_${i}_URL`];
23
+ if (url) {
24
+ shards.push({ id: `shard_${i}`, url });
25
+ }
26
+ }
27
+ if (shards.length === 0 && process.env.DATABASE_URL) {
28
+ shards.push({ id: "shard_1", url: process.env.DATABASE_URL });
29
+ }
30
+ return shards;
31
+ };
32
+ var runTest = async (name, testFn) => {
33
+ const start = Date.now();
34
+ try {
35
+ await testFn();
36
+ const duration = Date.now() - start;
37
+ results.push({ name, passed: true, message: "Passed", duration });
38
+ log.success(`${name} (${duration}ms)`);
39
+ } catch (error) {
40
+ const duration = Date.now() - start;
41
+ const message = error instanceof Error ? error.message : String(error);
42
+ results.push({ name, passed: false, message, duration });
43
+ log.error(`${name}: ${message}`);
44
+ }
45
+ };
46
+ var testConnection = (url) => {
47
+ return new Promise((resolve) => {
48
+ const prisma = (0, import_child_process.spawn)("npx", ["prisma", "db", "execute", "--url", url, "--stdin"], {
49
+ shell: true,
50
+ stdio: ["pipe", "pipe", "pipe"]
51
+ });
52
+ prisma.stdin.write("SELECT 1");
53
+ prisma.stdin.end();
54
+ prisma.on("close", (code) => {
55
+ resolve(code === 0);
56
+ });
57
+ prisma.on("error", () => {
58
+ resolve(false);
59
+ });
60
+ });
61
+ };
62
+ var runTests = async () => {
63
+ const shards = getShardConfigs();
64
+ console.log("\n\u{1F9EA} prisma-sharding: Shard Connection Test Suite\n");
65
+ console.log(`\u{1F4CA} Configuration:`);
66
+ console.log(` - Shard Count: ${shards.length}`);
67
+ console.log(` - Routing Strategy: ${process.env.SHARD_ROUTING_STRATEGY || "modulo"}`);
68
+ if (shards.length === 0) {
69
+ console.error(
70
+ "\n\u274C No shards configured. Set SHARD_COUNT and SHARD_N_URL environment variables."
71
+ );
72
+ process.exit(1);
73
+ }
74
+ log.section("Test 1: Shard Connection Tests");
75
+ for (const shard of shards) {
76
+ await runTest(`Connect to ${shard.id}`, async () => {
77
+ const success = await testConnection(shard.url);
78
+ if (!success) {
79
+ throw new Error(`Failed to connect to ${shard.id}`);
80
+ }
81
+ });
82
+ }
83
+ log.section("Test 2: Configuration Verification");
84
+ await runTest("Verify environment variables", async () => {
85
+ const shardCount = parseInt(process.env.SHARD_COUNT || "0", 10);
86
+ if (shardCount === 0 && !process.env.DATABASE_URL) {
87
+ throw new Error("SHARD_COUNT or DATABASE_URL must be set");
88
+ }
89
+ log.info(`SHARD_COUNT: ${shardCount}`);
90
+ log.info(`Routing Strategy: ${process.env.SHARD_ROUTING_STRATEGY || "modulo"}`);
91
+ });
92
+ await runTest("Verify all shard URLs are present", async () => {
93
+ const shardCount = parseInt(process.env.SHARD_COUNT || "0", 10);
94
+ for (let i = 1; i <= shardCount; i++) {
95
+ const url = process.env[`SHARD_${i}_URL`];
96
+ if (!url) {
97
+ throw new Error(`SHARD_${i}_URL is missing`);
98
+ }
99
+ }
100
+ });
101
+ log.section("Test Results Summary");
102
+ const passed = results.filter((r) => r.passed).length;
103
+ const failed = results.filter((r) => !r.passed).length;
104
+ const totalDuration = results.reduce((sum, r) => sum + r.duration, 0);
105
+ console.log(`
106
+ Total Tests: ${results.length}`);
107
+ console.log(` Passed: ${passed}`);
108
+ console.log(` Failed: ${failed}`);
109
+ console.log(` Duration: ${totalDuration}ms`);
110
+ if (failed > 0) {
111
+ console.log("\n Failed Tests:");
112
+ results.filter((r) => !r.passed).forEach((r) => {
113
+ console.log(` - ${r.name}: ${r.message}`);
114
+ });
115
+ process.exit(1);
116
+ }
117
+ console.log("\n\u2705 All shard tests passed!");
118
+ };
119
+ runTests().catch((error) => {
120
+ console.error("\n\u{1F4A5} Test suite crashed:", error);
121
+ process.exit(1);
122
+ });
package/dist/index.js CHANGED
@@ -33,25 +33,26 @@ __export(index_exports, {
33
33
  });
34
34
  module.exports = __toCommonJS(index_exports);
35
35
 
36
- // src/constants/index.ts
37
- var DEFAULTS = {
38
- POOL_MAX_CONNECTIONS: 10,
39
- POOL_IDLE_TIMEOUT_MS: 1e4,
40
- POOL_CONNECTION_TIMEOUT_MS: 5e3,
41
- HEALTH_CHECK_INTERVAL_MS: 3e4,
42
- CIRCUIT_BREAKER_THRESHOLD: 3,
43
- CONSISTENT_HASH_VIRTUAL_NODES: 150
44
- };
45
- var ERROR_MESSAGES = {
46
- NO_SHARDS: "At least one shard must be configured",
47
- SHARD_NOT_FOUND: (id) => `Shard "${id}" not found`,
48
- NO_HEALTHY_SHARDS: "No healthy shards available",
49
- INVALID_STRATEGY: (s) => `Invalid routing strategy: "${s}". Use "modulo" or "consistent-hash"`,
50
- NOT_CONNECTED: "Sharding not connected. Call connect() first",
51
- ALREADY_CONNECTED: "Sharding already connected",
52
- MISSING_CLIENT_FACTORY: "createClient function is required",
53
- INVALID_SHARD_URL: (id) => `Invalid or missing URL for shard "${id}"`
54
- };
36
+ // src/utils/index.ts
37
+ function hashString(str) {
38
+ let hash = 0;
39
+ for (let i = 0; i < str.length; i++) {
40
+ const char = str.charCodeAt(i);
41
+ hash = (hash << 5) - hash + char;
42
+ hash = hash & hash;
43
+ }
44
+ return Math.abs(hash);
45
+ }
46
+ function validateUrl(url) {
47
+ return url.startsWith("postgresql://") || url.startsWith("postgres://");
48
+ }
49
+ function createDefaultLogger() {
50
+ return {
51
+ info: (msg) => console.log(`[PrismaSharding] ${msg}`),
52
+ warn: (msg) => console.warn(`[PrismaSharding] ${msg}`),
53
+ error: (msg) => console.error(`[PrismaSharding] ${msg}`)
54
+ };
55
+ }
55
56
 
56
57
  // src/core/errors.ts
57
58
  var ShardingError = class _ShardingError extends Error {
@@ -85,6 +86,87 @@ var RoutingError = class _RoutingError extends ShardingError {
85
86
  }
86
87
  };
87
88
 
89
+ // src/constants/index.ts
90
+ var DEFAULTS = {
91
+ POOL_MAX_CONNECTIONS: 10,
92
+ POOL_IDLE_TIMEOUT_MS: 1e4,
93
+ POOL_CONNECTION_TIMEOUT_MS: 5e3,
94
+ HEALTH_CHECK_INTERVAL_MS: 3e4,
95
+ CIRCUIT_BREAKER_THRESHOLD: 3,
96
+ CONSISTENT_HASH_VIRTUAL_NODES: 150
97
+ };
98
+ var ERROR_MESSAGES = {
99
+ NO_SHARDS: "At least one shard must be configured",
100
+ SHARD_NOT_FOUND: (id) => `Shard "${id}" not found`,
101
+ NO_HEALTHY_SHARDS: "No healthy shards available",
102
+ INVALID_STRATEGY: (s) => `Invalid routing strategy: "${s}". Use "modulo" or "consistent-hash"`,
103
+ NOT_CONNECTED: "Sharding not connected. Call connect() first",
104
+ ALREADY_CONNECTED: "Sharding already connected",
105
+ MISSING_CLIENT_FACTORY: "createClient function is required",
106
+ INVALID_SHARD_URL: (id) => `Invalid or missing URL for shard "${id}"`
107
+ };
108
+
109
+ // src/core/router.ts
110
+ var ShardRouter = class {
111
+ constructor(config) {
112
+ this.consistentHashRing = /* @__PURE__ */ new Map();
113
+ this.virtualNodes = DEFAULTS.CONSISTENT_HASH_VIRTUAL_NODES;
114
+ this.strategy = config.strategy;
115
+ this.shardIds = config.shardIds;
116
+ this.logger = config.logger;
117
+ if (this.strategy === "consistent-hash") {
118
+ this.initializeConsistentHashRing();
119
+ }
120
+ }
121
+ initializeConsistentHashRing() {
122
+ for (const shardId of this.shardIds) {
123
+ for (let i = 0; i < this.virtualNodes; i++) {
124
+ const hash = hashString(`${shardId}:${i}`);
125
+ this.consistentHashRing.set(hash, shardId);
126
+ }
127
+ }
128
+ }
129
+ getShardIndex(key) {
130
+ const shardCount = this.shardIds.length;
131
+ if (shardCount === 0) {
132
+ throw new RoutingError("No shards available");
133
+ }
134
+ if (this.strategy === "consistent-hash") {
135
+ return this.getIndexConsistentHash(key);
136
+ }
137
+ return this.getIndexModulo(key, shardCount);
138
+ }
139
+ getIndexModulo(key, shardCount) {
140
+ const hash = hashString(key);
141
+ return hash % shardCount;
142
+ }
143
+ getIndexConsistentHash(key) {
144
+ const hash = hashString(key);
145
+ const sortedHashes = Array.from(this.consistentHashRing.keys()).sort((a, b) => a - b);
146
+ for (const ringHash of sortedHashes) {
147
+ if (hash <= ringHash) {
148
+ const shardId = this.consistentHashRing.get(ringHash);
149
+ const match2 = shardId.match(/shard_(\d+)/);
150
+ return match2 ? parseInt(match2[1], 10) - 1 : 0;
151
+ }
152
+ }
153
+ const firstShardId = this.consistentHashRing.get(sortedHashes[0]);
154
+ const match = firstShardId.match(/shard_(\d+)/);
155
+ return match ? parseInt(match[1], 10) - 1 : 0;
156
+ }
157
+ getShardId(key) {
158
+ const index = this.getShardIndex(key);
159
+ return this.shardIds[index] || `shard_${index + 1}`;
160
+ }
161
+ getRandomShardIndex() {
162
+ return Math.floor(Math.random() * this.shardIds.length);
163
+ }
164
+ getRandomShardId() {
165
+ const index = this.getRandomShardIndex();
166
+ return this.shardIds[index];
167
+ }
168
+ };
169
+
88
170
  // src/core/manager.ts
89
171
  var ShardManager = class {
90
172
  constructor(config) {
@@ -252,88 +334,6 @@ var ShardManager = class {
252
334
  }
253
335
  };
254
336
 
255
- // src/utils/index.ts
256
- function hashString(str) {
257
- let hash = 0;
258
- for (let i = 0; i < str.length; i++) {
259
- const char = str.charCodeAt(i);
260
- hash = (hash << 5) - hash + char;
261
- hash = hash & hash;
262
- }
263
- return Math.abs(hash);
264
- }
265
- function validateUrl(url) {
266
- return url.startsWith("postgresql://") || url.startsWith("postgres://");
267
- }
268
- function createDefaultLogger() {
269
- return {
270
- info: (msg) => console.log(`[PrismaSharding] ${msg}`),
271
- warn: (msg) => console.warn(`[PrismaSharding] ${msg}`),
272
- error: (msg) => console.error(`[PrismaSharding] ${msg}`)
273
- };
274
- }
275
-
276
- // src/core/router.ts
277
- var ShardRouter = class {
278
- constructor(config) {
279
- this.consistentHashRing = /* @__PURE__ */ new Map();
280
- this.virtualNodes = DEFAULTS.CONSISTENT_HASH_VIRTUAL_NODES;
281
- this.strategy = config.strategy;
282
- this.shardIds = config.shardIds;
283
- this.logger = config.logger;
284
- if (this.strategy === "consistent-hash") {
285
- this.initializeConsistentHashRing();
286
- }
287
- }
288
- initializeConsistentHashRing() {
289
- for (const shardId of this.shardIds) {
290
- for (let i = 0; i < this.virtualNodes; i++) {
291
- const hash = hashString(`${shardId}:${i}`);
292
- this.consistentHashRing.set(hash, shardId);
293
- }
294
- }
295
- }
296
- getShardIndex(key) {
297
- const shardCount = this.shardIds.length;
298
- if (shardCount === 0) {
299
- throw new RoutingError("No shards available");
300
- }
301
- if (this.strategy === "consistent-hash") {
302
- return this.getIndexConsistentHash(key);
303
- }
304
- return this.getIndexModulo(key, shardCount);
305
- }
306
- getIndexModulo(key, shardCount) {
307
- const hash = hashString(key);
308
- return hash % shardCount;
309
- }
310
- getIndexConsistentHash(key) {
311
- const hash = hashString(key);
312
- const sortedHashes = Array.from(this.consistentHashRing.keys()).sort((a, b) => a - b);
313
- for (const ringHash of sortedHashes) {
314
- if (hash <= ringHash) {
315
- const shardId = this.consistentHashRing.get(ringHash);
316
- const match2 = shardId.match(/shard_(\d+)/);
317
- return match2 ? parseInt(match2[1], 10) - 1 : 0;
318
- }
319
- }
320
- const firstShardId = this.consistentHashRing.get(sortedHashes[0]);
321
- const match = firstShardId.match(/shard_(\d+)/);
322
- return match ? parseInt(match[1], 10) - 1 : 0;
323
- }
324
- getShardId(key) {
325
- const index = this.getShardIndex(key);
326
- return this.shardIds[index] || `shard_${index + 1}`;
327
- }
328
- getRandomShardIndex() {
329
- return Math.floor(Math.random() * this.shardIds.length);
330
- }
331
- getRandomShardId() {
332
- const index = this.getRandomShardIndex();
333
- return this.shardIds[index];
334
- }
335
- };
336
-
337
337
  // src/core/sharding.ts
338
338
  var PrismaSharding = class {
339
339
  constructor(config) {
package/dist/index.mjs CHANGED
@@ -1,22 +1,23 @@
1
- // src/constants/index.ts
2
- var DEFAULTS = {
3
- POOL_MAX_CONNECTIONS: 10,
4
- POOL_IDLE_TIMEOUT_MS: 1e4,
5
- POOL_CONNECTION_TIMEOUT_MS: 5e3,
6
- HEALTH_CHECK_INTERVAL_MS: 3e4,
7
- CIRCUIT_BREAKER_THRESHOLD: 3,
8
- CONSISTENT_HASH_VIRTUAL_NODES: 150
9
- };
10
- var ERROR_MESSAGES = {
11
- NO_SHARDS: "At least one shard must be configured",
12
- SHARD_NOT_FOUND: (id) => `Shard "${id}" not found`,
13
- NO_HEALTHY_SHARDS: "No healthy shards available",
14
- INVALID_STRATEGY: (s) => `Invalid routing strategy: "${s}". Use "modulo" or "consistent-hash"`,
15
- NOT_CONNECTED: "Sharding not connected. Call connect() first",
16
- ALREADY_CONNECTED: "Sharding already connected",
17
- MISSING_CLIENT_FACTORY: "createClient function is required",
18
- INVALID_SHARD_URL: (id) => `Invalid or missing URL for shard "${id}"`
19
- };
1
+ // src/utils/index.ts
2
+ function hashString(str) {
3
+ let hash = 0;
4
+ for (let i = 0; i < str.length; i++) {
5
+ const char = str.charCodeAt(i);
6
+ hash = (hash << 5) - hash + char;
7
+ hash = hash & hash;
8
+ }
9
+ return Math.abs(hash);
10
+ }
11
+ function validateUrl(url) {
12
+ return url.startsWith("postgresql://") || url.startsWith("postgres://");
13
+ }
14
+ function createDefaultLogger() {
15
+ return {
16
+ info: (msg) => console.log(`[PrismaSharding] ${msg}`),
17
+ warn: (msg) => console.warn(`[PrismaSharding] ${msg}`),
18
+ error: (msg) => console.error(`[PrismaSharding] ${msg}`)
19
+ };
20
+ }
20
21
 
21
22
  // src/core/errors.ts
22
23
  var ShardingError = class _ShardingError extends Error {
@@ -50,6 +51,87 @@ var RoutingError = class _RoutingError extends ShardingError {
50
51
  }
51
52
  };
52
53
 
54
+ // src/constants/index.ts
55
+ var DEFAULTS = {
56
+ POOL_MAX_CONNECTIONS: 10,
57
+ POOL_IDLE_TIMEOUT_MS: 1e4,
58
+ POOL_CONNECTION_TIMEOUT_MS: 5e3,
59
+ HEALTH_CHECK_INTERVAL_MS: 3e4,
60
+ CIRCUIT_BREAKER_THRESHOLD: 3,
61
+ CONSISTENT_HASH_VIRTUAL_NODES: 150
62
+ };
63
+ var ERROR_MESSAGES = {
64
+ NO_SHARDS: "At least one shard must be configured",
65
+ SHARD_NOT_FOUND: (id) => `Shard "${id}" not found`,
66
+ NO_HEALTHY_SHARDS: "No healthy shards available",
67
+ INVALID_STRATEGY: (s) => `Invalid routing strategy: "${s}". Use "modulo" or "consistent-hash"`,
68
+ NOT_CONNECTED: "Sharding not connected. Call connect() first",
69
+ ALREADY_CONNECTED: "Sharding already connected",
70
+ MISSING_CLIENT_FACTORY: "createClient function is required",
71
+ INVALID_SHARD_URL: (id) => `Invalid or missing URL for shard "${id}"`
72
+ };
73
+
74
+ // src/core/router.ts
75
+ var ShardRouter = class {
76
+ constructor(config) {
77
+ this.consistentHashRing = /* @__PURE__ */ new Map();
78
+ this.virtualNodes = DEFAULTS.CONSISTENT_HASH_VIRTUAL_NODES;
79
+ this.strategy = config.strategy;
80
+ this.shardIds = config.shardIds;
81
+ this.logger = config.logger;
82
+ if (this.strategy === "consistent-hash") {
83
+ this.initializeConsistentHashRing();
84
+ }
85
+ }
86
+ initializeConsistentHashRing() {
87
+ for (const shardId of this.shardIds) {
88
+ for (let i = 0; i < this.virtualNodes; i++) {
89
+ const hash = hashString(`${shardId}:${i}`);
90
+ this.consistentHashRing.set(hash, shardId);
91
+ }
92
+ }
93
+ }
94
+ getShardIndex(key) {
95
+ const shardCount = this.shardIds.length;
96
+ if (shardCount === 0) {
97
+ throw new RoutingError("No shards available");
98
+ }
99
+ if (this.strategy === "consistent-hash") {
100
+ return this.getIndexConsistentHash(key);
101
+ }
102
+ return this.getIndexModulo(key, shardCount);
103
+ }
104
+ getIndexModulo(key, shardCount) {
105
+ const hash = hashString(key);
106
+ return hash % shardCount;
107
+ }
108
+ getIndexConsistentHash(key) {
109
+ const hash = hashString(key);
110
+ const sortedHashes = Array.from(this.consistentHashRing.keys()).sort((a, b) => a - b);
111
+ for (const ringHash of sortedHashes) {
112
+ if (hash <= ringHash) {
113
+ const shardId = this.consistentHashRing.get(ringHash);
114
+ const match2 = shardId.match(/shard_(\d+)/);
115
+ return match2 ? parseInt(match2[1], 10) - 1 : 0;
116
+ }
117
+ }
118
+ const firstShardId = this.consistentHashRing.get(sortedHashes[0]);
119
+ const match = firstShardId.match(/shard_(\d+)/);
120
+ return match ? parseInt(match[1], 10) - 1 : 0;
121
+ }
122
+ getShardId(key) {
123
+ const index = this.getShardIndex(key);
124
+ return this.shardIds[index] || `shard_${index + 1}`;
125
+ }
126
+ getRandomShardIndex() {
127
+ return Math.floor(Math.random() * this.shardIds.length);
128
+ }
129
+ getRandomShardId() {
130
+ const index = this.getRandomShardIndex();
131
+ return this.shardIds[index];
132
+ }
133
+ };
134
+
53
135
  // src/core/manager.ts
54
136
  var ShardManager = class {
55
137
  constructor(config) {
@@ -217,88 +299,6 @@ var ShardManager = class {
217
299
  }
218
300
  };
219
301
 
220
- // src/utils/index.ts
221
- function hashString(str) {
222
- let hash = 0;
223
- for (let i = 0; i < str.length; i++) {
224
- const char = str.charCodeAt(i);
225
- hash = (hash << 5) - hash + char;
226
- hash = hash & hash;
227
- }
228
- return Math.abs(hash);
229
- }
230
- function validateUrl(url) {
231
- return url.startsWith("postgresql://") || url.startsWith("postgres://");
232
- }
233
- function createDefaultLogger() {
234
- return {
235
- info: (msg) => console.log(`[PrismaSharding] ${msg}`),
236
- warn: (msg) => console.warn(`[PrismaSharding] ${msg}`),
237
- error: (msg) => console.error(`[PrismaSharding] ${msg}`)
238
- };
239
- }
240
-
241
- // src/core/router.ts
242
- var ShardRouter = class {
243
- constructor(config) {
244
- this.consistentHashRing = /* @__PURE__ */ new Map();
245
- this.virtualNodes = DEFAULTS.CONSISTENT_HASH_VIRTUAL_NODES;
246
- this.strategy = config.strategy;
247
- this.shardIds = config.shardIds;
248
- this.logger = config.logger;
249
- if (this.strategy === "consistent-hash") {
250
- this.initializeConsistentHashRing();
251
- }
252
- }
253
- initializeConsistentHashRing() {
254
- for (const shardId of this.shardIds) {
255
- for (let i = 0; i < this.virtualNodes; i++) {
256
- const hash = hashString(`${shardId}:${i}`);
257
- this.consistentHashRing.set(hash, shardId);
258
- }
259
- }
260
- }
261
- getShardIndex(key) {
262
- const shardCount = this.shardIds.length;
263
- if (shardCount === 0) {
264
- throw new RoutingError("No shards available");
265
- }
266
- if (this.strategy === "consistent-hash") {
267
- return this.getIndexConsistentHash(key);
268
- }
269
- return this.getIndexModulo(key, shardCount);
270
- }
271
- getIndexModulo(key, shardCount) {
272
- const hash = hashString(key);
273
- return hash % shardCount;
274
- }
275
- getIndexConsistentHash(key) {
276
- const hash = hashString(key);
277
- const sortedHashes = Array.from(this.consistentHashRing.keys()).sort((a, b) => a - b);
278
- for (const ringHash of sortedHashes) {
279
- if (hash <= ringHash) {
280
- const shardId = this.consistentHashRing.get(ringHash);
281
- const match2 = shardId.match(/shard_(\d+)/);
282
- return match2 ? parseInt(match2[1], 10) - 1 : 0;
283
- }
284
- }
285
- const firstShardId = this.consistentHashRing.get(sortedHashes[0]);
286
- const match = firstShardId.match(/shard_(\d+)/);
287
- return match ? parseInt(match[1], 10) - 1 : 0;
288
- }
289
- getShardId(key) {
290
- const index = this.getShardIndex(key);
291
- return this.shardIds[index] || `shard_${index + 1}`;
292
- }
293
- getRandomShardIndex() {
294
- return Math.floor(Math.random() * this.shardIds.length);
295
- }
296
- getRandomShardId() {
297
- const index = this.getRandomShardIndex();
298
- return this.shardIds[index];
299
- }
300
- };
301
-
302
302
  // src/core/sharding.ts
303
303
  var PrismaSharding = class {
304
304
  constructor(config) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prisma-sharding",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "description": "Lightweight database sharding library for Prisma with connection pooling and health monitoring",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -13,9 +13,16 @@
13
13
  "sharding",
14
14
  "database",
15
15
  "postgresql",
16
+ "db-sharding",
16
17
  "horizontal-scaling",
17
- "connection-pool"
18
+ "connection-pool",
19
+ "cli"
18
20
  ],
21
+ "bin": {
22
+ "prisma-sharding-migrate": "./dist/cli/migrate.js",
23
+ "prisma-sharding-studio": "./dist/cli/studio.js",
24
+ "prisma-sharding-test": "./dist/cli/test.js"
25
+ },
19
26
  "files": [
20
27
  "dist"
21
28
  ],
@@ -27,7 +34,9 @@
27
34
  }
28
35
  },
29
36
  "scripts": {
30
- "build": "tsup src/index.ts --format cjs,esm --dts --clean",
37
+ "build": "tsup src/index.ts --format cjs,esm --dts --clean && tsup src/cli/migrate.ts src/cli/studio.ts src/cli/test.ts --format cjs --outDir dist/cli --clean",
38
+ "build:lib": "tsup src/index.ts --format cjs,esm --dts --clean",
39
+ "build:cli": "tsup src/cli/migrate.ts src/cli/studio.ts src/cli/test.ts --format cjs --outDir dist/cli",
31
40
  "dev": "tsup src/index.ts --format cjs,esm --dts --watch",
32
41
  "lint": "eslint src --ext .ts",
33
42
  "typecheck": "tsc --noEmit",
@@ -40,6 +49,9 @@
40
49
  "peerDependencies": {
41
50
  "@prisma/client": ">=5.0.0"
42
51
  },
52
+ "dependencies": {
53
+ "dotenv": "^16.3.1"
54
+ },
43
55
  "devDependencies": {
44
56
  "@types/node": "^20.10.0",
45
57
  "@typescript-eslint/eslint-plugin": "^6.13.0",