Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: use kysely for handling migrations & add scripts #20

Merged
merged 4 commits into from
Nov 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 112 additions & 0 deletions apps/scripts/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# grants-stack-indexer: scripts

This package contains scripts for managing the database schema and migrations.

## Available Scripts

| Script | Description |
| ------------------- | --------------------------------------- |
| `script:db:migrate` | Runs all pending database migrations |
| `script:db:reset` | Drops and recreates the database schema |

## Environment Setup

1. Create a `.env` file in the `apps/scripts` directory:

```env
# Database connection URL
DATABASE_URL=postgresql://user:password@localhost:5432/mydb

# Schema name to manage
DATABASE_SCHEMA=grants_stack
```

### Environment Variables

| Variable | Description | Example |
| ----------------- | ------------------------- | ------------------------------------------------ |
| `DATABASE_URL` | PostgreSQL connection URL | `postgresql://user:password@localhost:5432/mydb` |
| `DATABASE_SCHEMA` | Database schema name | `grants_stack` |

## Usage

First, install dependencies:

```bash
pnpm install
```

### Running Migrations

To apply all pending migrations:

```bash
pnpm script:db:migrate
```

This will:

1. Load environment variables
2. Connect to the database
3. Create the schema if it doesn't exist
4. Run any pending migrations
5. Log the results
Comment on lines +31 to +53
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking about the "productive" stage of this app, how should migrations be applied? Should the operator stop the indexer, apply the migrations and then restart the indexer? Can we afford the indexer to be stopped to apply migrations or do you need a more sophisticated strategy?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice question ser, we yet have to discuss how this should work in prod but definitely the processor will need to be stopped

Copy link
Collaborator

@0xkenj1 0xkenj1 Nov 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


### Resetting the Database

To completely reset the database schema:

```bash
pnpm script:db:reset
```

**Warning**: This will:

1. Drop the existing schema and all its data
2. Recreate an empty schema
3. You'll need to run migrations again after reset

## Development

### Adding New Migrations

1. Create a new migration file in [`packages/repository/src/migrations`](../../packages//repository//migrations)
2. Name it using the format: `YYYYMMDDTHHmmss_description.ts`
3. Implement the `up` and `down` functions
4. Run `pnpm script:db:migrate` to apply the new migration

Example migration file:

```typescript
import { Kysely } from "kysely";

export async function up(db: Kysely<any>): Promise<void> {
// Your migration code here
}

export async function down(db: Kysely<any>): Promise<void> {
// Code to reverse the migration
}
```
Comment on lines +78 to +90
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extremely nice to have but it'd be cool to have a third script to be able to run something like:

pnpm script:db:prepare <migration name>

And automagically generate the timestamped migration file with this sweet placeholder.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

aaa 💯 i think kysely has a cli-tool but i can easily home-craft it

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


## Troubleshooting

### Common Issues

1. **Connection Error**

- Check if PostgreSQL is running
- Verify DATABASE_URL is correct
- Ensure the database exists

2. **Permission Error**

- Verify user has necessary permissions
- Check schema ownership

3. **Migration Failed**
- Check migration logs
- Ensure no conflicting changes
- Verify schema consistency

TODO: add E2E tests for the scripts
36 changes: 36 additions & 0 deletions apps/scripts/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"name": "@grants-stack-indexer/scripts",
"version": "0.0.1",
"private": true,
"description": "",
"license": "MIT",
"author": "Wonderland",
"type": "module",
"directories": {
"src": "src"
},
"files": [
"package.json"
],
"scripts": {
"build": "tsc -p tsconfig.build.json",
"check-types": "tsc --noEmit -p ./tsconfig.json",
"clean": "rm -rf dist/",
"format": "prettier --check \"{src,test}/**/*.{js,ts,json}\"",
"format:fix": "prettier --write \"{src,test}/**/*.{js,ts,json}\"",
"lint": "eslint \"{src,test}/**/*.{js,ts,json}\"",
"lint:fix": "pnpm lint --fix",
"script:db:migrate": "tsx src/migrateDb.script.ts",
"script:db:reset": "tsx src/resetDb.script.ts",
"test": "vitest run --config vitest.config.ts --passWithNoTests",
"test:cov": "vitest run --config vitest.config.ts --coverage --passWithNoTests"
},
"dependencies": {
"@grants-stack-indexer/repository": "workspace:*",
"dotenv": "16.4.5",
"zod": "3.23.8"
},
"devDependencies": {
"tsx": "4.19.2"
}
}
69 changes: 69 additions & 0 deletions apps/scripts/src/migrateDb.script.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { configDotenv } from "dotenv";

import { createKyselyDatabase, migrateToLatest } from "@grants-stack-indexer/repository";

import { getDatabaseConfigFromEnv } from "./schemas/index.js";

configDotenv();

/**
* This script handles database migrations for the grants-stack-indexer project.
*
* It performs the following steps:
* 1. Loads environment variables from .env file
* 2. Gets database configuration (URL and schema name) from environment
* 3. Creates a Kysely database connection with the specified schema
* 4. Runs any pending migrations from packages/repository/migrations
* 5. Reports success/failure of migrations
* 6. Closes database connection and exits
*
* Environment variables required:
* - DATABASE_URL: PostgreSQL connection string
* - DATABASE_SCHEMA: Schema name to migrate (e.g. "grants_stack")
*
* The script will:
* - Create the schema if it doesn't exist
* - Run all pending migrations in order
* - Log results of each migration
* - Exit with code 0 on success, 1 on failure
*/

export const main = async (): Promise<void> => {
const { DATABASE_URL, DATABASE_SCHEMA } = getDatabaseConfigFromEnv();

const db = createKyselyDatabase({
connectionString: DATABASE_URL,
withSchema: DATABASE_SCHEMA,
});

console.log(`Migrating database schema '${DATABASE_SCHEMA}'...`);

const migrationResults = await migrateToLatest({
db,
schema: DATABASE_SCHEMA,
});

if (migrationResults && migrationResults?.length > 0) {
const failedMigrations = migrationResults.filter(
(migrationResult) => migrationResult.status === "Error",
);

if (failedMigrations.length > 0) {
console.error("❌ Failed migrations:", failedMigrations);
throw new Error("Failed migrations");
}

console.log(`✅ Migrations applied successfully`);
} else {
console.log("No migrations to apply");
}

await db.destroy();

process.exit(0);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This process.exit(0) might not be needed as 0 should be the default Node exit code (unless there's something I'm missing and you want to explicitly exit the process)

};

main().catch((error) => {
console.error(error);
process.exit(1);
});
70 changes: 70 additions & 0 deletions apps/scripts/src/resetDb.script.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { configDotenv } from "dotenv";

import { createKyselyDatabase, resetDatabase } from "@grants-stack-indexer/repository";

import { getDatabaseConfigFromEnv } from "./schemas/index.js";

configDotenv();

/**
* This script handles database reset for the grants-stack-indexer project.
*
* It performs the following steps:
* 1. Loads environment variables from .env file
* 2. Gets database configuration (URL and schema name) from environment
* 3. Creates a Kysely database connection with the specified schema
* 4. Drops and recreates the database schema
* 5. Reports success/failure of reset operation
* 6. Closes database connection and exits
*
* Environment variables required:
* - DATABASE_URL: PostgreSQL connection string
* - DATABASE_SCHEMA: Schema name to reset (e.g. "grants_stack")
*
* The script will:
* - Drop the schema if it exists
* - Recreate an empty schema
* - Log results of the reset operation
* - Exit with code 0 on success, 1 on failure
*
* WARNING: This is a destructive operation that will delete all data in the schema.
* Make sure you have backups if needed before running this script.
*/

const main = async (): Promise<void> => {
const { DATABASE_URL, DATABASE_SCHEMA } = getDatabaseConfigFromEnv();

const db = createKyselyDatabase({
connectionString: DATABASE_URL,
withSchema: DATABASE_SCHEMA,
});

console.log(`Resetting database schema '${DATABASE_SCHEMA}'...`);

const resetResults = await resetDatabase({
db,
schema: DATABASE_SCHEMA,
});

if (resetResults && resetResults?.length > 0) {
const failedResets = resetResults.filter((resetResult) => resetResult.status === "Error");

if (failedResets.length > 0) {
console.error("❌ Failed resets:", failedResets);
throw new Error("Failed resets");
}

console.log(`✅ Reset applied successfully`);
} else {
console.log("No resets to apply");
}

await db.destroy();

process.exit(0);
};

main().catch((error) => {
console.error(error);
process.exit(1);
});
19 changes: 19 additions & 0 deletions apps/scripts/src/schemas/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { z } from "zod";

const dbEnvSchema = z.object({
DATABASE_URL: z.string().url(),
DATABASE_SCHEMA: z.string().min(1),
});

export type DbEnvConfig = z.infer<typeof dbEnvSchema>;

export function getDatabaseConfigFromEnv(): DbEnvConfig {
const result = dbEnvSchema.safeParse(process.env);

if (!result.success) {
console.error("❌ Invalid environment variables:", result.error.format());
throw new Error("Invalid environment variables");
}

return result.data;
}
11 changes: 11 additions & 0 deletions apps/scripts/tsconfig.build.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"extends": "../../tsconfig.build.json",
"compilerOptions": {
"composite": true,
"declarationMap": true,
"declaration": true,
"outDir": "dist"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "test"]
}
4 changes: 4 additions & 0 deletions apps/scripts/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"extends": "../../tsconfig.json",
"include": ["src/**/*", "test/**/*"]
}
22 changes: 22 additions & 0 deletions apps/scripts/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import path from "path";
import { configDefaults, defineConfig } from "vitest/config";

export default defineConfig({
test: {
globals: true, // Use Vitest's global API without importing it in each file
environment: "node", // Use the Node.js environment
include: ["test/**/*.spec.ts"], // Include test files
exclude: ["node_modules", "dist"], // Exclude certain directories
coverage: {
provider: "v8",
reporter: ["text", "json", "html"], // Coverage reporters
exclude: ["node_modules", "dist", ...configDefaults.exclude], // Files to exclude from coverage
},
},
resolve: {
alias: {
// Setup path alias based on tsconfig paths
"@": path.resolve(__dirname, "src"),
},
},
});
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
"lint": "turbo run lint",
"lint:fix": "turbo run lint:fix",
"prepare": "husky",
"script:db:migrate": "pnpm run --filter @grants-stack-indexer/scripts script:db:migrate",
"script:db:reset": "pnpm run --filter @grants-stack-indexer/scripts script:db:reset",
"start": "turbo run start",
"test": "turbo run test",
"test:cov": "turbo run test:cov",
Expand Down
Loading