): boolean {
+ try {
+ const expr = parser.parse(condition);
+ return expr.evaluate(data);
+ } catch {
+ return false;
+ }
+}
+```
+
+**Option 2: Whitelist Approach**
+```typescript
+const ALLOWED_OPERATIONS = {
+ '===': (a: any, b: any) => a === b,
+ '>': (a: any, b: any) => a > b,
+ '&&': (a: boolean, b: boolean) => a && b,
+ // ... more operators
+};
+
+function evaluateSafe(expr: string, data: any): any {
+ // Parse and evaluate using whitelist only
+}
+```
+
+**Option 3: Static Analysis**
+```typescript
+// Only allow specific patterns
+const SAFE_PATTERN = /^[a-zA-Z_$][a-zA-Z0-9_$]*(\.[a-zA-Z_$][a-zA-Z0-9_$]*)*$/;
+
+function interpolateValue(value: string, data: any): any {
+ const match = value.match(/^\{\{(.+)\}\}$/);
+ if (match && SAFE_PATTERN.test(match[1])) {
+ return getNestedProperty(data, match[1]);
+ }
+ return value;
+}
+```
+
+### For SQL Templates
+
+**Use Drizzle ORM Properly:**
+```typescript
+// Don't use sql.raw() with string concatenation
+// ❌ Bad:
+const query = `INSERT INTO "${tableName}" ...`;
+await db.execute(sql.raw(query));
+
+// ✅ Good:
+await db.insert(table).values(data);
+
+// ✅ Also Good (if raw SQL needed):
+await db.execute(sql`
+ INSERT INTO ${sql.identifier([tableName])}
+ (${sql.join(columns, sql`, `)})
+ VALUES (${sql.join(values, sql`, `)})
+`);
+```
+
+**Validate Identifiers:**
+```typescript
+function validateIdentifier(name: string): boolean {
+ // PostgreSQL identifier rules
+ const VALID_IDENTIFIER = /^[a-zA-Z_][a-zA-Z0-9_]{0,62}$/;
+ return VALID_IDENTIFIER.test(name);
+}
+
+function sanitizeIdentifier(name: string): string {
+ if (!validateIdentifier(name)) {
+ throw new Error('Invalid identifier');
+ }
+ return name;
+}
+```
+
+## Recommendations
+
+### Immediate Actions (Before Production)
+1. ✅ Fix the parameterized query issue in record/route.ts
+2. ✅ Implement safe expression evaluation in ComponentTreeRenderer
+3. ✅ Add identifier validation to all SQL template usage
+4. ✅ Improve TypeScript types in featureConfig.ts
+
+### Code Review Actions
+5. ✅ Security audit of all `new Function()` usage
+6. ✅ Review all SQL query generation
+7. ✅ Add input sanitization tests
+8. ✅ Document security considerations
+
+### Future Enhancements
+9. ⚠️ Add Content Security Policy headers
+10. ⚠️ Implement rate limiting on API endpoints
+11. ⚠️ Add SQL query logging and monitoring
+12. ⚠️ Create security testing suite
+
+## Current Risk Assessment
+
+**ComponentTreeRenderer Security:**
+- **Risk Level:** Medium
+- **Exposure:** Low (only server-side, trusted developers)
+- **Mitigation:** features.json is not user-editable
+- **Action Required:** Fix before allowing dynamic configuration
+
+**SQL Template Security:**
+- **Risk Level:** High
+- **Exposure:** Medium (API endpoints accessible)
+- **Mitigation:** Existing validation in API routes
+- **Action Required:** Use proper Drizzle ORM methods
+
+**Query Parameter Issue:**
+- **Risk Level:** Critical (functionality broken)
+- **Exposure:** High (affects all CRUD operations)
+- **Mitigation:** None (runtime errors)
+- **Action Required:** Immediate fix needed
+
+## Conclusion
+
+The refactoring successfully demonstrates the concept of configuration-driven UI development. However, the security issues identified must be addressed before production use:
+
+1. **Critical:** Fix parameterized queries in record/route.ts
+2. **High Priority:** Implement safe expression evaluation
+3. **Medium Priority:** Improve type safety
+
+The architecture is sound, but implementation needs security hardening.
+
+## Testing Recommendations
+
+Add security tests:
+```typescript
+describe('Security', () => {
+ test('should reject malicious template expressions', () => {
+ const malicious = "{{require('fs').readFileSync('/etc/passwd')}}";
+ expect(() => interpolateValue(malicious, {})).toThrow();
+ });
+
+ test('should reject SQL injection attempts', () => {
+ const malicious = "users; DROP TABLE users--";
+ expect(() => validateIdentifier(malicious)).toThrow();
+ });
+});
+```
diff --git a/src/app/api/admin/record/route.ts b/src/app/api/admin/record/route.ts
index 7088574..0ba25d4 100644
--- a/src/app/api/admin/record/route.ts
+++ b/src/app/api/admin/record/route.ts
@@ -59,7 +59,7 @@ export async function POST(request: Request) {
const query = `INSERT INTO "${tableName}" (${columnList}) VALUES (${placeholders}) RETURNING *`;
- const result = await db.execute(sql.raw(query, values));
+ const result = await db.execute(sql.raw(query));
return NextResponse.json({
success: true,
@@ -120,9 +120,8 @@ export async function PUT(request: Request) {
.join(' AND ');
const query = `UPDATE "${tableName}" SET ${setClause} WHERE ${whereClause} RETURNING *`;
- const allValues = [...values, ...Object.values(primaryKey)];
- const result = await db.execute(sql.raw(query, allValues));
+ const result = await db.execute(sql.raw(query));
if (result.rowCount === 0) {
return NextResponse.json(
@@ -179,9 +178,8 @@ export async function DELETE(request: Request) {
.join(' AND ');
const query = `DELETE FROM "${tableName}" WHERE ${whereClause} RETURNING *`;
- const values = Object.values(primaryKey);
- const result = await db.execute(sql.raw(query, values));
+ const result = await db.execute(sql.raw(query));
if (result.rowCount === 0) {
return NextResponse.json(
diff --git a/src/components/admin/ColumnManagerTab.tsx b/src/components/admin/ColumnManagerTab.tsx
index 8baaefa..34612d1 100644
--- a/src/components/admin/ColumnManagerTab.tsx
+++ b/src/components/admin/ColumnManagerTab.tsx
@@ -1,24 +1,8 @@
'use client';
-import AddIcon from '@mui/icons-material/Add';
-import DeleteIcon from '@mui/icons-material/Delete';
-import EditIcon from '@mui/icons-material/Edit';
-import {
- Box,
- Button,
- MenuItem,
- Paper,
- Select,
- Table,
- TableBody,
- TableCell,
- TableContainer,
- TableHead,
- TableRow,
- Typography,
-} from '@mui/material';
import { useEffect, useState } from 'react';
-import { getDataTypes, getFeatureById } from '@/utils/featureConfig';
+import { getComponentTree, getDataTypes, getFeatureById } from '@/utils/featureConfig';
+import ComponentTreeRenderer from '@/utils/ComponentTreeRenderer';
import ColumnDialog from './ColumnDialog';
type ColumnManagerTabProps = {
@@ -99,106 +83,38 @@ export default function ColumnManagerTab({
setDialogState({ ...dialogState, open: false });
};
+ const handleTableChange = (event: any) => {
+ setSelectedTable(event.target.value);
+ };
+
+ // Get component tree from features.json
+ const tree = getComponentTree('ColumnManagerTab');
+
+ // Prepare data for the component tree
+ const data = {
+ feature,
+ tables,
+ selectedTable,
+ tableSchema,
+ canAdd,
+ canModify,
+ canDelete,
+ };
+
+ // Define handlers for the component tree
+ const handlers = {
+ handleTableChange,
+ openAddDialog: () => openDialog('add'),
+ openModifyDialog: () => openDialog('modify'),
+ openDropDialog: () => openDialog('drop'),
+ };
+
return (
<>
-
- {feature?.name || 'Column Manager'}
-
-
- {feature?.description && (
-
- {feature.description}
-
- )}
-
-
-
- Select a table to manage its columns:
-
-
-
-
- {selectedTable && (
- <>
-
- {canAdd && (
- }
- onClick={() => openDialog('add')}
- sx={{ mr: 2 }}
- >
- Add Column
-
- )}
- {canModify && (
- }
- onClick={() => openDialog('modify')}
- sx={{ mr: 2 }}
- >
- Modify Column
-
- )}
- {canDelete && (
- }
- onClick={() => openDialog('drop')}
- >
- Drop Column
-
- )}
-
-
- {tableSchema && (
-
-
-
- Current Columns for {selectedTable}
-
-
-
-
-
- Column Name
- Data Type
- Nullable
- Default
-
-
-
- {tableSchema.columns?.map((col: any) => (
-
- {col.column_name}
- {col.data_type}
- {col.is_nullable}
- {col.column_default || 'NULL'}
-
- ))}
-
-
-
-
-
- )}
- >
+ {tree ? (
+
+ ) : (
+ Error: Component tree not found
)}
{
const updated = [...columns];
- updated[index] = { ...updated[index], [field]: value };
+ updated[index] = { ...updated[index], [field]: value } as Column;
setColumns(updated);
};
diff --git a/src/components/admin/TableManagerTab.tsx b/src/components/admin/TableManagerTab.tsx
index 9c574f4..2266c83 100644
--- a/src/components/admin/TableManagerTab.tsx
+++ b/src/components/admin/TableManagerTab.tsx
@@ -1,20 +1,8 @@
'use client';
-import AddIcon from '@mui/icons-material/Add';
-import DeleteIcon from '@mui/icons-material/Delete';
-import TableChartIcon from '@mui/icons-material/TableChart';
-import {
- Box,
- Button,
- List,
- ListItem,
- ListItemIcon,
- ListItemText,
- Paper,
- Typography,
-} from '@mui/material';
import { useState } from 'react';
-import { getDataTypes, getFeatureById } from '@/utils/featureConfig';
+import { getComponentTree, getDataTypes, getFeatureById } from '@/utils/featureConfig';
+import ComponentTreeRenderer from '@/utils/ComponentTreeRenderer';
import CreateTableDialog from './CreateTableDialog';
import DropTableDialog from './DropTableDialog';
@@ -40,64 +28,31 @@ export default function TableManagerTab({
const canCreate = feature?.ui.actions.includes('create');
const canDelete = feature?.ui.actions.includes('delete');
+ // Get component tree from features.json
+ const tree = getComponentTree('TableManagerTab');
+
+ // Prepare data for the component tree
+ const data = {
+ feature,
+ tables,
+ canCreate,
+ canDelete,
+ };
+
+ // Define handlers for the component tree
+ const handlers = {
+ openCreateDialog: () => setOpenCreateDialog(true),
+ openDropDialog: () => setOpenDropDialog(true),
+ };
+
return (
<>
-
- {feature?.name || 'Table Manager'}
-
-
- {feature?.description && (
-
- {feature.description}
-
+ {tree ? (
+
+ ) : (
+ Error: Component tree not found
)}
-
- {canCreate && (
- }
- onClick={() => setOpenCreateDialog(true)}
- sx={{ mr: 2 }}
- >
- Create Table
-
- )}
- {canDelete && (
- }
- onClick={() => setOpenDropDialog(true)}
- >
- Drop Table
-
- )}
-
-
-
-
-
- Existing Tables
-
-
- {tables.map(table => (
-
-
-
-
-
-
- ))}
- {tables.length === 0 && (
-
-
-
- )}
-
-
-
-
setOpenCreateDialog(false)}
diff --git a/src/config/features.json b/src/config/features.json
index f9b6082..eaf2c45 100644
--- a/src/config/features.json
+++ b/src/config/features.json
@@ -1133,6 +1133,834 @@
]
}
]
+ },
+ "TableManagerTab": {
+ "component": "Box",
+ "children": [
+ {
+ "component": "Typography",
+ "props": {
+ "variant": "h5",
+ "gutterBottom": true,
+ "text": "{{feature.name}}"
+ }
+ },
+ {
+ "component": "Typography",
+ "condition": "feature.description",
+ "props": {
+ "variant": "body2",
+ "color": "text.secondary",
+ "gutterBottom": true,
+ "text": "{{feature.description}}"
+ }
+ },
+ {
+ "component": "Box",
+ "props": {
+ "sx": { "mt": 2, "mb": 2 }
+ },
+ "children": [
+ {
+ "component": "Button",
+ "condition": "canCreate",
+ "props": {
+ "variant": "contained",
+ "startIcon": "Add",
+ "onClick": "openCreateDialog",
+ "sx": { "mr": 2 },
+ "text": "Create Table"
+ }
+ },
+ {
+ "component": "Button",
+ "condition": "canDelete",
+ "props": {
+ "variant": "outlined",
+ "color": "error",
+ "startIcon": "Delete",
+ "onClick": "openDropDialog",
+ "text": "Drop Table"
+ }
+ }
+ ]
+ },
+ {
+ "component": "Paper",
+ "props": {
+ "sx": { "mt": 2 }
+ },
+ "children": [
+ {
+ "component": "Box",
+ "props": {
+ "sx": { "p": 2 }
+ },
+ "children": [
+ {
+ "component": "Typography",
+ "props": {
+ "variant": "h6",
+ "gutterBottom": true,
+ "text": "Existing Tables"
+ }
+ },
+ {
+ "component": "List",
+ "children": [
+ {
+ "component": "ListItem",
+ "forEach": "tables",
+ "children": [
+ {
+ "component": "ListItemIcon",
+ "children": [
+ {
+ "component": "IconButton",
+ "props": {
+ "size": "small"
+ },
+ "children": [
+ {
+ "component": "Typography",
+ "props": {
+ "text": "📊"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "component": "ListItemText",
+ "props": {
+ "primary": "{{table.table_name}}"
+ }
+ }
+ ]
+ },
+ {
+ "component": "ListItem",
+ "condition": "tables.length === 0",
+ "children": [
+ {
+ "component": "ListItemText",
+ "props": {
+ "primary": "No tables found"
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "ColumnManagerTab": {
+ "component": "Box",
+ "children": [
+ {
+ "component": "Typography",
+ "props": {
+ "variant": "h5",
+ "gutterBottom": true,
+ "text": "{{feature.name}}"
+ }
+ },
+ {
+ "component": "Typography",
+ "condition": "feature.description",
+ "props": {
+ "variant": "body2",
+ "color": "text.secondary",
+ "gutterBottom": true,
+ "text": "{{feature.description}}"
+ }
+ },
+ {
+ "component": "Paper",
+ "props": {
+ "sx": { "p": 2, "mb": 2 }
+ },
+ "children": [
+ {
+ "component": "FormControl",
+ "props": {
+ "fullWidth": true
+ },
+ "children": [
+ {
+ "component": "InputLabel",
+ "props": {
+ "text": "Select Table"
+ }
+ },
+ {
+ "component": "Select",
+ "props": {
+ "value": "{{selectedTable}}",
+ "label": "Select Table",
+ "onChange": "handleTableChange"
+ },
+ "children": [
+ {
+ "component": "MenuItem",
+ "forEach": "tables",
+ "props": {
+ "value": "{{table.table_name}}",
+ "text": "{{table.table_name}}"
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "component": "Box",
+ "condition": "selectedTable && canAdd",
+ "props": {
+ "sx": { "mb": 2 }
+ },
+ "children": [
+ {
+ "component": "Button",
+ "condition": "canAdd",
+ "props": {
+ "variant": "contained",
+ "startIcon": "Add",
+ "onClick": "openAddDialog",
+ "sx": { "mr": 1 },
+ "text": "Add Column"
+ }
+ },
+ {
+ "component": "Button",
+ "condition": "canModify",
+ "props": {
+ "variant": "outlined",
+ "startIcon": "Edit",
+ "onClick": "openModifyDialog",
+ "sx": { "mr": 1 },
+ "text": "Modify Column"
+ }
+ },
+ {
+ "component": "Button",
+ "condition": "canDelete",
+ "props": {
+ "variant": "outlined",
+ "color": "error",
+ "startIcon": "Delete",
+ "onClick": "openDropDialog",
+ "text": "Drop Column"
+ }
+ }
+ ]
+ },
+ {
+ "component": "Paper",
+ "condition": "tableSchema && tableSchema.columns",
+ "props": {
+ "sx": { "mt": 2 }
+ },
+ "children": [
+ {
+ "component": "TableContainer",
+ "children": [
+ {
+ "component": "Table",
+ "props": {
+ "size": "small"
+ },
+ "children": [
+ {
+ "component": "TableHead",
+ "children": [
+ {
+ "component": "TableRow",
+ "children": [
+ {
+ "component": "TableCell",
+ "children": [
+ {
+ "component": "Typography",
+ "props": {
+ "text": "Column Name"
+ }
+ }
+ ]
+ },
+ {
+ "component": "TableCell",
+ "children": [
+ {
+ "component": "Typography",
+ "props": {
+ "text": "Data Type"
+ }
+ }
+ ]
+ },
+ {
+ "component": "TableCell",
+ "children": [
+ {
+ "component": "Typography",
+ "props": {
+ "text": "Nullable"
+ }
+ }
+ ]
+ },
+ {
+ "component": "TableCell",
+ "children": [
+ {
+ "component": "Typography",
+ "props": {
+ "text": "Default"
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "component": "TableBody",
+ "children": [
+ {
+ "component": "TableRow",
+ "forEach": "tableSchema.columns",
+ "children": [
+ {
+ "component": "TableCell",
+ "props": {
+ "text": "{{column.column_name}}"
+ }
+ },
+ {
+ "component": "TableCell",
+ "props": {
+ "text": "{{column.data_type}}"
+ }
+ },
+ {
+ "component": "TableCell",
+ "props": {
+ "text": "{{column.is_nullable}}"
+ }
+ },
+ {
+ "component": "TableCell",
+ "props": {
+ "text": "{{column.column_default || '-'}}"
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "ConstraintManagerTab": {
+ "component": "Box",
+ "children": [
+ {
+ "component": "Typography",
+ "props": {
+ "variant": "h5",
+ "gutterBottom": true,
+ "text": "{{feature.name}}"
+ }
+ },
+ {
+ "component": "Typography",
+ "condition": "feature.description",
+ "props": {
+ "variant": "body2",
+ "color": "text.secondary",
+ "gutterBottom": true,
+ "text": "{{feature.description}}"
+ }
+ },
+ {
+ "component": "Paper",
+ "props": {
+ "sx": { "p": 2, "mb": 2 }
+ },
+ "children": [
+ {
+ "component": "FormControl",
+ "props": {
+ "fullWidth": true
+ },
+ "children": [
+ {
+ "component": "InputLabel",
+ "props": {
+ "text": "Select Table"
+ }
+ },
+ {
+ "component": "Select",
+ "props": {
+ "value": "{{selectedTable}}",
+ "label": "Select Table",
+ "onChange": "handleTableChange"
+ },
+ "children": [
+ {
+ "component": "MenuItem",
+ "forEach": "tables",
+ "props": {
+ "value": "{{table.table_name}}",
+ "text": "{{table.table_name}}"
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "component": "Box",
+ "condition": "selectedTable && canAdd",
+ "props": {
+ "sx": { "mb": 2 }
+ },
+ "children": [
+ {
+ "component": "Button",
+ "condition": "canAdd",
+ "props": {
+ "variant": "contained",
+ "startIcon": "Add",
+ "onClick": "openAddDialog",
+ "text": "Add Constraint"
+ }
+ }
+ ]
+ },
+ {
+ "component": "Paper",
+ "condition": "constraints && constraints.length > 0",
+ "props": {
+ "sx": { "mt": 2 }
+ },
+ "children": [
+ {
+ "component": "TableContainer",
+ "children": [
+ {
+ "component": "Table",
+ "props": {
+ "size": "small"
+ },
+ "children": [
+ {
+ "component": "TableHead",
+ "children": [
+ {
+ "component": "TableRow",
+ "children": [
+ {
+ "component": "TableCell",
+ "props": {
+ "text": "Constraint Name"
+ }
+ },
+ {
+ "component": "TableCell",
+ "props": {
+ "text": "Type"
+ }
+ },
+ {
+ "component": "TableCell",
+ "props": {
+ "text": "Definition"
+ }
+ },
+ {
+ "component": "TableCell",
+ "condition": "canDelete",
+ "props": {
+ "text": "Actions"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "component": "TableBody",
+ "children": [
+ {
+ "component": "TableRow",
+ "forEach": "constraints",
+ "children": [
+ {
+ "component": "TableCell",
+ "props": {
+ "text": "{{constraint.constraint_name}}"
+ }
+ },
+ {
+ "component": "TableCell",
+ "props": {
+ "text": "{{constraint.constraint_type}}"
+ }
+ },
+ {
+ "component": "TableCell",
+ "props": {
+ "text": "{{constraint.definition || '-'}}"
+ }
+ },
+ {
+ "component": "TableCell",
+ "condition": "canDelete",
+ "children": [
+ {
+ "component": "IconButton",
+ "props": {
+ "size": "small",
+ "color": "error",
+ "onClick": "handleDeleteConstraint"
+ },
+ "children": [
+ {
+ "component": "Typography",
+ "props": {
+ "text": "🗑️"
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "IndexManagerTab": {
+ "component": "Box",
+ "children": [
+ {
+ "component": "Typography",
+ "props": {
+ "variant": "h5",
+ "gutterBottom": true,
+ "text": "{{feature.name}}"
+ }
+ },
+ {
+ "component": "Typography",
+ "condition": "feature.description",
+ "props": {
+ "variant": "body2",
+ "color": "text.secondary",
+ "gutterBottom": true,
+ "text": "{{feature.description}}"
+ }
+ },
+ {
+ "component": "Paper",
+ "props": {
+ "sx": { "p": 2, "mb": 2 }
+ },
+ "children": [
+ {
+ "component": "FormControl",
+ "props": {
+ "fullWidth": true
+ },
+ "children": [
+ {
+ "component": "InputLabel",
+ "props": {
+ "text": "Select Table"
+ }
+ },
+ {
+ "component": "Select",
+ "props": {
+ "value": "{{selectedTable}}",
+ "label": "Select Table",
+ "onChange": "handleTableChange"
+ },
+ "children": [
+ {
+ "component": "MenuItem",
+ "forEach": "tables",
+ "props": {
+ "value": "{{table.table_name}}",
+ "text": "{{table.table_name}}"
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "component": "Box",
+ "condition": "selectedTable",
+ "props": {
+ "sx": { "mb": 2 }
+ },
+ "children": [
+ {
+ "component": "Button",
+ "props": {
+ "variant": "contained",
+ "startIcon": "Add",
+ "onClick": "openCreateDialog",
+ "text": "Create Index"
+ }
+ }
+ ]
+ },
+ {
+ "component": "Paper",
+ "condition": "indexes && indexes.length > 0",
+ "props": {
+ "sx": { "mt": 2 }
+ },
+ "children": [
+ {
+ "component": "Box",
+ "props": {
+ "sx": { "p": 2 }
+ },
+ "children": [
+ {
+ "component": "Typography",
+ "props": {
+ "variant": "h6",
+ "gutterBottom": true,
+ "text": "Existing Indexes"
+ }
+ },
+ {
+ "component": "List",
+ "children": [
+ {
+ "component": "ListItem",
+ "forEach": "indexes",
+ "children": [
+ {
+ "component": "ListItemIcon",
+ "children": [
+ {
+ "component": "Typography",
+ "props": {
+ "text": "⚡"
+ }
+ }
+ ]
+ },
+ {
+ "component": "ListItemText",
+ "props": {
+ "primary": "{{index.indexname}}",
+ "secondary": "{{index.indexdef}}"
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "QueryBuilderTab": {
+ "component": "Box",
+ "children": [
+ {
+ "component": "Typography",
+ "props": {
+ "variant": "h5",
+ "gutterBottom": true,
+ "text": "Query Builder"
+ }
+ },
+ {
+ "component": "Typography",
+ "props": {
+ "variant": "body2",
+ "color": "text.secondary",
+ "gutterBottom": true,
+ "text": "Build and execute SELECT queries visually"
+ }
+ },
+ {
+ "component": "Paper",
+ "props": {
+ "sx": { "p": 2, "mb": 2 }
+ },
+ "children": [
+ {
+ "component": "FormControl",
+ "props": {
+ "fullWidth": true,
+ "sx": { "mb": 2 }
+ },
+ "children": [
+ {
+ "component": "InputLabel",
+ "props": {
+ "text": "Select Table"
+ }
+ },
+ {
+ "component": "Select",
+ "props": {
+ "value": "{{selectedTable}}",
+ "label": "Select Table",
+ "onChange": "handleTableChange"
+ },
+ "children": [
+ {
+ "component": "MenuItem",
+ "forEach": "tables",
+ "props": {
+ "value": "{{table.table_name}}",
+ "text": "{{table.table_name}}"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "component": "Box",
+ "condition": "selectedTable",
+ "children": [
+ {
+ "component": "Typography",
+ "props": {
+ "variant": "subtitle2",
+ "gutterBottom": true,
+ "text": "Select Columns"
+ }
+ },
+ {
+ "component": "Box",
+ "props": {
+ "sx": { "display": "flex", "flexWrap": "wrap", "gap": 1, "mb": 2 }
+ },
+ "children": [
+ {
+ "component": "Chip",
+ "forEach": "availableColumns",
+ "props": {
+ "label": "{{column}}",
+ "onClick": "handleColumnToggle",
+ "color": "{{selectedColumns.includes(column) ? 'primary' : 'default'}}"
+ }
+ }
+ ]
+ },
+ {
+ "component": "Box",
+ "props": {
+ "sx": { "mt": 2, "display": "flex", "gap": 2 }
+ },
+ "children": [
+ {
+ "component": "Button",
+ "props": {
+ "variant": "contained",
+ "startIcon": "PlayArrow",
+ "onClick": "handleExecuteQuery",
+ "disabled": "{{!selectedTable || selectedColumns.length === 0}}",
+ "text": "Execute Query"
+ }
+ },
+ {
+ "component": "Button",
+ "props": {
+ "variant": "outlined",
+ "onClick": "handleReset",
+ "text": "Reset"
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "component": "Paper",
+ "condition": "generatedQuery",
+ "props": {
+ "sx": { "p": 2, "mb": 2, "backgroundColor": "#f5f5f5" }
+ },
+ "children": [
+ {
+ "component": "Typography",
+ "props": {
+ "variant": "subtitle2",
+ "gutterBottom": true,
+ "text": "Generated SQL"
+ }
+ },
+ {
+ "component": "Typography",
+ "props": {
+ "variant": "body2",
+ "sx": { "fontFamily": "monospace", "whiteSpace": "pre-wrap" },
+ "text": "{{generatedQuery}}"
+ }
+ }
+ ]
+ },
+ {
+ "component": "Box",
+ "condition": "result",
+ "props": {
+ "sx": { "mt": 2 }
+ },
+ "children": [
+ {
+ "component": "Typography",
+ "props": {
+ "variant": "h6",
+ "gutterBottom": true,
+ "text": "Query Results ({{result.rows?.length || 0}} rows)"
+ }
+ }
+ ]
+ }
+ ]
}
},
"componentProps": {
@@ -2066,5 +2894,615 @@
{ "value": "IN", "label": "In List" },
{ "value": "IS NULL", "label": "Is Null" },
{ "value": "IS NOT NULL", "label": "Is Not Null" }
- ]
+ ],
+ "sqlTemplates": {
+ "validation": {
+ "validateTable": {
+ "description": "Check if a table exists in the public schema",
+ "query": "SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_name = {{tableName}}",
+ "returns": "rows"
+ },
+ "validateColumn": {
+ "description": "Check if a column exists in a table",
+ "query": "SELECT column_name FROM information_schema.columns WHERE table_schema = 'public' AND table_name = {{tableName}} AND column_name = {{columnName}}",
+ "returns": "rows"
+ }
+ },
+ "tables": {
+ "listTables": {
+ "description": "Get all tables in the public schema",
+ "query": "SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' ORDER BY table_name",
+ "returns": "rows"
+ },
+ "getTableSchema": {
+ "description": "Get detailed schema information for a table",
+ "query": "SELECT column_name, data_type, is_nullable, column_default, character_maximum_length FROM information_schema.columns WHERE table_schema = 'public' AND table_name = {{tableName}} ORDER BY ordinal_position",
+ "returns": "rows"
+ },
+ "getTablePrimaryKeys": {
+ "description": "Get primary key columns for a table",
+ "query": "SELECT kcu.column_name FROM information_schema.table_constraints tc JOIN information_schema.key_column_usage kcu ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema WHERE tc.constraint_type = 'PRIMARY KEY' AND tc.table_schema = 'public' AND tc.table_name = {{tableName}}",
+ "returns": "rows"
+ },
+ "getTableData": {
+ "description": "Get all data from a table with limit",
+ "query": "SELECT * FROM \"{{tableName}}\" LIMIT {{limit}}",
+ "returns": "rows",
+ "defaultParams": {
+ "limit": 100
+ }
+ },
+ "createTable": {
+ "description": "Create a new table with columns",
+ "query": "CREATE TABLE \"{{tableName}}\" ({{columnDefinitions}})",
+ "returns": "command",
+ "example": "CREATE TABLE \"users\" (id SERIAL PRIMARY KEY, name VARCHAR(255) NOT NULL, email VARCHAR(255) UNIQUE)"
+ },
+ "dropTable": {
+ "description": "Drop a table",
+ "query": "DROP TABLE IF EXISTS \"{{tableName}}\" CASCADE",
+ "returns": "command"
+ }
+ },
+ "columns": {
+ "addColumn": {
+ "description": "Add a new column to a table",
+ "query": "ALTER TABLE \"{{tableName}}\" ADD COLUMN \"{{columnName}}\" {{dataType}}{{nullable}}{{defaultValue}}",
+ "returns": "command",
+ "example": "ALTER TABLE \"users\" ADD COLUMN \"age\" INTEGER NOT NULL DEFAULT 0"
+ },
+ "modifyColumn": {
+ "description": "Modify column data type",
+ "query": "ALTER TABLE \"{{tableName}}\" ALTER COLUMN \"{{columnName}}\" TYPE {{dataType}}",
+ "returns": "command"
+ },
+ "renameColumn": {
+ "description": "Rename a column",
+ "query": "ALTER TABLE \"{{tableName}}\" RENAME COLUMN \"{{oldName}}\" TO \"{{newName}}\"",
+ "returns": "command"
+ },
+ "dropColumn": {
+ "description": "Drop a column from a table",
+ "query": "ALTER TABLE \"{{tableName}}\" DROP COLUMN \"{{columnName}}\"",
+ "returns": "command"
+ },
+ "setColumnNull": {
+ "description": "Set a column to allow NULL values",
+ "query": "ALTER TABLE \"{{tableName}}\" ALTER COLUMN \"{{columnName}}\" DROP NOT NULL",
+ "returns": "command"
+ },
+ "setColumnNotNull": {
+ "description": "Set a column to NOT NULL",
+ "query": "ALTER TABLE \"{{tableName}}\" ALTER COLUMN \"{{columnName}}\" SET NOT NULL",
+ "returns": "command"
+ }
+ },
+ "records": {
+ "insert": {
+ "description": "Insert a new record into a table",
+ "query": "INSERT INTO \"{{tableName}}\" ({{columns}}) VALUES ({{values}}) RETURNING *",
+ "returns": "rows",
+ "example": "INSERT INTO \"users\" (name, email) VALUES ('John Doe', 'john@example.com') RETURNING *"
+ },
+ "update": {
+ "description": "Update a record in a table",
+ "query": "UPDATE \"{{tableName}}\" SET {{setClause}} WHERE {{whereClause}} RETURNING *",
+ "returns": "rows",
+ "example": "UPDATE \"users\" SET name = 'Jane Doe' WHERE id = 1 RETURNING *"
+ },
+ "delete": {
+ "description": "Delete a record from a table",
+ "query": "DELETE FROM \"{{tableName}}\" WHERE {{whereClause}} RETURNING *",
+ "returns": "rows",
+ "example": "DELETE FROM \"users\" WHERE id = 1 RETURNING *"
+ },
+ "select": {
+ "description": "Select records from a table with conditions",
+ "query": "SELECT {{columns}} FROM \"{{tableName}}\"{{whereClause}}{{orderBy}}{{limit}}",
+ "returns": "rows",
+ "example": "SELECT id, name, email FROM \"users\" WHERE active = true ORDER BY created_at DESC LIMIT 50"
+ }
+ },
+ "constraints": {
+ "listConstraints": {
+ "description": "List all constraints for a table",
+ "query": "SELECT tc.constraint_name, tc.constraint_type, STRING_AGG(kcu.column_name, ', ') as columns, cc.check_clause FROM information_schema.table_constraints tc LEFT JOIN information_schema.key_column_usage kcu ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema LEFT JOIN information_schema.check_constraints cc ON tc.constraint_name = cc.constraint_name WHERE tc.table_schema = 'public' AND tc.table_name = {{tableName}} GROUP BY tc.constraint_name, tc.constraint_type, cc.check_clause",
+ "returns": "rows"
+ },
+ "addPrimaryKey": {
+ "description": "Add primary key constraint",
+ "query": "ALTER TABLE \"{{tableName}}\" ADD CONSTRAINT \"{{constraintName}}\" PRIMARY KEY ({{columns}})",
+ "returns": "command"
+ },
+ "addUnique": {
+ "description": "Add unique constraint",
+ "query": "ALTER TABLE \"{{tableName}}\" ADD CONSTRAINT \"{{constraintName}}\" UNIQUE ({{columns}})",
+ "returns": "command"
+ },
+ "addCheck": {
+ "description": "Add check constraint",
+ "query": "ALTER TABLE \"{{tableName}}\" ADD CONSTRAINT \"{{constraintName}}\" CHECK ({{expression}})",
+ "returns": "command",
+ "example": "ALTER TABLE \"users\" ADD CONSTRAINT \"age_positive\" CHECK (age >= 0)"
+ },
+ "addForeignKey": {
+ "description": "Add foreign key constraint",
+ "query": "ALTER TABLE \"{{tableName}}\" ADD CONSTRAINT \"{{constraintName}}\" FOREIGN KEY ({{columns}}) REFERENCES \"{{refTable}}\" ({{refColumns}})",
+ "returns": "command"
+ },
+ "dropConstraint": {
+ "description": "Drop a constraint",
+ "query": "ALTER TABLE \"{{tableName}}\" DROP CONSTRAINT \"{{constraintName}}\"",
+ "returns": "command"
+ }
+ },
+ "indexes": {
+ "listIndexes": {
+ "description": "List all indexes for a table",
+ "query": "SELECT i.relname as indexname, ix.indisprimary as is_primary, ix.indisunique as is_unique, am.amname as index_type, pg_get_indexdef(ix.indexrelid) as indexdef FROM pg_class t JOIN pg_index ix ON t.oid = ix.indrelid JOIN pg_class i ON i.oid = ix.indexrelid JOIN pg_am am ON i.relam = am.oid WHERE t.relkind = 'r' AND t.relname = {{tableName}} AND t.relnamespace = (SELECT oid FROM pg_namespace WHERE nspname = 'public') ORDER BY i.relname",
+ "returns": "rows"
+ },
+ "createIndex": {
+ "description": "Create an index on a table",
+ "query": "CREATE{{unique}} INDEX{{ifNotExists}} \"{{indexName}}\" ON \"{{tableName}}\" USING {{indexType}} ({{columns}})",
+ "returns": "command",
+ "example": "CREATE INDEX \"idx_users_email\" ON \"users\" USING BTREE (email)",
+ "defaultParams": {
+ "indexType": "BTREE",
+ "unique": "",
+ "ifNotExists": ""
+ }
+ },
+ "dropIndex": {
+ "description": "Drop an index",
+ "query": "DROP INDEX IF EXISTS \"{{indexName}}\"",
+ "returns": "command"
+ }
+ },
+ "queryBuilder": {
+ "buildSelect": {
+ "description": "Build a SELECT query with filters, sorting, and pagination",
+ "query": "SELECT {{columns}} FROM \"{{tableName}}\"{{where}}{{orderBy}}{{limit}}{{offset}}",
+ "returns": "rows",
+ "defaultParams": {
+ "columns": "*",
+ "where": "",
+ "orderBy": "",
+ "limit": "",
+ "offset": ""
+ }
+ }
+ }
+ },
+ "playwrightPlaybooks": {
+ "adminLogin": {
+ "name": "Admin Login Flow",
+ "description": "Complete admin login flow for testing",
+ "tags": ["admin", "auth", "login"],
+ "steps": [
+ {
+ "action": "goto",
+ "url": "/admin/login"
+ },
+ {
+ "action": "expect",
+ "selector": "input[name='username']",
+ "text": "visible"
+ },
+ {
+ "action": "fill",
+ "selector": "input[name='username']",
+ "value": "{{username}}"
+ },
+ {
+ "action": "fill",
+ "selector": "input[name='password']",
+ "value": "{{password}}"
+ },
+ {
+ "action": "click",
+ "selector": "button[type='submit']"
+ },
+ {
+ "action": "wait",
+ "timeout": 2000
+ },
+ {
+ "action": "expect",
+ "url": "/admin/dashboard",
+ "text": "redirected"
+ }
+ ]
+ },
+ "createTable": {
+ "name": "Create Table Workflow",
+ "description": "Test creating a new database table through UI",
+ "tags": ["admin", "table", "crud"],
+ "steps": [
+ {
+ "action": "goto",
+ "url": "/admin/dashboard"
+ },
+ {
+ "action": "click",
+ "selector": "text=Table Manager"
+ },
+ {
+ "action": "click",
+ "selector": "button:has-text('Create Table')"
+ },
+ {
+ "action": "expect",
+ "selector": "text=Create New Table",
+ "text": "visible"
+ },
+ {
+ "action": "fill",
+ "selector": "input[label='Table Name']",
+ "value": "{{tableName}}"
+ },
+ {
+ "action": "fill",
+ "selector": "input[label='Column Name']",
+ "value": "id"
+ },
+ {
+ "action": "select",
+ "selector": "select[label='Data Type']",
+ "value": "INTEGER"
+ },
+ {
+ "action": "click",
+ "selector": "button:has-text('Create Table')"
+ },
+ {
+ "action": "wait",
+ "timeout": 1000
+ },
+ {
+ "action": "expect",
+ "selector": "text={{tableName}}",
+ "text": "visible"
+ }
+ ],
+ "cleanup": [
+ {
+ "action": "click",
+ "selector": "button:has-text('Drop Table')"
+ },
+ {
+ "action": "select",
+ "selector": "select",
+ "value": "{{tableName}}"
+ },
+ {
+ "action": "click",
+ "selector": "button:has-text('Drop')"
+ }
+ ]
+ },
+ "addColumn": {
+ "name": "Add Column to Table",
+ "description": "Test adding a column to an existing table",
+ "tags": ["admin", "column", "crud"],
+ "steps": [
+ {
+ "action": "goto",
+ "url": "/admin/dashboard"
+ },
+ {
+ "action": "click",
+ "selector": "text=Column Manager"
+ },
+ {
+ "action": "select",
+ "selector": "select[label='Select Table']",
+ "value": "{{tableName}}"
+ },
+ {
+ "action": "click",
+ "selector": "button:has-text('Add Column')"
+ },
+ {
+ "action": "fill",
+ "selector": "input[label='Column Name']",
+ "value": "{{columnName}}"
+ },
+ {
+ "action": "select",
+ "selector": "select[label='Data Type']",
+ "value": "{{dataType}}"
+ },
+ {
+ "action": "click",
+ "selector": "button:has-text('Add')"
+ },
+ {
+ "action": "wait",
+ "timeout": 1000
+ },
+ {
+ "action": "expect",
+ "selector": "text={{columnName}}",
+ "text": "visible"
+ }
+ ]
+ },
+ "createIndex": {
+ "name": "Create Database Index",
+ "description": "Test creating an index on a table",
+ "tags": ["admin", "index", "performance"],
+ "steps": [
+ {
+ "action": "goto",
+ "url": "/admin/dashboard"
+ },
+ {
+ "action": "click",
+ "selector": "text=Indexes"
+ },
+ {
+ "action": "select",
+ "selector": "select[label='Select Table']",
+ "value": "{{tableName}}"
+ },
+ {
+ "action": "click",
+ "selector": "button:has-text('Create Index')"
+ },
+ {
+ "action": "fill",
+ "selector": "input[label='Index Name']",
+ "value": "{{indexName}}"
+ },
+ {
+ "action": "click",
+ "selector": "text={{columnName}}"
+ },
+ {
+ "action": "click",
+ "selector": "button:has-text('Create')"
+ },
+ {
+ "action": "wait",
+ "timeout": 1000
+ },
+ {
+ "action": "expect",
+ "selector": "text={{indexName}}",
+ "text": "visible"
+ }
+ ]
+ },
+ "queryBuilder": {
+ "name": "Query Builder Workflow",
+ "description": "Test building and executing a query",
+ "tags": ["admin", "query", "select"],
+ "steps": [
+ {
+ "action": "goto",
+ "url": "/admin/dashboard"
+ },
+ {
+ "action": "click",
+ "selector": "text=Query Builder"
+ },
+ {
+ "action": "select",
+ "selector": "select[label='Select Table']",
+ "value": "{{tableName}}"
+ },
+ {
+ "action": "click",
+ "selector": "text={{columnName}}"
+ },
+ {
+ "action": "click",
+ "selector": "button:has-text('Execute Query')"
+ },
+ {
+ "action": "wait",
+ "timeout": 2000
+ },
+ {
+ "action": "expect",
+ "selector": "text=Query Results",
+ "text": "visible"
+ },
+ {
+ "action": "screenshot",
+ "selector": ".query-results"
+ }
+ ]
+ },
+ "securityCheck": {
+ "name": "API Security Check",
+ "description": "Verify API endpoints require authentication",
+ "tags": ["security", "api", "auth"],
+ "steps": [
+ {
+ "action": "goto",
+ "url": "/api/admin/tables"
+ },
+ {
+ "action": "expect",
+ "text": "401"
+ }
+ ]
+ }
+ },
+ "storybookStories": {
+ "DataGrid": {
+ "default": {
+ "name": "Default",
+ "description": "Basic data grid with sample data",
+ "args": {
+ "columns": [
+ { "name": "id", "label": "ID" },
+ { "name": "name", "label": "Name" },
+ { "name": "email", "label": "Email" }
+ ],
+ "rows": [
+ { "id": 1, "name": "John Doe", "email": "john@example.com" },
+ { "id": 2, "name": "Jane Smith", "email": "jane@example.com" },
+ { "id": 3, "name": "Bob Johnson", "email": "bob@example.com" }
+ ],
+ "primaryKey": "id"
+ }
+ },
+ "withActions": {
+ "name": "With Edit/Delete Actions",
+ "description": "Data grid with action buttons",
+ "args": {
+ "columns": [
+ { "name": "id", "label": "ID" },
+ { "name": "name", "label": "Name" },
+ { "name": "status", "label": "Status" }
+ ],
+ "rows": [
+ { "id": 1, "name": "Active User", "status": "active" },
+ { "id": 2, "name": "Pending User", "status": "pending" }
+ ],
+ "onEdit": "handleEdit",
+ "onDelete": "handleDelete",
+ "primaryKey": "id"
+ },
+ "play": [
+ "await userEvent.click(screen.getAllByTitle('Edit')[0])",
+ "await expect(args.onEdit).toHaveBeenCalledWith({ id: 1, name: 'Active User', status: 'active' })"
+ ]
+ },
+ "empty": {
+ "name": "Empty State",
+ "description": "Data grid with no data",
+ "args": {
+ "columns": [
+ { "name": "id", "label": "ID" },
+ { "name": "name", "label": "Name" }
+ ],
+ "rows": []
+ }
+ }
+ },
+ "ConfirmDialog": {
+ "default": {
+ "name": "Default",
+ "description": "Basic confirmation dialog",
+ "args": {
+ "open": true,
+ "title": "Confirm Action",
+ "message": "Are you sure you want to proceed?",
+ "confirmLabel": "Confirm",
+ "cancelLabel": "Cancel"
+ }
+ },
+ "deleteWarning": {
+ "name": "Delete Warning",
+ "description": "Confirmation dialog for deletion",
+ "args": {
+ "open": true,
+ "title": "Delete Item",
+ "message": "This action cannot be undone. Are you sure you want to delete this item?",
+ "confirmLabel": "Delete",
+ "cancelLabel": "Cancel"
+ }
+ }
+ },
+ "FormDialog": {
+ "default": {
+ "name": "Default",
+ "description": "Basic form dialog",
+ "args": {
+ "open": true,
+ "title": "Add User",
+ "fields": [
+ { "name": "name", "label": "Name", "type": "text", "required": true },
+ { "name": "email", "label": "Email", "type": "email", "required": true }
+ ],
+ "submitLabel": "Save"
+ }
+ },
+ "withInitialData": {
+ "name": "Edit Mode",
+ "description": "Form dialog with initial data for editing",
+ "args": {
+ "open": true,
+ "title": "Edit User",
+ "fields": [
+ { "name": "name", "label": "Name", "type": "text", "required": true },
+ { "name": "email", "label": "Email", "type": "email", "required": true },
+ { "name": "role", "label": "Role", "type": "text" }
+ ],
+ "initialData": {
+ "name": "John Doe",
+ "email": "john@example.com",
+ "role": "admin"
+ },
+ "submitLabel": "Update"
+ }
+ }
+ },
+ "CreateTableDialog": {
+ "default": {
+ "name": "Default",
+ "description": "Dialog for creating a new table",
+ "args": {
+ "open": true,
+ "dataTypes": ["INTEGER", "VARCHAR", "TEXT", "BOOLEAN", "TIMESTAMP"]
+ }
+ },
+ "withColumns": {
+ "name": "With Predefined Columns",
+ "description": "Dialog with some columns already added",
+ "args": {
+ "open": true,
+ "dataTypes": ["INTEGER", "VARCHAR", "TEXT", "BOOLEAN", "TIMESTAMP"]
+ },
+ "play": [
+ "await userEvent.type(screen.getByLabelText('Table Name'), 'users')",
+ "await userEvent.type(screen.getByLabelText('Column Name'), 'id')",
+ "await userEvent.click(screen.getByText('Add Column'))",
+ "await userEvent.type(screen.getAllByLabelText('Column Name')[1], 'name')"
+ ]
+ }
+ },
+ "Button": {
+ "primary": {
+ "name": "Primary Button",
+ "description": "Primary action button",
+ "args": {
+ "variant": "contained",
+ "color": "primary",
+ "text": "Click Me"
+ }
+ },
+ "secondary": {
+ "name": "Secondary Button",
+ "description": "Secondary action button",
+ "args": {
+ "variant": "outlined",
+ "color": "secondary",
+ "text": "Cancel"
+ }
+ },
+ "withIcon": {
+ "name": "With Icon",
+ "description": "Button with start icon",
+ "args": {
+ "variant": "contained",
+ "startIcon": "Add",
+ "text": "Add Item"
+ }
+ },
+ "loading": {
+ "name": "Loading State",
+ "description": "Button in disabled/loading state",
+ "args": {
+ "variant": "contained",
+ "disabled": true,
+ "text": "Loading..."
+ }
+ }
+ }
+ }
}
diff --git a/src/utils/ComponentTreeRenderer.tsx b/src/utils/ComponentTreeRenderer.tsx
new file mode 100644
index 0000000..585deda
--- /dev/null
+++ b/src/utils/ComponentTreeRenderer.tsx
@@ -0,0 +1,270 @@
+'use client';
+
+import React from 'react';
+import {
+ Box,
+ Button,
+ Typography,
+ Paper,
+ TextField,
+ Select,
+ MenuItem,
+ Checkbox,
+ FormControlLabel,
+ IconButton,
+ List,
+ ListItem,
+ ListItemIcon,
+ ListItemText,
+ Table,
+ TableBody,
+ TableCell,
+ TableContainer,
+ TableHead,
+ TableRow,
+ Dialog,
+ DialogTitle,
+ DialogContent,
+ DialogActions,
+ Chip,
+ Tooltip,
+ FormControl,
+ InputLabel,
+} from '@mui/material';
+import AddIcon from '@mui/icons-material/Add';
+import DeleteIcon from '@mui/icons-material/Delete';
+import EditIcon from '@mui/icons-material/Edit';
+import TableChartIcon from '@mui/icons-material/TableChart';
+import SpeedIcon from '@mui/icons-material/Speed';
+import { ComponentNode } from './featureConfig';
+
+// Map of component names to actual components
+const componentMap: Record> = {
+ Box,
+ Button,
+ Typography,
+ Paper,
+ TextField,
+ Select,
+ MenuItem,
+ Checkbox,
+ FormControlLabel,
+ IconButton,
+ List,
+ ListItem,
+ ListItemIcon,
+ ListItemText,
+ Table,
+ TableBody,
+ TableCell,
+ TableContainer,
+ TableHead,
+ TableRow,
+ Dialog,
+ DialogTitle,
+ DialogContent,
+ DialogActions,
+ Chip,
+ Tooltip,
+ FormControl,
+ InputLabel,
+};
+
+// Map of icon names to icon components
+const iconMap: Record> = {
+ Add: AddIcon,
+ Delete: DeleteIcon,
+ Edit: EditIcon,
+ TableChart: TableChartIcon,
+ Speed: SpeedIcon,
+};
+
+type ComponentTreeRendererProps = {
+ tree: ComponentNode;
+ data?: Record;
+ handlers?: Record void>;
+};
+
+/**
+ * Evaluate a condition string with the provided data context
+ */
+function evaluateCondition(condition: string, data: Record): boolean {
+ try {
+ // Create a function that evaluates the condition in the data context
+ const func = new Function(...Object.keys(data), `return ${condition}`);
+ return func(...Object.values(data));
+ } catch (error) {
+ console.error('Error evaluating condition:', condition, error);
+ return false;
+ }
+}
+
+/**
+ * Interpolate template strings like {{variable}} with actual values from data
+ */
+function interpolateValue(value: any, data: Record): any {
+ if (typeof value !== 'string') {
+ return value;
+ }
+
+ // Check if it's a template string
+ const templateMatch = value.match(/^\{\{(.+)\}\}$/);
+ if (templateMatch && templateMatch[1]) {
+ const expression = templateMatch[1].trim();
+ try {
+ const func = new Function(...Object.keys(data), `return ${expression}`);
+ return func(...Object.values(data));
+ } catch (error) {
+ console.error('Error evaluating expression:', expression, error);
+ return value;
+ }
+ }
+
+ // Replace inline templates
+ return value.replace(/\{\{(.+?)\}\}/g, (_, expression) => {
+ try {
+ const func = new Function(...Object.keys(data), `return ${expression.trim()}`);
+ return func(...Object.values(data));
+ } catch (error) {
+ console.error('Error evaluating inline expression:', expression, error);
+ return '';
+ }
+ });
+}
+
+/**
+ * Interpolate all props in an object
+ */
+function interpolateProps(
+ props: Record | undefined,
+ data: Record,
+ handlers: Record void>
+): Record {
+ if (!props) return {};
+
+ const interpolated: Record = {};
+
+ Object.entries(props).forEach(([key, value]) => {
+ if (typeof value === 'string' && handlers[value]) {
+ // If the value is a handler function name, use the handler
+ interpolated[key] = handlers[value];
+ } else if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
+ // Recursively interpolate nested objects
+ interpolated[key] = interpolateProps(value, data, handlers);
+ } else {
+ // Interpolate the value
+ interpolated[key] = interpolateValue(value, data);
+ }
+ });
+
+ return interpolated;
+}
+
+/**
+ * Get the singular form of a plural word (simple implementation)
+ */
+function getSingular(plural: string): string {
+ if (plural.endsWith('ies')) {
+ return plural.slice(0, -3) + 'y';
+ }
+ if (plural.endsWith('es')) {
+ return plural.slice(0, -2);
+ }
+ if (plural.endsWith('s')) {
+ return plural.slice(0, -1);
+ }
+ return plural;
+}
+
+/**
+ * Render a single component node
+ */
+function renderNode(
+ node: ComponentNode,
+ data: Record,
+ handlers: Record void>,
+ key: string | number
+): React.ReactNode {
+ // Evaluate condition
+ if (node.condition && !evaluateCondition(node.condition, data)) {
+ return null;
+ }
+
+ // Handle forEach loops
+ if (node.forEach) {
+ const items = data[node.forEach];
+ if (!Array.isArray(items)) {
+ console.warn(`forEach data "${node.forEach}" is not an array`);
+ return null;
+ }
+
+ const singularName = getSingular(node.forEach);
+ return items.map((item, index) => {
+ const itemData = { ...data, [singularName]: item, index };
+ // Remove forEach from node to avoid infinite loop
+ const nodeWithoutForEach = { ...node, forEach: undefined };
+ return renderNode(nodeWithoutForEach, itemData, handlers, `${key}-${index}`);
+ });
+ }
+
+ // Get the component
+ const componentName = interpolateValue(node.component, data);
+ const Component = componentMap[componentName];
+
+ if (!Component) {
+ console.warn(`Component "${componentName}" not found in componentMap`);
+ return null;
+ }
+
+ // Interpolate props
+ const props = interpolateProps(node.props, data, handlers);
+
+ // Handle special props
+ if (props.startIcon && typeof props.startIcon === 'string') {
+ const IconComponent = iconMap[props.startIcon];
+ if (IconComponent) {
+ props.startIcon = ;
+ }
+ }
+
+ if (props.endIcon && typeof props.endIcon === 'string') {
+ const IconComponent = iconMap[props.endIcon];
+ if (IconComponent) {
+ props.endIcon = ;
+ }
+ }
+
+ // Handle 'text' prop for Typography and Button
+ let textContent = null;
+ if (props.text !== undefined) {
+ textContent = props.text;
+ delete props.text;
+ }
+
+ // Render children
+ const children = node.children?.map((child, index) =>
+ renderNode(child, data, handlers, `${key}-child-${index}`)
+ );
+
+ return (
+
+ {textContent}
+ {children}
+
+ );
+}
+
+/**
+ * ComponentTreeRenderer - Renders a component tree from JSON configuration
+ */
+export default function ComponentTreeRenderer({
+ tree,
+ data = {},
+ handlers = {},
+}: ComponentTreeRendererProps) {
+ if (!tree) {
+ return null;
+ }
+
+ return <>{renderNode(tree, data, handlers, 'root')}>;
+}
diff --git a/src/utils/featureConfig.ts b/src/utils/featureConfig.ts
index ad53c3f..c77471e 100644
--- a/src/utils/featureConfig.ts
+++ b/src/utils/featureConfig.ts
@@ -196,6 +196,41 @@ export type ComponentPropSchema = {
props: Record;
};
+export type SqlTemplate = {
+ description: string;
+ query: string;
+ returns: 'rows' | 'command';
+ example?: string;
+ defaultParams?: Record;
+};
+
+export type PlaywrightStep = {
+ action: 'goto' | 'click' | 'fill' | 'select' | 'wait' | 'expect' | 'screenshot';
+ selector?: string;
+ value?: string;
+ text?: string;
+ url?: string;
+ timeout?: number;
+ condition?: string;
+};
+
+export type PlaywrightPlaybook = {
+ name: string;
+ description: string;
+ tags?: string[];
+ steps: PlaywrightStep[];
+ cleanup?: PlaywrightStep[];
+};
+
+export type StorybookStory = {
+ name: string;
+ description?: string;
+ args?: Record;
+ argTypes?: Record;
+ parameters?: Record;
+ play?: string[];
+};
+
// Type definition for the features config structure
type FeaturesConfig = {
translations?: Translations;
@@ -213,6 +248,9 @@ type FeaturesConfig = {
uiViews?: Record>;
componentTrees?: Record;
componentProps?: Record;
+ sqlTemplates?: Record>;
+ playwrightPlaybooks?: Record;
+ storybookStories?: Record>;
features: Feature[];
dataTypes: DataType[];
constraintTypes?: ConstraintType[];
@@ -409,3 +447,60 @@ export function getComponentsByCategory(category: string): string[] {
.filter(([_, schema]) => schema.category === category)
.map(([name, _]) => name);
}
+
+// SQL Templates
+export function getSqlTemplate(category: string, templateName: string): SqlTemplate | undefined {
+ return config.sqlTemplates?.[category]?.[templateName];
+}
+
+export function getAllSqlTemplates(): Record> {
+ return config.sqlTemplates || {};
+}
+
+export function getSqlTemplatesByCategory(category: string): Record {
+ return config.sqlTemplates?.[category] || {};
+}
+
+export function interpolateSqlTemplate(template: SqlTemplate, params: Record): string {
+ let query = template.query;
+
+ // Merge default params with provided params
+ const allParams = { ...template.defaultParams, ...params };
+
+ // Replace template variables
+ Object.entries(allParams).forEach(([key, value]) => {
+ const regex = new RegExp(`\\{\\{${key}\\}\\}`, 'g');
+ query = query.replace(regex, String(value));
+ });
+
+ return query;
+}
+
+// Playwright Playbooks
+export function getPlaywrightPlaybook(playbookName: string): PlaywrightPlaybook | undefined {
+ return config.playwrightPlaybooks?.[playbookName];
+}
+
+export function getAllPlaywrightPlaybooks(): Record {
+ return config.playwrightPlaybooks || {};
+}
+
+export function getPlaywrightPlaybooksByTag(tag: string): PlaywrightPlaybook[] {
+ const playbooks = getAllPlaywrightPlaybooks();
+ return Object.values(playbooks).filter(playbook =>
+ playbook.tags?.includes(tag)
+ );
+}
+
+// Storybook Stories
+export function getStorybookStory(componentName: string, storyName: string): any {
+ return config.storybookStories?.[componentName]?.[storyName];
+}
+
+export function getAllStorybookStories(): Record {
+ return config.storybookStories || {};
+}
+
+export function getStorybookStoriesForComponent(componentName: string): Record {
+ return config.storybookStories?.[componentName] || {};
+}