Initial push of Plunk Next
This commit is contained in:
@@ -0,0 +1,186 @@
|
||||
import {PrismaClient} from '@plunk/db';
|
||||
import {execSync} from 'child_process';
|
||||
|
||||
/**
|
||||
* Test database helper
|
||||
* Manages test database isolation and cleanup
|
||||
*/
|
||||
class TestDatabase {
|
||||
private prisma: PrismaClient | null = null;
|
||||
|
||||
async initialize() {
|
||||
// Use test database URL if provided, otherwise use main database
|
||||
const databaseUrl = process.env.TEST_DATABASE_URL || process.env.DATABASE_URL;
|
||||
|
||||
if (!databaseUrl) {
|
||||
throw new Error('DATABASE_URL or TEST_DATABASE_URL must be set for testing');
|
||||
}
|
||||
|
||||
// Create Prisma client with connection pool limits
|
||||
this.prisma = new PrismaClient({
|
||||
datasources: {
|
||||
db: {
|
||||
url: databaseUrl,
|
||||
},
|
||||
},
|
||||
// Limit connection pool to prevent memory issues in tests
|
||||
// @ts-ignore - These options exist but may not be in types
|
||||
__internal: {
|
||||
engine: {
|
||||
connection_limit: 5,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Connect to database
|
||||
await this.prisma.$connect();
|
||||
|
||||
// Run migrations (only once per test suite)
|
||||
try {
|
||||
execSync('yarn workspace @plunk/db migrate:dev', {
|
||||
env: {
|
||||
...process.env,
|
||||
DATABASE_URL: databaseUrl,
|
||||
},
|
||||
stdio: 'ignore',
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn('Migration warning (may already be up to date):', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Prisma client instance
|
||||
*/
|
||||
getClient(): PrismaClient {
|
||||
if (!this.prisma) {
|
||||
throw new Error('Database not initialized. Call initialize() first.');
|
||||
}
|
||||
return this.prisma;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up database after each test
|
||||
* Deletes all records in reverse order of dependencies
|
||||
* Uses batched deletes to prevent memory issues with large datasets
|
||||
*/
|
||||
async cleanup() {
|
||||
if (!this.prisma) return;
|
||||
|
||||
try {
|
||||
// Use a transaction to ensure all deletes happen atomically
|
||||
// This prevents foreign key constraint violations and race conditions
|
||||
await this.prisma.$transaction([
|
||||
// Level 1: Delete deepest dependencies first
|
||||
this.prisma.event.deleteMany(),
|
||||
this.prisma.workflowStepExecution.deleteMany(),
|
||||
|
||||
// Level 2: Delete entities that depend on Level 1
|
||||
this.prisma.email.deleteMany(),
|
||||
this.prisma.workflowExecution.deleteMany(),
|
||||
|
||||
// Level 3: Delete workflow structure
|
||||
this.prisma.workflowTransition.deleteMany(),
|
||||
this.prisma.workflowStep.deleteMany(),
|
||||
this.prisma.workflow.deleteMany(),
|
||||
|
||||
// Level 4: Delete campaigns and templates
|
||||
this.prisma.campaign.deleteMany(),
|
||||
this.prisma.template.deleteMany(),
|
||||
|
||||
// Level 5: Delete segment relationships
|
||||
this.prisma.segmentMembership.deleteMany(),
|
||||
this.prisma.segment.deleteMany(),
|
||||
|
||||
// Level 6: Delete contacts
|
||||
this.prisma.contact.deleteMany(),
|
||||
|
||||
// Level 7: Delete domains
|
||||
this.prisma.domain.deleteMany(),
|
||||
|
||||
// Level 8: Delete memberships (has FK to both user and project)
|
||||
this.prisma.membership.deleteMany(),
|
||||
|
||||
// Level 9: Delete projects
|
||||
this.prisma.project.deleteMany(),
|
||||
|
||||
// Level 10: Delete users last
|
||||
this.prisma.user.deleteMany(),
|
||||
]);
|
||||
} catch (error) {
|
||||
console.error('Error cleaning up database:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect from database
|
||||
*/
|
||||
async disconnect() {
|
||||
if (this.prisma) {
|
||||
await this.prisma.$disconnect();
|
||||
this.prisma = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute raw SQL (useful for advanced test setup)
|
||||
*/
|
||||
async executeRaw(sql: string) {
|
||||
if (!this.prisma) {
|
||||
throw new Error('Database not initialized');
|
||||
}
|
||||
return this.prisma.$executeRawUnsafe(sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset database sequences (useful for predictable IDs in tests)
|
||||
*/
|
||||
async resetSequences() {
|
||||
if (!this.prisma) return;
|
||||
|
||||
// Get all tables with sequences
|
||||
const tables = [
|
||||
'User',
|
||||
'Project',
|
||||
'Contact',
|
||||
'Campaign',
|
||||
'Email',
|
||||
'Workflow',
|
||||
'WorkflowExecution',
|
||||
'Template',
|
||||
'Segment',
|
||||
];
|
||||
|
||||
for (const table of tables) {
|
||||
try {
|
||||
await this.prisma.$executeRawUnsafe(`ALTER SEQUENCE "${table}_id_seq" RESTART WITH 1`);
|
||||
} catch (error) {
|
||||
// Sequence might not exist, ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const testDatabase = new TestDatabase();
|
||||
|
||||
// Export helper to get Prisma client in tests
|
||||
// Returns a Proxy that lazily initializes the database on first property access
|
||||
export const getPrismaClient = (() => {
|
||||
let clientProxy: PrismaClient | null = null;
|
||||
|
||||
return () => {
|
||||
if (!clientProxy) {
|
||||
clientProxy = new Proxy({} as PrismaClient, {
|
||||
get(target, prop) {
|
||||
const client = testDatabase.getClient();
|
||||
const value = client[prop as keyof PrismaClient];
|
||||
// Bind methods to the actual client
|
||||
return typeof value === 'function' ? value.bind(client) : value;
|
||||
},
|
||||
});
|
||||
}
|
||||
return clientProxy;
|
||||
};
|
||||
})();
|
||||
Reference in New Issue
Block a user