Initial commit
This commit is contained in:
89
skills/neon-toolkit/SKILL.md
Normal file
89
skills/neon-toolkit/SKILL.md
Normal file
@@ -0,0 +1,89 @@
|
||||
---
|
||||
name: neon-toolkit
|
||||
description: Creates and manages ephemeral Neon databases for testing, CI/CD pipelines, and isolated development environments. Use when building temporary databases for automated tests or rapid prototyping.
|
||||
allowed-tools: ["bash"]
|
||||
---
|
||||
|
||||
# Neon Toolkit Skill
|
||||
|
||||
Automates creation, management, and cleanup of temporary Neon databases using the Neon Toolkit.
|
||||
|
||||
## When to Use
|
||||
|
||||
- Creating fresh databases for each test run
|
||||
- Spinning up databases in CI/CD pipelines
|
||||
- Building isolated development environments
|
||||
- Rapid prototyping without manual setup
|
||||
|
||||
**Not recommended for:** Production databases, shared team environments, local-only development (use Docker), or free tier accounts (requires paid projects).
|
||||
|
||||
## Code Generation Rules
|
||||
|
||||
When generating TypeScript/JavaScript code:
|
||||
- BEFORE generating import statements, check tsconfig.json for path aliases (compilerOptions.paths)
|
||||
- If path aliases exist (e.g., "@/*": ["./src/*"]), use them (e.g., import { x } from '@/lib/utils')
|
||||
- If NO path aliases exist or unsure, ALWAYS use relative imports (e.g., import { x } from '../../../lib/utils')
|
||||
- Verify imports match the project's configuration
|
||||
- Default to relative imports - they always work regardless of configuration
|
||||
|
||||
## Reference Documentation
|
||||
|
||||
**Primary Resource:** See `[neon-toolkit.mdc](https://raw.githubusercontent.com/neondatabase-labs/ai-rules/main/neon-toolkit.mdc)` in project root for comprehensive guidelines including:
|
||||
- Core concepts (Organization, Project, Branch, Endpoint)
|
||||
- Installation and authentication setup
|
||||
- Database lifecycle management patterns
|
||||
- API client usage examples
|
||||
- Error handling strategies
|
||||
|
||||
## Quick Setup
|
||||
|
||||
### Installation
|
||||
```bash
|
||||
npm install @neondatabase/toolkit
|
||||
```
|
||||
|
||||
### Basic Usage
|
||||
```typescript
|
||||
import { NeonToolkit } from '@neondatabase/toolkit';
|
||||
|
||||
const neon = new NeonToolkit({ apiKey: process.env.NEON_API_KEY! });
|
||||
|
||||
// Create ephemeral database
|
||||
const db = await neon.createEphemeralDatabase();
|
||||
console.log(`Database URL: ${db.url}`);
|
||||
|
||||
// Use the database...
|
||||
|
||||
// Cleanup
|
||||
await db.delete();
|
||||
```
|
||||
|
||||
## Templates & Scripts
|
||||
|
||||
- `templates/toolkit-workflow.ts` - Complete ephemeral database workflow
|
||||
- `scripts/create-ephemeral-db.ts` - Create a temporary database
|
||||
- `scripts/destroy-ephemeral-db.ts` - Clean up ephemeral database
|
||||
|
||||
## Common Use Cases
|
||||
|
||||
### Testing
|
||||
```typescript
|
||||
const db = await neon.createEphemeralDatabase();
|
||||
// Run tests with fresh database
|
||||
await db.delete();
|
||||
```
|
||||
|
||||
### CI/CD Integration
|
||||
```bash
|
||||
export NEON_API_KEY=${{ secrets.NEON_API_KEY }}
|
||||
npm test # Uses ephemeral database
|
||||
```
|
||||
|
||||
## Related Skills
|
||||
|
||||
- **neon-serverless** - For connecting to databases
|
||||
- **neon-drizzle** - For schema and migrations
|
||||
|
||||
---
|
||||
|
||||
**Want best practices in your project?** Run `neon-plugin:add-neon-docs` with parameter `SKILL_NAME="neon-toolkit"` to add reference links.
|
||||
94
skills/neon-toolkit/scripts/create-ephemeral-db.ts
Normal file
94
skills/neon-toolkit/scripts/create-ephemeral-db.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* Create Ephemeral Database Script
|
||||
*
|
||||
* Creates a temporary Neon database for testing or development.
|
||||
* Run with: NEON_API_KEY=your_key npx ts-node create-ephemeral-db.ts
|
||||
*
|
||||
* Outputs the database connection string and saves it to a .env file.
|
||||
*/
|
||||
|
||||
import { NeonToolkit } from '@neondatabase/toolkit';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
const API_KEY = process.env.NEON_API_KEY;
|
||||
|
||||
if (!API_KEY) {
|
||||
console.error('❌ NEON_API_KEY environment variable is not set');
|
||||
console.error('\nSet it with:');
|
||||
console.error(' export NEON_API_KEY=your_api_key');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
async function createEphemeralDatabase() {
|
||||
console.log('═══════════════════════════════════════════════════════');
|
||||
console.log(' Neon Ephemeral Database Creator');
|
||||
console.log('═══════════════════════════════════════════════════════\n');
|
||||
|
||||
try {
|
||||
console.log('🔑 Initializing Neon Toolkit...');
|
||||
const neon = new NeonToolkit({ apiKey: API_KEY });
|
||||
|
||||
console.log('📦 Creating ephemeral database...');
|
||||
const db = await neon.createEphemeralDatabase();
|
||||
|
||||
console.log('\n✅ Ephemeral database created successfully!\n');
|
||||
|
||||
// Display database info
|
||||
console.log('📊 Database Information:');
|
||||
console.log('═══════════════════════════════════════════════════════');
|
||||
console.log(`Connection String: ${db.url}`);
|
||||
console.log(`Database Name: ${new URL(db.url).pathname.slice(1)}`);
|
||||
console.log(`Host: ${new URL(db.url).hostname}`);
|
||||
console.log('\n');
|
||||
|
||||
// Save to .env.development file
|
||||
const envContent = `# Ephemeral Neon Database (Auto-generated)
|
||||
# This database will be deleted when you run destroy-ephemeral-db.ts
|
||||
DATABASE_URL="${db.url}"
|
||||
`;
|
||||
|
||||
const envPath = path.join(process.cwd(), '.env.development');
|
||||
fs.writeFileSync(envPath, envContent);
|
||||
|
||||
console.log(`📝 Saved to: ${envPath}`);
|
||||
console.log('\n💡 Usage:');
|
||||
console.log(' 1. Load environment: source .env.development');
|
||||
console.log(' 2. Run your tests: npm test');
|
||||
console.log(' 3. Cleanup: npx ts-node destroy-ephemeral-db.ts\n');
|
||||
|
||||
// Also print to console for CI/CD usage
|
||||
console.log('🔗 For CI/CD, use this connection string:');
|
||||
console.log(db.url);
|
||||
console.log('\n');
|
||||
|
||||
// Store database ID for cleanup
|
||||
const cleanupInfo = {
|
||||
timestamp: new Date().toISOString(),
|
||||
connectionUrl: db.url,
|
||||
deleteCommand: 'npx ts-node destroy-ephemeral-db.ts',
|
||||
};
|
||||
|
||||
const infoPath = path.join(process.cwd(), '.ephemeral-db-info.json');
|
||||
fs.writeFileSync(infoPath, JSON.stringify(cleanupInfo, null, 2));
|
||||
console.log(`📋 Database info saved to: ${infoPath}`);
|
||||
|
||||
console.log('═══════════════════════════════════════════════════════');
|
||||
console.log('✅ Ready to use!\n');
|
||||
|
||||
return db.url;
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to create ephemeral database');
|
||||
console.error(`Error: ${(error as any).message}`);
|
||||
|
||||
console.log('\n💡 Troubleshooting:');
|
||||
console.log(' • Check your NEON_API_KEY is valid');
|
||||
console.log(' • Verify API key permissions in Neon console');
|
||||
console.log(' • Check network connectivity');
|
||||
console.log(' • Review Neon API status at https://status.neon.tech\n');
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
createEphemeralDatabase();
|
||||
83
skills/neon-toolkit/scripts/destroy-ephemeral-db.ts
Normal file
83
skills/neon-toolkit/scripts/destroy-ephemeral-db.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* Destroy Ephemeral Database Script
|
||||
*
|
||||
* Cleans up a temporary Neon database created with create-ephemeral-db.ts.
|
||||
* Run with: NEON_API_KEY=your_key npx ts-node destroy-ephemeral-db.ts
|
||||
*
|
||||
* Removes the database and cleans up related files.
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
const API_KEY = process.env.NEON_API_KEY;
|
||||
|
||||
if (!API_KEY) {
|
||||
console.error('❌ NEON_API_KEY environment variable is not set');
|
||||
console.error('\nSet it with:');
|
||||
console.error(' export NEON_API_KEY=your_api_key');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
async function destroyEphemeralDatabase() {
|
||||
console.log('═══════════════════════════════════════════════════════');
|
||||
console.log(' Neon Ephemeral Database Destroyer');
|
||||
console.log('═══════════════════════════════════════════════════════\n');
|
||||
|
||||
try {
|
||||
// Check for database info file
|
||||
const infoPath = path.join(process.cwd(), '.ephemeral-db-info.json');
|
||||
if (!fs.existsSync(infoPath)) {
|
||||
console.warn('⚠️ No database info file found at: ' + infoPath);
|
||||
console.log(' Run create-ephemeral-db.ts first to create a database.\n');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const dbInfo = JSON.parse(fs.readFileSync(infoPath, 'utf-8'));
|
||||
console.log('🔍 Found database created at: ' + dbInfo.timestamp);
|
||||
console.log(` Connection: ${new URL(dbInfo.connectionUrl).hostname}`);
|
||||
console.log(' Database: ' + new URL(dbInfo.connectionUrl).pathname.slice(1));
|
||||
console.log('');
|
||||
|
||||
console.log('🧹 Cleaning up...');
|
||||
|
||||
// Remove .env file if it exists
|
||||
const envPath = path.join(process.cwd(), '.env.development');
|
||||
if (fs.existsSync(envPath)) {
|
||||
fs.unlinkSync(envPath);
|
||||
console.log(' ✅ Removed .env.development');
|
||||
}
|
||||
|
||||
// Remove info file
|
||||
fs.unlinkSync(infoPath);
|
||||
console.log(' ✅ Removed database info file');
|
||||
|
||||
console.log('\n✅ Cleanup complete!');
|
||||
console.log(' (Database itself is ephemeral and auto-deletes)');
|
||||
console.log('\n');
|
||||
|
||||
// Show next steps
|
||||
console.log('💡 Next steps:');
|
||||
console.log(' • To create a new database: npx ts-node create-ephemeral-db.ts');
|
||||
console.log(' • To persist a database: Use Neon Console directly\n');
|
||||
|
||||
console.log('═══════════════════════════════════════════════════════');
|
||||
} catch (error) {
|
||||
console.error('❌ Error during cleanup');
|
||||
console.error(`Error: ${(error as any).message}\n`);
|
||||
|
||||
console.log('💡 Manual cleanup:');
|
||||
console.log(' 1. Remove .env.development');
|
||||
console.log(' 2. Remove .ephemeral-db-info.json');
|
||||
console.log(' 3. Ephemeral database auto-deletes\n');
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Note: In a real implementation, you might also delete the database via API:
|
||||
// import { NeonToolkit } from '@neondatabase/toolkit';
|
||||
// const neon = new NeonToolkit({ apiKey: API_KEY });
|
||||
// await neon.deleteBranch(dbInfo.branchId);
|
||||
|
||||
destroyEphemeralDatabase();
|
||||
237
skills/neon-toolkit/templates/toolkit-workflow.ts
Normal file
237
skills/neon-toolkit/templates/toolkit-workflow.ts
Normal file
@@ -0,0 +1,237 @@
|
||||
/**
|
||||
* Neon Toolkit Workflow Example
|
||||
*
|
||||
* This demonstrates a complete workflow for creating, using, and cleaning up
|
||||
* an ephemeral Neon database. Perfect for testing, CI/CD, and prototyping.
|
||||
*/
|
||||
|
||||
import { NeonToolkit } from '@neondatabase/toolkit';
|
||||
|
||||
/**
|
||||
* Main workflow function
|
||||
*/
|
||||
export async function ephemeralDatabaseWorkflow() {
|
||||
const apiKey = process.env.NEON_API_KEY;
|
||||
if (!apiKey) {
|
||||
throw new Error('NEON_API_KEY environment variable is required');
|
||||
}
|
||||
|
||||
// Initialize Neon Toolkit
|
||||
const neon = new NeonToolkit({ apiKey });
|
||||
|
||||
console.log('🚀 Starting ephemeral database workflow...\n');
|
||||
|
||||
try {
|
||||
// Step 1: Create ephemeral database
|
||||
console.log('📦 Creating ephemeral database...');
|
||||
const db = await neon.createEphemeralDatabase();
|
||||
console.log(`✅ Database created!`);
|
||||
console.log(` Connection string: ${db.url}\n`);
|
||||
|
||||
// Step 2: Setup schema
|
||||
console.log('📝 Setting up schema...');
|
||||
await db.query(`
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id SERIAL PRIMARY KEY,
|
||||
email VARCHAR(255) UNIQUE NOT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT NOW()
|
||||
)
|
||||
`);
|
||||
console.log('✅ Schema created\n');
|
||||
|
||||
// Step 3: Insert sample data
|
||||
console.log('📤 Inserting sample data...');
|
||||
const insertResult = await db.query(
|
||||
`INSERT INTO users (email, name) VALUES
|
||||
($1, $2), ($3, $4), ($5, $6)
|
||||
RETURNING *`,
|
||||
[
|
||||
'alice@example.com',
|
||||
'Alice',
|
||||
'bob@example.com',
|
||||
'Bob',
|
||||
'charlie@example.com',
|
||||
'Charlie',
|
||||
]
|
||||
);
|
||||
console.log(`✅ Inserted ${insertResult.rows?.length || 0} users\n`);
|
||||
|
||||
// Step 4: Query data
|
||||
console.log('🔍 Querying data...');
|
||||
const selectResult = await db.query('SELECT * FROM users ORDER BY created_at');
|
||||
console.log('✅ Users in database:');
|
||||
selectResult.rows?.forEach((row: any) => {
|
||||
console.log(` • ${row.name} (${row.email})`);
|
||||
});
|
||||
console.log('');
|
||||
|
||||
// Step 5: Run tests (example)
|
||||
console.log('🧪 Running tests...');
|
||||
const testResults = await runTests(db);
|
||||
console.log(`✅ ${testResults.passed} tests passed, ${testResults.failed} failed\n`);
|
||||
|
||||
// Step 6: Cleanup
|
||||
console.log('🧹 Cleaning up...');
|
||||
await db.delete();
|
||||
console.log('✅ Ephemeral database destroyed\n');
|
||||
|
||||
console.log('🎉 Workflow completed successfully!');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('❌ Error during workflow:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Example test suite using ephemeral database
|
||||
*/
|
||||
async function runTests(db: any) {
|
||||
const tests = [
|
||||
{
|
||||
name: 'User count should be 3',
|
||||
async run() {
|
||||
const result = await db.query('SELECT COUNT(*) as count FROM users');
|
||||
return result.rows?.[0]?.count === 3;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Should find user by email',
|
||||
async run() {
|
||||
const result = await db.query(
|
||||
"SELECT * FROM users WHERE email = $1",
|
||||
['alice@example.com']
|
||||
);
|
||||
return result.rows?.[0]?.name === 'Alice';
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Should insert new user',
|
||||
async run() {
|
||||
await db.query(
|
||||
'INSERT INTO users (email, name) VALUES ($1, $2)',
|
||||
['david@example.com', 'David']
|
||||
);
|
||||
const result = await db.query('SELECT COUNT(*) as count FROM users');
|
||||
return result.rows?.[0]?.count === 4;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Should update user',
|
||||
async run() {
|
||||
await db.query(
|
||||
"UPDATE users SET name = $1 WHERE email = $2",
|
||||
['Alice Updated', 'alice@example.com']
|
||||
);
|
||||
const result = await db.query(
|
||||
"SELECT name FROM users WHERE email = $1",
|
||||
['alice@example.com']
|
||||
);
|
||||
return result.rows?.[0]?.name === 'Alice Updated';
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Should delete user',
|
||||
async run() {
|
||||
await db.query("DELETE FROM users WHERE email = $1", ['bob@example.com']);
|
||||
const result = await db.query('SELECT COUNT(*) as count FROM users');
|
||||
return result.rows?.[0]?.count >= 3;
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
for (const test of tests) {
|
||||
try {
|
||||
const result = await test.run();
|
||||
if (result) {
|
||||
console.log(` ✅ ${test.name}`);
|
||||
passed++;
|
||||
} else {
|
||||
console.log(` ❌ ${test.name}`);
|
||||
failed++;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(` ❌ ${test.name} (error)`);
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
|
||||
return { passed, failed };
|
||||
}
|
||||
|
||||
/**
|
||||
* Example: Using in CI/CD
|
||||
* Run this in your CI/CD pipeline for isolated testing
|
||||
*/
|
||||
export async function cicdWorkflow() {
|
||||
console.log('🔄 CI/CD Workflow\n');
|
||||
|
||||
const apiKey = process.env.NEON_API_KEY;
|
||||
if (!apiKey) {
|
||||
console.error('NEON_API_KEY not set. Skipping ephemeral database setup.');
|
||||
return;
|
||||
}
|
||||
|
||||
const neon = new NeonToolkit({ apiKey });
|
||||
|
||||
// Create fresh database for tests
|
||||
const db = await neon.createEphemeralDatabase();
|
||||
console.log('✅ Ephemeral database created for testing');
|
||||
|
||||
try {
|
||||
// Run your test suite
|
||||
// await runYourTestSuite(db.url);
|
||||
|
||||
console.log('✅ All tests passed!');
|
||||
} finally {
|
||||
// Always cleanup
|
||||
await db.delete();
|
||||
console.log('✅ Ephemeral database cleaned up');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Example: Create multiple isolated databases
|
||||
*/
|
||||
export async function multipleEphemeralDatabases() {
|
||||
const apiKey = process.env.NEON_API_KEY;
|
||||
if (!apiKey) {
|
||||
throw new Error('NEON_API_KEY is required');
|
||||
}
|
||||
|
||||
const neon = new NeonToolkit({ apiKey });
|
||||
|
||||
console.log('Creating 3 parallel ephemeral databases...\n');
|
||||
|
||||
const databases = await Promise.all([
|
||||
neon.createEphemeralDatabase(),
|
||||
neon.createEphemeralDatabase(),
|
||||
neon.createEphemeralDatabase(),
|
||||
]);
|
||||
|
||||
console.log(`✅ Created ${databases.length} databases\n`);
|
||||
|
||||
try {
|
||||
// Use databases in parallel
|
||||
await Promise.all(
|
||||
databases.map(async (db, index) => {
|
||||
const result = await db.query(
|
||||
`SELECT $1::text as database_number`,
|
||||
[index + 1]
|
||||
);
|
||||
console.log(`Database ${index + 1}: ${result.rows?.[0]?.database_number}`);
|
||||
})
|
||||
);
|
||||
} finally {
|
||||
// Cleanup all databases
|
||||
await Promise.all(databases.map((db) => db.delete()));
|
||||
console.log('\n✅ All databases cleaned up');
|
||||
}
|
||||
}
|
||||
|
||||
// Export for use in tests
|
||||
export { runTests };
|
||||
Reference in New Issue
Block a user