Skip to content

Commit 023edd1

Browse files
Improved database DX
1 parent 98abe23 commit 023edd1

File tree

7 files changed

+179
-12
lines changed

7 files changed

+179
-12
lines changed

packages/db/README.md

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,24 @@
1-
This package contains the database schema (prisma/schema.prisma), migrations (prisma/migrations) and the client library for interacting with the database. Before making edits to the schema, please read about prisma's [migration model](https://www.prisma.io/docs/orm/prisma-migrate/understanding-prisma-migrate/mental-model) to get an idea of how migrations work.
1+
# @sourcebot/db
2+
3+
This package contains the database schema (prisma/schema.prisma), migrations (prisma/migrations) and the client library for interacting with the database. Before making edits to the schema, please read about prisma's [migration model](https://www.prisma.io/docs/orm/prisma-migrate/understanding-prisma-migrate/mental-model) to get an idea of how migrations work.
4+
5+
## Tools
6+
7+
This library contains a `/tools` directory with a collection of tooling needed for database management. Notable tools are:
8+
9+
- `yarn tool:prisma` - runs the prisma CLI with an additional required param `--url`, the connection URL of the database you want the command to run against. This tool is geared towards running commands against non-dev database like staging or prod since 1) it allows you to quickly switch between environments, and 2) connection URLs do not need to be persisted in a `DATABASE_URL` environment variable. Examples:
10+
11+
```sh
12+
# Run prisma studio
13+
yarn tool:prisma studio --url postgresql://username:password@url:5432/db_name
14+
15+
# Rollback a migration
16+
yarn tool:prisma migrate resolve --rolled-back "migration_name" --url postgresql://username:password@url:5432/db_name
17+
```
18+
19+
- `yarn tool:run-script` - runs a script (located in the `/tools/scripts` directory) that performs some operations against the DB. This is useful for writing bespoke CRUD operations while still being type-safe and having all the perks of the prisma client lib.
20+
21+
```sh
22+
# Run `migrate-duplicate-connections.ts`
23+
yarn tool:run-script --script migrate-duplicate-connections --url postgresql://username:password@url:5432/db_name
24+
```

packages/db/package.json

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,30 @@
44
"main": "dist/index.js",
55
"private": true,
66
"scripts": {
7+
"build": "yarn prisma:generate && tsc",
8+
"postinstall": "yarn build",
9+
710
"prisma:generate": "prisma generate",
811
"prisma:generate:watch": "prisma generate --watch",
912
"prisma:migrate:dev": "prisma migrate dev",
1013
"prisma:migrate:prod": "prisma migrate deploy",
1114
"prisma:migrate:reset": "prisma migrate reset",
1215
"prisma:db:push": "prisma db push",
1316
"prisma:studio": "prisma studio",
14-
"build": "yarn prisma:generate && tsc",
15-
"postinstall": "yarn build"
17+
18+
"tool:prisma": "tsx tools/runPrismaCommand.ts",
19+
"tool:run-script": "tsx tools/scriptRunner.ts"
1620
},
1721
"devDependencies": {
22+
"@types/argparse": "^2.0.16",
23+
"argparse": "^2.0.1",
1824
"prisma": "^6.2.1",
25+
"tsx": "^4.19.1",
1926
"typescript": "^5.7.3"
2027
},
2128
"dependencies": {
22-
"@prisma/client": "6.2.1"
29+
"@prisma/client": "6.2.1",
30+
"@types/readline-sync": "^1.4.8",
31+
"readline-sync": "^1.4.10"
2332
}
2433
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { ArgumentParser } from "argparse";
2+
import { spawn } from 'child_process';
3+
import { confirmAction } from "./utils";
4+
5+
// This script is used to run a prisma command with a database URL.
6+
7+
const parser = new ArgumentParser({
8+
add_help: false,
9+
});
10+
parser.add_argument("--url", { required: true, help: "Database URL" });
11+
12+
// Parse known args to get the URL, but preserve the rest
13+
const parsed = parser.parse_known_args();
14+
const args = parsed[0];
15+
const remainingArgs = parsed[1];
16+
17+
process.env.DATABASE_URL = args.url;
18+
19+
confirmAction(`command: prisma ${remainingArgs.join(' ')}\nurl: ${args.url}\n\nContinue? [N/y]`);
20+
21+
// Run prisma command with remaining arguments
22+
const prisma = spawn('npx', ['prisma', ...remainingArgs], {
23+
stdio: 'inherit',
24+
env: process.env
25+
});
26+
27+
prisma.on('exit', (code) => {
28+
process.exit(code);
29+
});

packages/db/tools/scriptRunner.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { PrismaClient } from "@sourcebot/db";
2+
import { ArgumentParser } from "argparse";
3+
import { migrateDuplicateConnections } from "./scripts/migrate-duplicate-connections";
4+
import { confirmAction } from "./utils";
5+
6+
export interface Script {
7+
run: (prisma: PrismaClient) => Promise<void>;
8+
}
9+
10+
export const scripts: Record<string, Script> = {
11+
"migrate-duplicate-connections": migrateDuplicateConnections,
12+
}
13+
14+
const parser = new ArgumentParser();
15+
parser.add_argument("--url", { required: true, help: "Database URL" });
16+
parser.add_argument("--script", { required: true, help: "Script to run" });
17+
const args = parser.parse_args();
18+
19+
(async () => {
20+
if (!(args.script in scripts)) {
21+
console.log("Invalid script");
22+
process.exit(1);
23+
}
24+
25+
const selectedScript = scripts[args.script];
26+
27+
console.log("\nTo confirm:");
28+
console.log(`- Database URL: ${args.url}`);
29+
console.log(`- Script: ${args.script}`);
30+
31+
confirmAction();
32+
33+
const prisma = new PrismaClient({
34+
datasourceUrl: args.url,
35+
});
36+
37+
await selectedScript.run(prisma);
38+
39+
console.log("\nDone.");
40+
process.exit(0);
41+
})();
42+
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { Script } from "../scriptRunner";
2+
import { PrismaClient } from "../../dist";
3+
import { confirmAction } from "../utils";
4+
5+
// Handles duplicate connections by renaming them to be unique.
6+
// @see: 20250320215449_unique_connection_name_constraint_within_org
7+
export const migrateDuplicateConnections: Script = {
8+
run: async (prisma: PrismaClient) => {
9+
10+
// Find all duplicate connections based on name and orgId
11+
const duplicates = (await prisma.connection.groupBy({
12+
by: ['name', 'orgId'],
13+
_count: {
14+
_all: true,
15+
},
16+
})).filter(({ _count }) => _count._all > 1);
17+
18+
console.log(`Found ${duplicates.reduce((acc, { _count }) => acc + _count._all, 0)} duplicate connections.`);
19+
20+
confirmAction();
21+
22+
let migrated = 0;
23+
24+
for (const duplicate of duplicates) {
25+
const { name, orgId } = duplicate;
26+
const connections = await prisma.connection.findMany({
27+
where: {
28+
name,
29+
orgId,
30+
},
31+
orderBy: {
32+
createdAt: 'asc',
33+
},
34+
});
35+
36+
for (let i = 0; i < connections.length; i++) {
37+
const connection = connections[i];
38+
const newName = `${name}-${i + 1}`;
39+
40+
console.log(`Migrating connection with id ${connection.id} from name=${name} to name=${newName}`);
41+
42+
await prisma.connection.update({
43+
where: { id: connection.id },
44+
data: { name: newName },
45+
});
46+
migrated++;
47+
}
48+
}
49+
50+
console.log(`Migrated ${migrated} connections.`);
51+
},
52+
};

packages/db/tools/utils.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import readline from 'readline-sync';
2+
3+
export const confirmAction = (message: string = "Are you sure you want to proceed? [N/y]") => {
4+
const response = readline.question(message).toLowerCase();
5+
if (response !== 'y') {
6+
console.log("Aborted.");
7+
process.exit(0);
8+
}
9+
}

yarn.lock

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3813,6 +3813,11 @@
38133813
"@types/prop-types" "*"
38143814
csstype "^3.0.2"
38153815

3816+
"@types/readline-sync@^1.4.8":
3817+
version "1.4.8"
3818+
resolved "https://registry.npmjs.org/@types/readline-sync/-/readline-sync-1.4.8.tgz#dc9767a93fc83825d90331f2549a2e90fc3255f0"
3819+
integrity sha512-BL7xOf0yKLA6baAX6MMOnYkoflUyj/c7y3pqMRfU0va7XlwHAOTOIo4x55P/qLfMsuaYdJJKubToLqRVmRtRZA==
3820+
38163821
"@types/send@*":
38173822
version "0.17.4"
38183823
resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.4.tgz#6619cd24e7270793702e4e6a4b958a9010cfc57a"
@@ -8822,6 +8827,11 @@ readdirp@~3.6.0:
88228827
dependencies:
88238828
picomatch "^2.2.1"
88248829

8830+
readline-sync@^1.4.10:
8831+
version "1.4.10"
8832+
resolved "https://registry.npmjs.org/readline-sync/-/readline-sync-1.4.10.tgz#41df7fbb4b6312d673011594145705bf56d8873b"
8833+
integrity sha512-gNva8/6UAe8QYepIQH/jQ2qn91Qj0B9sYjMBBs3QOB8F2CXcKgLxQaJRP76sWVRQt+QU+8fAkCbCvjjMFu7Ycw==
8834+
88258835
redis-errors@^1.0.0, redis-errors@^1.2.0:
88268836
version "1.2.0"
88278837
resolved "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz"
@@ -9574,14 +9584,7 @@ stringify-entities@^4.0.0:
95749584
character-entities-html4 "^2.0.0"
95759585
character-entities-legacy "^3.0.0"
95769586

9577-
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
9578-
version "6.0.1"
9579-
resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz"
9580-
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
9581-
dependencies:
9582-
ansi-regex "^5.0.1"
9583-
9584-
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
9587+
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
95859588
version "6.0.1"
95869589
resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz"
95879590
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==

0 commit comments

Comments
 (0)