Skip to content

Commit 2ca27bf

Browse files
committed
feat: use kysely for handling migrations & add scripts
1 parent 9ad1569 commit 2ca27bf

File tree

16 files changed

+1137
-4
lines changed

16 files changed

+1137
-4
lines changed

apps/scripts/README.md

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
# grants-stack-indexer: scripts
2+
3+
This package contains scripts for managing the database schema and migrations.
4+
5+
## Available Scripts
6+
7+
| Script | Description |
8+
| ------------------- | --------------------------------------- |
9+
| `script:db:migrate` | Runs all pending database migrations |
10+
| `script:db:reset` | Drops and recreates the database schema |
11+
12+
## Environment Setup
13+
14+
1. Create a `.env` file in the `apps/scripts` directory:
15+
16+
```env
17+
# Database connection URL
18+
DATABASE_URL=postgresql://user:password@localhost:5432/mydb
19+
20+
# Schema name to manage
21+
DATABASE_SCHEMA=grants_stack
22+
```
23+
24+
### Environment Variables
25+
26+
| Variable | Description | Example |
27+
| ----------------- | ------------------------- | ------------------------------------------------ |
28+
| `DATABASE_URL` | PostgreSQL connection URL | `postgresql://user:password@localhost:5432/mydb` |
29+
| `DATABASE_SCHEMA` | Database schema name | `grants_stack` |
30+
31+
## Usage
32+
33+
First, install dependencies:
34+
35+
```bash
36+
pnpm install
37+
```
38+
39+
### Running Migrations
40+
41+
To apply all pending migrations:
42+
43+
```bash
44+
pnpm script:db:migrate
45+
```
46+
47+
This will:
48+
49+
1. Load environment variables
50+
2. Connect to the database
51+
3. Create the schema if it doesn't exist
52+
4. Run any pending migrations
53+
5. Log the results
54+
55+
### Resetting the Database
56+
57+
To completely reset the database schema:
58+
59+
```bash
60+
pnpm script:db:reset
61+
```
62+
63+
**Warning**: This will:
64+
65+
1. Drop the existing schema and all its data
66+
2. Recreate an empty schema
67+
3. You'll need to run migrations again after reset
68+
69+
## Development
70+
71+
### Adding New Migrations
72+
73+
1. Create a new migration file in [`packages/repository/src/migrations`](../../packages//repository//migrations)
74+
2. Name it using the format: `YYYYMMDDTHHmmss_description.ts`
75+
3. Implement the `up` and `down` functions
76+
4. Run `pnpm script:db:migrate` to apply the new migration
77+
78+
Example migration file:
79+
80+
```typescript
81+
import { Kysely } from "kysely";
82+
83+
export async function up(db: Kysely<any>): Promise<void> {
84+
// Your migration code here
85+
}
86+
87+
export async function down(db: Kysely<any>): Promise<void> {
88+
// Code to reverse the migration
89+
}
90+
```
91+
92+
## Troubleshooting
93+
94+
### Common Issues
95+
96+
1. **Connection Error**
97+
98+
- Check if PostgreSQL is running
99+
- Verify DATABASE_URL is correct
100+
- Ensure the database exists
101+
102+
2. **Permission Error**
103+
104+
- Verify user has necessary permissions
105+
- Check schema ownership
106+
107+
3. **Migration Failed**
108+
- Check migration logs
109+
- Ensure no conflicting changes
110+
- Verify schema consistency

apps/scripts/package.json

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
{
2+
"name": "@grants-stack-indexer/scripts",
3+
"version": "0.0.1",
4+
"private": true,
5+
"description": "",
6+
"license": "MIT",
7+
"author": "Wonderland",
8+
"type": "module",
9+
"directories": {
10+
"src": "src"
11+
},
12+
"files": [
13+
"package.json"
14+
],
15+
"scripts": {
16+
"build": "tsc -p tsconfig.build.json",
17+
"check-types": "tsc --noEmit -p ./tsconfig.json",
18+
"clean": "rm -rf dist/",
19+
"format": "prettier --check \"{src,test}/**/*.{js,ts,json}\"",
20+
"format:fix": "prettier --write \"{src,test}/**/*.{js,ts,json}\"",
21+
"lint": "eslint \"{src,test}/**/*.{js,ts,json}\"",
22+
"lint:fix": "pnpm lint --fix",
23+
"script:db:migrate": "tsx src/migrateDb.script.ts",
24+
"script:db:reset": "tsx src/resetDb.script.ts",
25+
"test": "vitest run --config vitest.config.ts --passWithNoTests",
26+
"test:cov": "vitest run --config vitest.config.ts --coverage"
27+
},
28+
"dependencies": {
29+
"@grants-stack-indexer/repository": "workspace:*",
30+
"dotenv": "16.4.5",
31+
"zod": "3.23.8"
32+
},
33+
"devDependencies": {
34+
"tsx": "4.19.2"
35+
}
36+
}

apps/scripts/src/migrateDb.script.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { configDotenv } from "dotenv";
2+
3+
import { createKyselyDatabase, migrateToLatest } from "@grants-stack-indexer/repository";
4+
5+
import { getDatabaseConfigFromEnv } from "./schemas/index.js";
6+
7+
configDotenv();
8+
9+
/**
10+
* This script handles database migrations for the grants-stack-indexer project.
11+
*
12+
* It performs the following steps:
13+
* 1. Loads environment variables from .env file
14+
* 2. Gets database configuration (URL and schema name) from environment
15+
* 3. Creates a Kysely database connection with the specified schema
16+
* 4. Runs any pending migrations from packages/repository/migrations
17+
* 5. Reports success/failure of migrations
18+
* 6. Closes database connection and exits
19+
*
20+
* Environment variables required:
21+
* - DATABASE_URL: PostgreSQL connection string
22+
* - DATABASE_SCHEMA: Schema name to migrate (e.g. "grants_stack")
23+
*
24+
* The script will:
25+
* - Create the schema if it doesn't exist
26+
* - Run all pending migrations in order
27+
* - Log results of each migration
28+
* - Exit with code 0 on success, 1 on failure
29+
*/
30+
31+
export const main = async (): Promise<void> => {
32+
const { DATABASE_URL, DATABASE_SCHEMA } = getDatabaseConfigFromEnv();
33+
34+
const db = createKyselyDatabase({
35+
connectionString: DATABASE_URL,
36+
withSchema: DATABASE_SCHEMA,
37+
});
38+
39+
console.log(`Migrating database schema '${DATABASE_SCHEMA}'...`);
40+
41+
const migrationResults = await migrateToLatest({
42+
db,
43+
schema: DATABASE_SCHEMA,
44+
});
45+
46+
if (migrationResults && migrationResults?.length > 0) {
47+
const failedMigrations = migrationResults.filter(
48+
(migrationResult) => migrationResult.status === "Error",
49+
);
50+
51+
if (failedMigrations.length > 0) {
52+
console.error("❌ Failed migrations:", failedMigrations);
53+
throw new Error("Failed migrations");
54+
}
55+
56+
console.log(`✅ Migrations applied successfully`);
57+
} else {
58+
console.log("No migrations to apply");
59+
}
60+
61+
await db.destroy();
62+
63+
process.exit(0);
64+
};
65+
66+
main().catch((error) => {
67+
console.error(error);
68+
process.exit(1);
69+
});

apps/scripts/src/resetDb.script.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { configDotenv } from "dotenv";
2+
3+
import { createKyselyDatabase, resetDatabase } from "@grants-stack-indexer/repository";
4+
5+
import { getDatabaseConfigFromEnv } from "./schemas/index.js";
6+
7+
configDotenv();
8+
9+
/**
10+
* This script handles database reset for the grants-stack-indexer project.
11+
*
12+
* It performs the following steps:
13+
* 1. Loads environment variables from .env file
14+
* 2. Gets database configuration (URL and schema name) from environment
15+
* 3. Creates a Kysely database connection with the specified schema
16+
* 4. Drops and recreates the database schema
17+
* 5. Reports success/failure of reset operation
18+
* 6. Closes database connection and exits
19+
*
20+
* Environment variables required:
21+
* - DATABASE_URL: PostgreSQL connection string
22+
* - DATABASE_SCHEMA: Schema name to reset (e.g. "grants_stack")
23+
*
24+
* The script will:
25+
* - Drop the schema if it exists
26+
* - Recreate an empty schema
27+
* - Log results of the reset operation
28+
* - Exit with code 0 on success, 1 on failure
29+
*
30+
* WARNING: This is a destructive operation that will delete all data in the schema.
31+
* Make sure you have backups if needed before running this script.
32+
*/
33+
34+
const main = async (): Promise<void> => {
35+
const { DATABASE_URL, DATABASE_SCHEMA } = getDatabaseConfigFromEnv();
36+
37+
const db = createKyselyDatabase({
38+
connectionString: DATABASE_URL,
39+
withSchema: DATABASE_SCHEMA,
40+
});
41+
42+
console.log(`Resetting database schema '${DATABASE_SCHEMA}'...`);
43+
44+
const resetResults = await resetDatabase({
45+
db,
46+
schema: DATABASE_SCHEMA,
47+
});
48+
49+
if (resetResults && resetResults?.length > 0) {
50+
const failedResets = resetResults.filter((resetResult) => resetResult.status === "Error");
51+
52+
if (failedResets.length > 0) {
53+
console.error("❌ Failed resets:", failedResets);
54+
throw new Error("Failed resets");
55+
}
56+
57+
console.log(`✅ Reset applied successfully`);
58+
} else {
59+
console.log("No resets to apply");
60+
}
61+
62+
await db.destroy();
63+
64+
process.exit(0);
65+
};
66+
67+
main().catch((error) => {
68+
console.error(error);
69+
process.exit(1);
70+
});

apps/scripts/src/schemas/index.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { z } from "zod";
2+
3+
const dbEnvSchema = z.object({
4+
DATABASE_URL: z.string().url(),
5+
DATABASE_SCHEMA: z.string().min(1),
6+
});
7+
8+
export type DbEnvConfig = z.infer<typeof dbEnvSchema>;
9+
10+
export function getDatabaseConfigFromEnv(): DbEnvConfig {
11+
const result = dbEnvSchema.safeParse(process.env);
12+
13+
if (!result.success) {
14+
console.error("❌ Invalid environment variables:", result.error.format());
15+
throw new Error("Invalid environment variables");
16+
}
17+
18+
return result.data;
19+
}

apps/scripts/tsconfig.build.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"extends": "../../tsconfig.build.json",
3+
"compilerOptions": {
4+
"composite": true,
5+
"declarationMap": true,
6+
"declaration": true,
7+
"outDir": "dist"
8+
},
9+
"include": ["src/**/*"],
10+
"exclude": ["node_modules", "dist", "test"]
11+
}

apps/scripts/tsconfig.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"extends": "../../tsconfig.json",
3+
"include": ["src/**/*", "test/**/*"]
4+
}

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
"lint": "turbo run lint",
1818
"lint:fix": "turbo run lint:fix",
1919
"prepare": "husky",
20+
"script:db:migrate": "pnpm run --filter @grants-stack-indexer/scripts script:db:migrate",
21+
"script:db:reset": "pnpm run --filter @grants-stack-indexer/scripts script:db:reset",
2022
"start": "turbo run start",
2123
"test": "turbo run test",
2224
"test:cov": "turbo run test:cov",

packages/repository/src/db/connection.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { CamelCasePlugin, Kysely, PostgresDialect } from "kysely";
2-
import { Pool, PoolConfig } from "pg";
1+
import { CamelCasePlugin, Kysely, PostgresDialect, WithSchemaPlugin } from "kysely";
2+
import pg from "pg";
33

44
import {
55
PendingProjectRole as PendingProjectRoleTable,
@@ -10,8 +10,11 @@ import {
1010
Round as RoundTable,
1111
} from "../internal.js";
1212

13-
export interface DatabaseConfig extends PoolConfig {
13+
const { Pool } = pg;
14+
15+
export interface DatabaseConfig extends pg.PoolConfig {
1416
connectionString: string;
17+
withSchema?: string;
1518
}
1619

1720
export interface Database {
@@ -27,10 +30,15 @@ export interface Database {
2730
* Creates and configures a Kysely database instance for PostgreSQL.
2831
*
2932
* @param config - The database configuration object extending PoolConfig.
33+
* @param config.connectionString - The connection string for the database.
34+
* @param config.withSchema - The schema to use for the database. Defaults to `public`.
3035
* @returns A configured Kysely instance for the Database.
3136
*
3237
* This function sets up a PostgreSQL database connection using Kysely ORM.
3338
*
39+
* It uses the `CamelCasePlugin` to convert all table names to camel case.
40+
* It uses the `WithSchemaPlugin` to automatically prefix all table names with the schema name on queries.
41+
*
3442
* @example
3543
* const dbConfig: DatabaseConfig = {
3644
* connectionString: 'postgresql://user:password@localhost:5432/mydb'
@@ -48,5 +56,10 @@ export const createKyselyPostgresDb = (config: DatabaseConfig): Kysely<Database>
4856
}),
4957
});
5058

51-
return new Kysely<Database>({ dialect, plugins: [new CamelCasePlugin()] });
59+
const withSchema = config.withSchema ?? "public";
60+
61+
return new Kysely<Database>({
62+
dialect,
63+
plugins: [new CamelCasePlugin(), new WithSchemaPlugin(withSchema)],
64+
});
5265
};

packages/repository/src/db/helpers.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { SchemaModule } from "kysely";
2+
3+
/**
4+
* Since WithSchemaPlugin doesn't work with `sql.table`, we need to get the schema name manually.
5+
* ref: https://github.com/kysely-org/kysely/issues/761
6+
*/
7+
export const getSchemaName = (schema: SchemaModule): string => {
8+
let name = "public";
9+
schema.createTable("test").$call((b) => {
10+
name = b.toOperationNode().table.table.schema?.name ?? "public";
11+
});
12+
return name;
13+
};

0 commit comments

Comments
 (0)