Skip to content
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
13 changes: 5 additions & 8 deletions .kiro/database-cleanup-prompt.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,14 @@ The project uses SQLite with a hybrid approach: base schema files + migration sy

```
backend/src/database/
├── schema.sql # Base schema: executions table
├── rbac-schema.sql # Base schema: RBAC tables (DUPLICATE of 001)
├── audit-schema.sql # ⚠️ ORPHANED - never loaded, duplicates 004
├── migrations.sql # ⚠️ ORPHANED - never loaded, superseded
├── DatabaseService.ts # Loads schema.sql + rbac-schema.sql, then runs migrations
├── DatabaseService.ts # Runs only MigrationRunner (migration-first approach)
├── MigrationRunner.ts # Runs numbered migrations from migrations/
└── migrations/
├── 001_initial_rbac.sql # Creates RBAC tables (duplicates rbac-schema.sql)
├── 000_initial_schema.sql # Initial schema: executions + revoked_tokens tables
├── 001_initial_rbac.sql # Creates RBAC tables
Comment on lines 14 to +19
├── 002_seed_rbac_data.sql # Seeds roles, permissions, config
├── 003_failed_login_attempts.sql # Adds security tables
├── 004_audit_logging.sql # Adds audit_logs (duplicates audit-schema.sql)
├── 004_audit_logging.sql # Adds audit_logs table
├── 005_add_ssh_execution_tool.sql # Updates executions table
└── 006_add_batch_executions.sql # Adds batch support
```
Expand Down Expand Up @@ -146,4 +143,4 @@ After any changes:
- ✅ No orphaned files in the database directory
- ✅ Clear documentation of the schema management approach
- ✅ All tests pass
- ✅ Docker deployment works with cl
- ✅ Docker deployment works correctly
6 changes: 3 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -104,9 +104,9 @@ COPY --from=backend-builder --chown=pabawi:pabawi /app/backend/dist ./dist
COPY --from=backend-deps --chown=pabawi:pabawi /app/backend/node_modules ./node_modules
COPY --from=backend-builder --chown=pabawi:pabawi /app/backend/package*.json ./

# Copy database directory with all SQL files and migrations (not copied by TypeScript compiler)
# This ensures schema files, migrations, and any future database-related files are included
COPY --from=backend-builder --chown=pabawi:pabawi /app/backend/src/database/ ./dist/database/
# Copy only database migrations (not copied by TypeScript compiler)
# This avoids copying TypeScript sources into the runtime image
COPY --from=backend-builder --chown=pabawi:pabawi /app/backend/src/database/migrations ./dist/database/migrations


# Copy built frontend to public directory
Expand Down
5 changes: 2 additions & 3 deletions backend/test/database/006_add_batch_executions.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import sqlite3 from "sqlite3";
import { MigrationRunner } from "../../src/database/MigrationRunner";
import { readFileSync } from "fs";
import { join } from "path";

Expand All @@ -10,9 +9,9 @@ describe("Migration 006: Add batch executions", () => {
beforeEach(async () => {
db = new sqlite3.Database(":memory:");

// Apply base schema first (executions table)
// Apply migration 000 (initial schema with executions table)
const baseSchema = readFileSync(
join(__dirname, "../../src/database/schema.sql"),
join(__dirname, "../../src/database/migrations/000_initial_schema.sql"),
"utf-8"
);
await new Promise<void>((resolve, reject) => {
Expand Down
20 changes: 7 additions & 13 deletions backend/test/database/index-verification.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,6 @@ describe('Database Index Verification', () => {
'idx_role_permissions_role',
'idx_role_permissions_perm',

// Composite indexes for optimized permission checks
'idx_user_roles_composite',
'idx_user_groups_composite',
'idx_group_roles_composite',
'idx_role_permissions_composite',

// Permission lookups
'idx_permissions_resource_action',

Expand All @@ -78,27 +72,27 @@ describe('Database Index Verification', () => {
console.log(`✓ Verified ${requiredIndexes.length} indexes are created`);
});

it('should have composite indexes on junction tables', async () => {
const compositeIndexes = await new Promise<any[]>((resolve, reject) => {
it('should have junction table indexes for permission checks', async () => {
const junctionIndexes = await new Promise<any[]>((resolve, reject) => {
db.all(
"SELECT name, tbl_name, sql FROM sqlite_master WHERE type='index' AND name LIKE '%composite%'",
"SELECT name, tbl_name FROM sqlite_master WHERE type='index' AND (tbl_name='user_roles' OR tbl_name='user_groups' OR tbl_name='group_roles' OR tbl_name='role_permissions')",
(err, rows) => {
if (err) reject(err);
else resolve(rows);
}
);
});

expect(compositeIndexes.length).toBeGreaterThanOrEqual(4);
expect(junctionIndexes.length).toBeGreaterThanOrEqual(8);

const indexInfo = compositeIndexes.map((idx) => ({
const indexInfo = junctionIndexes.map((idx) => ({
name: idx.name,
table: idx.tbl_name,
}));

console.log('Composite indexes:', indexInfo);
console.log('Junction table indexes:', indexInfo);

// Verify composite indexes are on the correct tables
// Verify indexes are on the correct tables
expect(indexInfo.some((idx) => idx.table === 'user_roles')).toBe(true);
expect(indexInfo.some((idx) => idx.table === 'user_groups')).toBe(true);
expect(indexInfo.some((idx) => idx.table === 'group_roles')).toBe(true);
Expand Down
21 changes: 11 additions & 10 deletions backend/test/database/migration-integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,15 @@ describe('Migration Integration Test', () => {
it('should apply all migrations on initialization', async () => {
const status = await dbService.getMigrationStatus();

// Should have applied all migrations
expect(status.applied).toHaveLength(6);
expect(status.applied[0].id).toBe('001');
expect(status.applied[1].id).toBe('002');
expect(status.applied[2].id).toBe('003');
expect(status.applied[3].id).toBe('004');
expect(status.applied[4].id).toBe('005');
expect(status.applied[5].id).toBe('006');
// Should have applied all migrations (000 through 006 = 7 total)
expect(status.applied).toHaveLength(7);
expect(status.applied[0].id).toBe('000');
expect(status.applied[1].id).toBe('001');
expect(status.applied[2].id).toBe('002');
expect(status.applied[3].id).toBe('003');
expect(status.applied[4].id).toBe('004');
expect(status.applied[5].id).toBe('005');
expect(status.applied[6].id).toBe('006');
expect(status.pending).toHaveLength(0);
});

Expand Down Expand Up @@ -90,8 +91,8 @@ describe('Migration Integration Test', () => {

const status = await dbService2.getMigrationStatus();

// Should still have 6 applied, 0 pending
expect(status.applied).toHaveLength(6);
// Should still have 7 applied, 0 pending
expect(status.applied).toHaveLength(7);
expect(status.pending).toHaveLength(0);

await dbService2.close();
Expand Down
4 changes: 2 additions & 2 deletions backend/test/database/performance-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ function runAsync(db: sqlite3.Database, sql: string): Promise<void> {
async function setupDatabase(): Promise<sqlite3.Database> {
const db = new sqlite3.Database(':memory:');

// Load and execute schema
const schemaPath = join(__dirname, '../../src/database/schema.sql');
// Load and execute migration 000 (initial schema)
const schemaPath = join(__dirname, '../../src/database/migrations/000_initial_schema.sql');
const schema = readFileSync(schemaPath, 'utf-8');

// Split by semicolon and execute each statement
Expand Down
21 changes: 5 additions & 16 deletions backend/test/performance/database-performance.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import sqlite3 from 'sqlite3';
import { ExecutionRepository, type ExecutionRecord } from '../../src/database/ExecutionRepository';
import { readFileSync } from 'fs';
import { MigrationRunner } from '../../src/database/MigrationRunner';
import { join } from 'path';

// Performance thresholds (in milliseconds)
Expand Down Expand Up @@ -46,21 +46,10 @@ function allAsync(db: sqlite3.Database, sql: string, params?: any[]): Promise<an
async function setupDatabase(): Promise<sqlite3.Database> {
const db = new sqlite3.Database(':memory:');

// Load and execute schema
const schemaPath = join(__dirname, '../../src/database/schema.sql');
const schema = readFileSync(schemaPath, 'utf-8');

// Execute full schema using db.exec (handles comments and multiple statements)
await new Promise<void>((resolve, reject) => {
db.exec(schema, (err) => {
if (err) reject(err);
else resolve();
});
});

// Add columns from migration 006 (batch execution support)
await runAsync(db, 'ALTER TABLE executions ADD COLUMN batch_id TEXT');
await runAsync(db, 'ALTER TABLE executions ADD COLUMN batch_position INTEGER');
// Apply all migrations using MigrationRunner (migration-first approach)
const migrationsDir = join(__dirname, '../../src/database/migrations');
const runner = new MigrationRunner(db, migrationsDir);
await runner.runPendingMigrations();

return db;
}
Expand Down
Loading