diff --git a/README.md b/README.md index 5a83916..016b758 100644 --- a/README.md +++ b/README.md @@ -53,8 +53,8 @@ This project is a full-stack web application featuring: - 🗄️ **Database CRUD Operations** - Full Create, Read, Update, Delete functionality - 🛠️ **Admin Panel** - Manage tables, columns, and data through a beautiful UI - 📊 **Table Manager** - Create and drop tables with visual column definition -- 🔧 **Column Manager** - Add, modify, and drop columns from existing tables -- 🔒 **Constraint Manager** - Add and manage UNIQUE and CHECK constraints (fully implemented) +- 🔧 **Column Manager** - Add, modify, and drop columns with DEFAULT values and NOT NULL support +- 🔒 **Constraint Manager** - Add and manage UNIQUE, CHECK, and PRIMARY KEY constraints (fully implemented) - 📊 **SQL Query Interface** - Execute custom queries with safety validation - 🔒 **JWT Authentication** with secure session management - 📦 **DrizzleORM** - Support for PostgreSQL, MySQL, and SQLite @@ -73,8 +73,8 @@ This is a **PostgreSQL database administration panel** that provides: - 🔒 **Secure authentication** with bcrypt password hashing and JWT sessions - 📊 **Database viewing** - Browse tables, view data, and explore schema - 🛠️ **Table management** - Create and drop tables through intuitive UI -- 🔧 **Column management** - Add, modify, and drop columns with type selection -- 🔐 **Constraint management** - Add UNIQUE and CHECK constraints for data validation +- 🔧 **Column management** - Add, modify, and drop columns with DEFAULT values and NOT NULL support +- 🔐 **Constraint management** - Add UNIQUE, CHECK, and PRIMARY KEY constraints for data validation - 🔍 **SQL query interface** - Execute SELECT queries safely with result display - 🐳 **All-in-one Docker image** - PostgreSQL 15 and admin UI in one container - ⚡ **Production-ready** - Deploy to Caprover, Docker, or any cloud platform diff --git a/ROADMAP.md b/ROADMAP.md index 860fa63..eb3ab19 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -65,9 +65,9 @@ See `src/config/features.json` for the complete feature configuration. - [x] ✅ Add constraint listing endpoint - [x] ✅ Add constraint creation/deletion endpoints - [x] ✅ Build constraints management UI - - [ ] Add PRIMARY KEY constraint support - - [ ] Add DEFAULT value management - - [ ] Add NOT NULL constraint management + - [x] Add PRIMARY KEY constraint support ✅ **COMPLETED** + - [x] Add DEFAULT value management ✅ **COMPLETED** + - [x] Add NOT NULL constraint management ✅ **COMPLETED** - [ ] Build query builder interface - [ ] Add foreign key relationship management - [ ] Implement index management UI diff --git a/TESTING.md b/TESTING.md index 03b4537..76fb3f6 100644 --- a/TESTING.md +++ b/TESTING.md @@ -35,11 +35,16 @@ Tests for the Column Management API endpoints (`/api/admin/column-manage`): - ✅ Validates all required fields (tableName, columnName, dataType) - ✅ Rejects invalid table names - ✅ Rejects invalid column names +- ✅ Accepts columns with NOT NULL constraint +- ✅ Accepts columns with DEFAULT values +- ✅ Accepts columns with both DEFAULT and NOT NULL **Modify Column Tests:** - ✅ Requires authentication - ✅ Validates required fields - ✅ Rejects invalid identifiers +- ✅ Accepts setting NOT NULL constraint +- ✅ Accepts dropping NOT NULL constraint **Drop Column Tests:** - ✅ Requires authentication @@ -148,10 +153,10 @@ All tests verify that: |---------|-----------|----------|----------------|------------|-------------| | Feature Config | - | - | - | 40 | 40 | | Table Manager | 7 | 2 (2 skipped) | 3 | - | 12 | -| Column Manager | 9 | 2 (2 skipped) | 3 | - | 14 | -| Constraint Manager | 14 | 3 (3 skipped) | 4 | 4 | 25 | +| Column Manager | 12 | 2 (2 skipped) | 3 | - | 17 | +| Constraint Manager | 15 | 3 (3 skipped) | 4 | 5 | 27 | | Admin Dashboard | - | 3 | 3 | - | 6 | -| **Total** | **30** | **10** | **16** | **44** | **100** | +| **Total** | **34** | **10** | **16** | **45** | **105** | ## Feature: Constraint Management Tests @@ -169,6 +174,7 @@ Tests for the Constraint Management API endpoints (`/api/admin/constraints`): - ✅ Rejects add without authentication - ✅ Rejects add without required fields - ✅ Rejects add with invalid table name +- ✅ Rejects PRIMARY KEY constraint without column name - ✅ Rejects UNIQUE constraint without column name - ✅ Rejects CHECK constraint without expression - ✅ Rejects CHECK constraint with dangerous expression (SQL injection prevention) @@ -184,7 +190,7 @@ Tests for the Constraint Management API endpoints (`/api/admin/constraints`): - SQL injection prevention - Authentication/authorization - Error handling for all CRUD operations -- Support for UNIQUE and CHECK constraints +- Support for PRIMARY KEY, UNIQUE and CHECK constraints ### End-to-End Tests (Playwright UI Tests) @@ -213,6 +219,7 @@ Tests for the constraint types configuration: **Constraint Types Tests:** - ✅ Returns array of constraint types - ✅ Validates constraint type properties +- ✅ Includes PRIMARY KEY constraint type with correct flags - ✅ Includes UNIQUE constraint type with correct flags - ✅ Includes CHECK constraint type with correct flags diff --git a/src/app/api/admin/constraints/route.ts b/src/app/api/admin/constraints/route.ts index 6a6d654..4443206 100644 --- a/src/app/api/admin/constraints/route.ts +++ b/src/app/api/admin/constraints/route.ts @@ -69,7 +69,7 @@ export async function GET(request: Request) { ON tc.constraint_name = cc.constraint_name WHERE tc.table_schema = 'public' AND tc.table_name = ${tableName} - AND tc.constraint_type IN ('UNIQUE', 'CHECK') + AND tc.constraint_type IN ('PRIMARY KEY', 'UNIQUE', 'CHECK') ORDER BY tc.constraint_name `); @@ -133,7 +133,15 @@ export async function POST(request: Request) { let alterQuery = ''; - if (constraintType === 'UNIQUE') { + if (constraintType === 'PRIMARY KEY') { + if (!columnName) { + return NextResponse.json( + { error: 'Column name is required for PRIMARY KEY constraint' }, + { status: 400 }, + ); + } + alterQuery = `ALTER TABLE "${tableName}" ADD CONSTRAINT "${constraintName}" PRIMARY KEY ("${columnName}")`; + } else if (constraintType === 'UNIQUE') { if (!columnName) { return NextResponse.json( { error: 'Column name is required for UNIQUE constraint' }, @@ -170,7 +178,7 @@ export async function POST(request: Request) { alterQuery = `ALTER TABLE "${tableName}" ADD CONSTRAINT "${constraintName}" CHECK (${checkExpression})`; } else { return NextResponse.json( - { error: 'Unsupported constraint type. Supported types: UNIQUE, CHECK' }, + { error: 'Unsupported constraint type. Supported types: PRIMARY KEY, UNIQUE, CHECK' }, { status: 400 }, ); } diff --git a/src/config/features.json b/src/config/features.json index 4ba7643..732b428 100644 --- a/src/config/features.json +++ b/src/config/features.json @@ -107,6 +107,12 @@ } ], "constraintTypes": [ + { + "name": "PRIMARY KEY", + "description": "Unique identifier for table rows", + "requiresColumn": true, + "requiresExpression": false + }, { "name": "UNIQUE", "description": "Ensure column values are unique", diff --git a/src/utils/featureConfig.test.ts b/src/utils/featureConfig.test.ts index cb06634..afe0cf5 100644 --- a/src/utils/featureConfig.test.ts +++ b/src/utils/featureConfig.test.ts @@ -280,6 +280,15 @@ describe('FeatureConfig', () => { }); }); + it('should include PRIMARY KEY constraint type', () => { + const constraintTypes = getConstraintTypes(); + const primaryKeyConstraint = constraintTypes.find(ct => ct.name === 'PRIMARY KEY'); + + expect(primaryKeyConstraint).toBeDefined(); + expect(primaryKeyConstraint?.requiresColumn).toBe(true); + expect(primaryKeyConstraint?.requiresExpression).toBe(false); + }); + it('should include UNIQUE constraint type', () => { const constraintTypes = getConstraintTypes(); const uniqueConstraint = constraintTypes.find(ct => ct.name === 'UNIQUE'); diff --git a/tests/integration/ColumnManager.spec.ts b/tests/integration/ColumnManager.spec.ts index 4df9f2d..c9008a2 100644 --- a/tests/integration/ColumnManager.spec.ts +++ b/tests/integration/ColumnManager.spec.ts @@ -48,6 +48,46 @@ test.describe('Column Manager', () => { expect([400, 401]).toContain(response.status()); }); + + test('should accept add column with NOT NULL constraint', async ({ page }) => { + const response = await page.request.post('/api/admin/column-manage', { + data: { + tableName: 'test_table', + columnName: 'test_column', + dataType: 'INTEGER', + nullable: false, + }, + }); + + expect([400, 401, 404, 500]).toContain(response.status()); + }); + + test('should accept add column with DEFAULT value', async ({ page }) => { + const response = await page.request.post('/api/admin/column-manage', { + data: { + tableName: 'test_table', + columnName: 'test_column', + dataType: 'INTEGER', + defaultValue: 0, + }, + }); + + expect([400, 401, 404, 500]).toContain(response.status()); + }); + + test('should accept add column with DEFAULT value and NOT NULL', async ({ page }) => { + const response = await page.request.post('/api/admin/column-manage', { + data: { + tableName: 'test_table', + columnName: 'test_column', + dataType: 'VARCHAR', + nullable: false, + defaultValue: 'default_value', + }, + }); + + expect([400, 401, 404, 500]).toContain(response.status()); + }); }); test.describe('Modify Column API', () => { @@ -84,6 +124,30 @@ test.describe('Column Manager', () => { expect([400, 401]).toContain(response.status()); }); + + test('should accept modify column to set NOT NULL', async ({ page }) => { + const response = await page.request.put('/api/admin/column-manage', { + data: { + tableName: 'test_table', + columnName: 'test_column', + nullable: false, + }, + }); + + expect([400, 401, 404, 500]).toContain(response.status()); + }); + + test('should accept modify column to drop NOT NULL', async ({ page }) => { + const response = await page.request.put('/api/admin/column-manage', { + data: { + tableName: 'test_table', + columnName: 'test_column', + nullable: true, + }, + }); + + expect([400, 401, 404, 500]).toContain(response.status()); + }); }); test.describe('Drop Column API', () => { diff --git a/tests/integration/ConstraintManager.spec.ts b/tests/integration/ConstraintManager.spec.ts index cfd6dcf..a8f15ad 100644 --- a/tests/integration/ConstraintManager.spec.ts +++ b/tests/integration/ConstraintManager.spec.ts @@ -58,6 +58,18 @@ test.describe('Constraint Manager', () => { expect([400, 401]).toContain(response.status()); }); + test('should reject PRIMARY KEY constraint without column name', async ({ page }) => { + const response = await page.request.post('/api/admin/constraints', { + data: { + tableName: 'test_table', + constraintName: 'test_pk', + constraintType: 'PRIMARY KEY', + }, + }); + + expect([400, 401]).toContain(response.status()); + }); + test('should reject UNIQUE constraint without column name', async ({ page }) => { const response = await page.request.post('/api/admin/constraints', { data: {