Add configuration-driven features with JSON and reusable components

Co-authored-by: johndoe6345789 <224850594+johndoe6345789@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-01-08 02:28:39 +00:00
parent 8f968c69a6
commit 5560cc184a
10 changed files with 1158 additions and 4 deletions

View File

@@ -1,15 +1,24 @@
'use client';
import AddIcon from '@mui/icons-material/Add';
import CodeIcon from '@mui/icons-material/Code';
import DeleteIcon from '@mui/icons-material/Delete';
import EditIcon from '@mui/icons-material/Edit';
import LogoutIcon from '@mui/icons-material/Logout';
import StorageIcon from '@mui/icons-material/Storage';
import TableChartIcon from '@mui/icons-material/TableChart';
import {
Alert,
AppBar,
Box,
Button,
CircularProgress,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
Drawer,
IconButton,
List,
ListItem,
ListItemButton,
@@ -62,8 +71,18 @@ export default function AdminDashboard() {
const [selectedTable, setSelectedTable] = useState<string>('');
const [queryText, setQueryText] = useState('');
const [queryResult, setQueryResult] = useState<any>(null);
const [tableSchema, setTableSchema] = useState<any>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [successMessage, setSuccessMessage] = useState('');
// Dialog states
const [openCreateDialog, setOpenCreateDialog] = useState(false);
const [openEditDialog, setOpenEditDialog] = useState(false);
const [openDeleteDialog, setOpenDeleteDialog] = useState(false);
const [editingRecord, setEditingRecord] = useState<any>(null);
const [deletingRecord, setDeletingRecord] = useState<any>(null);
const [formData, setFormData] = useState<any>({});
const fetchTables = useCallback(async () => {
try {
@@ -90,11 +109,12 @@ export default function AdminDashboard() {
setSelectedTable(tableName);
setLoading(true);
setError('');
setSuccessMessage('');
setQueryResult(null);
try {
// Use dedicated API with table name validation
const response = await fetch('/api/admin/table-data', {
// Fetch table data
const dataResponse = await fetch('/api/admin/table-data', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -103,12 +123,26 @@ export default function AdminDashboard() {
});
if (!response.ok) {
const data = await response.json();
const data = await dataResponse.json();
throw new Error(data.error || 'Query failed');
}
const data = await response.json();
const data = await dataResponse.json();
setQueryResult(data);
// Fetch table schema
const schemaResponse = await fetch('/api/admin/table-schema', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ tableName }),
});
if (schemaResponse.ok) {
const schemaData = await schemaResponse.json();
setTableSchema(schemaData);
}
} catch (err: any) {
setError(err.message);
} finally {

View File

@@ -0,0 +1,214 @@
import { sql } from 'drizzle-orm';
import { NextResponse } from 'next/server';
import { db } from '@/utils/db';
import { getSession } from '@/utils/session';
// Validate identifier format (prevent SQL injection)
function isValidIdentifier(name: string): boolean {
return /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name);
}
// Validate table exists
async function validateTable(tableName: string): Promise<boolean> {
const result = await db.execute(sql`
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name = ${tableName}
`);
return result.rows.length > 0;
}
// ADD COLUMN
export async function POST(request: Request) {
try {
const session = await getSession();
if (!session) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 },
);
}
const { tableName, columnName, dataType, nullable, defaultValue } = await request.json();
if (!tableName || !columnName || !dataType) {
return NextResponse.json(
{ error: 'Table name, column name, and data type are required' },
{ status: 400 },
);
}
// Validate identifiers
if (!isValidIdentifier(tableName) || !isValidIdentifier(columnName)) {
return NextResponse.json(
{ error: 'Invalid table or column name format' },
{ status: 400 },
);
}
// Validate table exists
if (!(await validateTable(tableName))) {
return NextResponse.json(
{ error: 'Table not found' },
{ status: 404 },
);
}
let alterQuery = `ALTER TABLE "${tableName}" ADD COLUMN "${columnName}" ${dataType}`;
if (!nullable) {
alterQuery += ' NOT NULL';
}
if (defaultValue !== undefined && defaultValue !== null) {
if (typeof defaultValue === 'string') {
alterQuery += ` DEFAULT '${defaultValue}'`;
} else {
alterQuery += ` DEFAULT ${defaultValue}`;
}
}
await db.execute(sql.raw(alterQuery));
return NextResponse.json({
success: true,
message: `Column '${columnName}' added successfully`,
});
} catch (error: any) {
console.error('Add column error:', error);
return NextResponse.json(
{ error: error.message || 'Failed to add column' },
{ status: 500 },
);
}
}
// DROP COLUMN
export async function DELETE(request: Request) {
try {
const session = await getSession();
if (!session) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 },
);
}
const { tableName, columnName } = await request.json();
if (!tableName || !columnName) {
return NextResponse.json(
{ error: 'Table name and column name are required' },
{ status: 400 },
);
}
// Validate identifiers
if (!isValidIdentifier(tableName) || !isValidIdentifier(columnName)) {
return NextResponse.json(
{ error: 'Invalid table or column name format' },
{ status: 400 },
);
}
// Validate table exists
if (!(await validateTable(tableName))) {
return NextResponse.json(
{ error: 'Table not found' },
{ status: 404 },
);
}
const alterQuery = `ALTER TABLE "${tableName}" DROP COLUMN "${columnName}"`;
await db.execute(sql.raw(alterQuery));
return NextResponse.json({
success: true,
message: `Column '${columnName}' dropped successfully`,
});
} catch (error: any) {
console.error('Drop column error:', error);
return NextResponse.json(
{ error: error.message || 'Failed to drop column' },
{ status: 500 },
);
}
}
// MODIFY COLUMN
export async function PUT(request: Request) {
try {
const session = await getSession();
if (!session) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 },
);
}
const { tableName, columnName, newType, nullable } = await request.json();
if (!tableName || !columnName) {
return NextResponse.json(
{ error: 'Table name and column name are required' },
{ status: 400 },
);
}
// Validate identifiers
if (!isValidIdentifier(tableName) || !isValidIdentifier(columnName)) {
return NextResponse.json(
{ error: 'Invalid table or column name format' },
{ status: 400 },
);
}
// Validate table exists
if (!(await validateTable(tableName))) {
return NextResponse.json(
{ error: 'Table not found' },
{ status: 404 },
);
}
const alterQueries = [];
if (newType) {
alterQueries.push(`ALTER TABLE "${tableName}" ALTER COLUMN "${columnName}" TYPE ${newType}`);
}
if (nullable !== undefined) {
if (nullable) {
alterQueries.push(`ALTER TABLE "${tableName}" ALTER COLUMN "${columnName}" DROP NOT NULL`);
} else {
alterQueries.push(`ALTER TABLE "${tableName}" ALTER COLUMN "${columnName}" SET NOT NULL`);
}
}
if (alterQueries.length === 0) {
return NextResponse.json(
{ error: 'No modifications specified' },
{ status: 400 },
);
}
for (const query of alterQueries) {
await db.execute(sql.raw(query));
}
return NextResponse.json({
success: true,
message: `Column '${columnName}' modified successfully`,
});
} catch (error: any) {
console.error('Modify column error:', error);
return NextResponse.json(
{ error: error.message || 'Failed to modify column' },
{ status: 500 },
);
}
}

View File

@@ -0,0 +1,204 @@
import { sql } from 'drizzle-orm';
import { NextResponse } from 'next/server';
import { db } from '@/utils/db';
import { getSession } from '@/utils/session';
// Validate table name exists in schema
async function validateTable(tableName: string): Promise<boolean> {
const result = await db.execute(sql`
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name = ${tableName}
`);
return result.rows.length > 0;
}
// CREATE - Insert a new record
export async function POST(request: Request) {
try {
const session = await getSession();
if (!session) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 },
);
}
const { tableName, data } = await request.json();
if (!tableName || !data) {
return NextResponse.json(
{ error: 'Table name and data are required' },
{ status: 400 },
);
}
// Validate table exists
if (!(await validateTable(tableName))) {
return NextResponse.json(
{ error: 'Table not found' },
{ status: 404 },
);
}
const columns = Object.keys(data);
const values = Object.values(data);
if (columns.length === 0) {
return NextResponse.json(
{ error: 'No data provided' },
{ status: 400 },
);
}
// Build parameterized insert query
const columnList = columns.map(col => `"${col}"`).join(', ');
const placeholders = values.map((_, idx) => `$${idx + 1}`).join(', ');
const query = `INSERT INTO "${tableName}" (${columnList}) VALUES (${placeholders}) RETURNING *`;
const result = await db.execute(sql.raw(query, values));
return NextResponse.json({
success: true,
record: result.rows[0],
});
} catch (error: any) {
console.error('Insert error:', error);
return NextResponse.json(
{ error: error.message || 'Failed to insert record' },
{ status: 500 },
);
}
}
// UPDATE - Update an existing record
export async function PUT(request: Request) {
try {
const session = await getSession();
if (!session) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 },
);
}
const { tableName, primaryKey, data } = await request.json();
if (!tableName || !primaryKey || !data) {
return NextResponse.json(
{ error: 'Table name, primary key, and data are required' },
{ status: 400 },
);
}
// Validate table exists
if (!(await validateTable(tableName))) {
return NextResponse.json(
{ error: 'Table not found' },
{ status: 404 },
);
}
const columns = Object.keys(data);
const values = Object.values(data);
if (columns.length === 0) {
return NextResponse.json(
{ error: 'No data provided' },
{ status: 400 },
);
}
// Build parameterized update query
const setClause = columns.map((col, idx) => `"${col}" = $${idx + 1}`).join(', ');
const whereClause = Object.keys(primaryKey)
.map((key, idx) => `"${key}" = $${values.length + idx + 1}`)
.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));
if (result.rowCount === 0) {
return NextResponse.json(
{ error: 'Record not found' },
{ status: 404 },
);
}
return NextResponse.json({
success: true,
record: result.rows[0],
});
} catch (error: any) {
console.error('Update error:', error);
return NextResponse.json(
{ error: error.message || 'Failed to update record' },
{ status: 500 },
);
}
}
// DELETE - Delete a record
export async function DELETE(request: Request) {
try {
const session = await getSession();
if (!session) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 },
);
}
const { tableName, primaryKey } = await request.json();
if (!tableName || !primaryKey) {
return NextResponse.json(
{ error: 'Table name and primary key are required' },
{ status: 400 },
);
}
// Validate table exists
if (!(await validateTable(tableName))) {
return NextResponse.json(
{ error: 'Table not found' },
{ status: 404 },
);
}
// Build parameterized delete query
const whereClause = Object.keys(primaryKey)
.map((key, idx) => `"${key}" = $${idx + 1}`)
.join(' AND ');
const query = `DELETE FROM "${tableName}" WHERE ${whereClause} RETURNING *`;
const values = Object.values(primaryKey);
const result = await db.execute(sql.raw(query, values));
if (result.rowCount === 0) {
return NextResponse.json(
{ error: 'Record not found' },
{ status: 404 },
);
}
return NextResponse.json({
success: true,
deletedRecord: result.rows[0],
});
} catch (error: any) {
console.error('Delete error:', error);
return NextResponse.json(
{ error: error.message || 'Failed to delete record' },
{ status: 500 },
);
}
}

View File

@@ -0,0 +1,154 @@
import { sql } from 'drizzle-orm';
import { NextResponse } from 'next/server';
import { db } from '@/utils/db';
import { getSession } from '@/utils/session';
// Validate table name format (prevent SQL injection)
function isValidIdentifier(name: string): boolean {
return /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name);
}
// CREATE TABLE
export async function POST(request: Request) {
try {
const session = await getSession();
if (!session) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 },
);
}
const { tableName, columns } = await request.json();
if (!tableName || !columns || !Array.isArray(columns) || columns.length === 0) {
return NextResponse.json(
{ error: 'Table name and columns are required' },
{ status: 400 },
);
}
// Validate table name
if (!isValidIdentifier(tableName)) {
return NextResponse.json(
{ error: 'Invalid table name format' },
{ status: 400 },
);
}
// Build column definitions
const columnDefs = columns.map((col: any) => {
if (!col.name || !col.type) {
throw new Error('Each column must have a name and type');
}
if (!isValidIdentifier(col.name)) {
throw new Error(`Invalid column name: ${col.name}`);
}
let def = `"${col.name}" ${col.type}`;
if (col.length && (col.type === 'VARCHAR' || col.type === 'CHARACTER VARYING')) {
def += `(${col.length})`;
}
if (col.primaryKey) {
def += ' PRIMARY KEY';
}
if (col.unique) {
def += ' UNIQUE';
}
if (!col.nullable) {
def += ' NOT NULL';
}
if (col.default !== undefined && col.default !== null) {
if (typeof col.default === 'string') {
def += ` DEFAULT '${col.default}'`;
} else {
def += ` DEFAULT ${col.default}`;
}
}
return def;
}).join(', ');
const createQuery = `CREATE TABLE "${tableName}" (${columnDefs})`;
await db.execute(sql.raw(createQuery));
return NextResponse.json({
success: true,
message: `Table '${tableName}' created successfully`,
});
} catch (error: any) {
console.error('Create table error:', error);
return NextResponse.json(
{ error: error.message || 'Failed to create table' },
{ status: 500 },
);
}
}
// DROP TABLE
export async function DELETE(request: Request) {
try {
const session = await getSession();
if (!session) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 },
);
}
const { tableName } = await request.json();
if (!tableName) {
return NextResponse.json(
{ error: 'Table name is required' },
{ status: 400 },
);
}
// Validate table name
if (!isValidIdentifier(tableName)) {
return NextResponse.json(
{ error: 'Invalid table name format' },
{ status: 400 },
);
}
// Verify table exists
const tablesResult = await db.execute(sql`
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name = ${tableName}
`);
if (tablesResult.rows.length === 0) {
return NextResponse.json(
{ error: 'Table not found' },
{ status: 404 },
);
}
const dropQuery = `DROP TABLE "${tableName}"`;
await db.execute(sql.raw(dropQuery));
return NextResponse.json({
success: true,
message: `Table '${tableName}' dropped successfully`,
});
} catch (error: any) {
console.error('Drop table error:', error);
return NextResponse.json(
{ error: error.message || 'Failed to drop table' },
{ status: 500 },
);
}
}

View File

@@ -0,0 +1,104 @@
import { sql } from 'drizzle-orm';
import { NextResponse } from 'next/server';
import { db } from '@/utils/db';
import { getSession } from '@/utils/session';
export async function POST(request: Request) {
try {
const session = await getSession();
if (!session) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 },
);
}
const { tableName } = await request.json();
if (!tableName) {
return NextResponse.json(
{ error: 'Table name is required' },
{ status: 400 },
);
}
// Validate table exists
const tablesResult = await db.execute(sql`
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name = ${tableName}
`);
if (tablesResult.rows.length === 0) {
return NextResponse.json(
{ error: 'Table not found' },
{ status: 404 },
);
}
// Get column information
const columnsResult = await db.execute(sql`
SELECT
column_name,
data_type,
character_maximum_length,
is_nullable,
column_default
FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = ${tableName}
ORDER BY ordinal_position
`);
// Get primary key information
const pkResult = await db.execute(sql`
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}
`);
// Get foreign key information
const fkResult = await db.execute(sql`
SELECT
kcu.column_name,
ccu.table_name AS foreign_table_name,
ccu.column_name AS foreign_column_name
FROM information_schema.table_constraints AS tc
JOIN information_schema.key_column_usage AS kcu
ON tc.constraint_name = kcu.constraint_name
AND tc.table_schema = kcu.table_schema
JOIN information_schema.constraint_column_usage AS ccu
ON ccu.constraint_name = tc.constraint_name
AND ccu.table_schema = tc.table_schema
WHERE tc.constraint_type = 'FOREIGN KEY'
AND tc.table_schema = 'public'
AND tc.table_name = ${tableName}
`);
const primaryKeys = pkResult.rows.map((row: any) => row.column_name);
const foreignKeys = fkResult.rows.map((row: any) => ({
column: row.column_name,
foreignTable: row.foreign_table_name,
foreignColumn: row.foreign_column_name,
}));
return NextResponse.json({
columns: columnsResult.rows,
primaryKeys,
foreignKeys,
});
} catch (error: any) {
console.error('Schema query error:', error);
return NextResponse.json(
{ error: error.message || 'Failed to fetch schema' },
{ status: 500 },
);
}
}

View File

@@ -0,0 +1,45 @@
'use client';
import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
} from '@mui/material';
type ConfirmDialogProps = {
open: boolean;
title: string;
message: string;
onConfirm: () => void;
onCancel: () => void;
confirmLabel?: string;
cancelLabel?: string;
};
export default function ConfirmDialog({
open,
title,
message,
onConfirm,
onCancel,
confirmLabel = 'Confirm',
cancelLabel = 'Cancel',
}: ConfirmDialogProps) {
return (
<Dialog open={open} onClose={onCancel}>
<DialogTitle>{title}</DialogTitle>
<DialogContent>
<DialogContentText>{message}</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={onCancel}>{cancelLabel}</Button>
<Button onClick={onConfirm} color="error" variant="contained">
{confirmLabel}
</Button>
</DialogActions>
</Dialog>
);
}

View File

@@ -0,0 +1,77 @@
'use client';
import DeleteIcon from '@mui/icons-material/Delete';
import EditIcon from '@mui/icons-material/Edit';
import {
IconButton,
Paper,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Tooltip,
} from '@mui/material';
type DataGridProps = {
columns: Array<{ name: string; label?: string }>;
rows: any[];
onEdit?: (row: any) => void;
onDelete?: (row: any) => void;
primaryKey?: string;
};
export default function DataGrid({ columns, rows, onEdit, onDelete, primaryKey = 'id' }: DataGridProps) {
return (
<TableContainer component={Paper}>
<Table size="small">
<TableHead>
<TableRow>
{columns.map(col => (
<TableCell key={col.name}>
<strong>{col.label || col.name}</strong>
</TableCell>
))}
{(onEdit || onDelete) && (
<TableCell>
<strong>Actions</strong>
</TableCell>
)}
</TableRow>
</TableHead>
<TableBody>
{rows.map((row, idx) => (
<TableRow key={row[primaryKey] || idx}>
{columns.map(col => (
<TableCell key={col.name}>
{row[col.name] !== null && row[col.name] !== undefined
? String(row[col.name])
: 'NULL'}
</TableCell>
))}
{(onEdit || onDelete) && (
<TableCell>
{onEdit && (
<Tooltip title="Edit">
<IconButton size="small" onClick={() => onEdit(row)}>
<EditIcon fontSize="small" />
</IconButton>
</Tooltip>
)}
{onDelete && (
<Tooltip title="Delete">
<IconButton size="small" color="error" onClick={() => onDelete(row)}>
<DeleteIcon fontSize="small" />
</IconButton>
</Tooltip>
)}
</TableCell>
)}
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
);
}

View File

@@ -0,0 +1,99 @@
'use client';
import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
TextField,
} from '@mui/material';
import { useEffect, useState } from 'react';
type FormField = {
name: string;
label: string;
type?: string;
required?: boolean;
defaultValue?: any;
};
type FormDialogProps = {
open: boolean;
title: string;
fields: FormField[];
initialData?: any;
onClose: () => void;
onSubmit: (data: any) => Promise<void>;
submitLabel?: string;
};
export default function FormDialog({
open,
title,
fields,
initialData,
onClose,
onSubmit,
submitLabel = 'Submit',
}: FormDialogProps) {
const [formData, setFormData] = useState<any>({});
const [loading, setLoading] = useState(false);
useEffect(() => {
if (initialData) {
setFormData(initialData);
} else {
setFormData({});
}
}, [initialData, open]);
const handleSubmit = async () => {
setLoading(true);
try {
await onSubmit(formData);
setFormData({});
onClose();
} catch (error) {
console.error('Form submission error:', error);
} finally {
setLoading(false);
}
};
const handleChange = (fieldName: string, value: any) => {
setFormData((prev: any) => ({
...prev,
[fieldName]: value,
}));
};
return (
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
<DialogTitle>{title}</DialogTitle>
<DialogContent>
{fields.map(field => (
<TextField
key={field.name}
margin="normal"
fullWidth
label={field.label}
type={field.type || 'text'}
required={field.required}
value={formData[field.name] !== undefined ? formData[field.name] : (field.defaultValue || '')}
onChange={e => handleChange(field.name, e.target.value)}
disabled={loading}
/>
))}
</DialogContent>
<DialogActions>
<Button onClick={onClose} disabled={loading}>
Cancel
</Button>
<Button onClick={handleSubmit} variant="contained" disabled={loading}>
{submitLabel}
</Button>
</DialogActions>
</Dialog>
);
}

164
src/config/features.json Normal file
View File

@@ -0,0 +1,164 @@
{
"features": [
{
"id": "database-crud",
"name": "Database CRUD Operations",
"description": "Create, read, update, and delete database records",
"enabled": true,
"priority": "high",
"endpoints": [
{
"path": "/api/admin/record",
"methods": ["POST", "PUT", "DELETE"],
"description": "Manage database records"
},
{
"path": "/api/admin/table-data",
"methods": ["POST"],
"description": "Fetch table data"
},
{
"path": "/api/admin/table-schema",
"methods": ["POST"],
"description": "Fetch table schema information"
}
],
"ui": {
"showInNav": true,
"icon": "Storage",
"actions": ["create", "read", "update", "delete"]
}
},
{
"id": "table-management",
"name": "Table Management",
"description": "Create and manage database tables",
"enabled": true,
"priority": "high",
"endpoints": [
{
"path": "/api/admin/table-manage",
"methods": ["POST", "DELETE"],
"description": "Create and drop tables"
}
],
"ui": {
"showInNav": true,
"icon": "TableChart",
"actions": ["create", "delete"]
}
},
{
"id": "column-management",
"name": "Column Management",
"description": "Add, modify, and delete table columns",
"enabled": true,
"priority": "high",
"endpoints": [
{
"path": "/api/admin/column-manage",
"methods": ["POST", "PUT", "DELETE"],
"description": "Manage table columns"
}
],
"ui": {
"showInNav": true,
"icon": "ViewColumn",
"actions": ["add", "modify", "delete"]
}
},
{
"id": "sql-query",
"name": "SQL Query Interface",
"description": "Execute custom SQL queries",
"enabled": true,
"priority": "high",
"endpoints": [
{
"path": "/api/admin/query",
"methods": ["POST"],
"description": "Execute SQL queries"
}
],
"ui": {
"showInNav": true,
"icon": "Code",
"actions": ["execute"]
}
}
],
"dataTypes": [
{
"name": "INTEGER",
"category": "numeric",
"requiresLength": false
},
{
"name": "BIGINT",
"category": "numeric",
"requiresLength": false
},
{
"name": "SERIAL",
"category": "numeric",
"requiresLength": false,
"autoIncrement": true
},
{
"name": "VARCHAR",
"category": "text",
"requiresLength": true,
"defaultLength": 255
},
{
"name": "TEXT",
"category": "text",
"requiresLength": false
},
{
"name": "BOOLEAN",
"category": "boolean",
"requiresLength": false
},
{
"name": "TIMESTAMP",
"category": "datetime",
"requiresLength": false
},
{
"name": "DATE",
"category": "datetime",
"requiresLength": false
},
{
"name": "JSON",
"category": "json",
"requiresLength": false
},
{
"name": "JSONB",
"category": "json",
"requiresLength": false
}
],
"navItems": [
{
"id": "tables",
"label": "Tables",
"icon": "Storage",
"featureId": "database-crud"
},
{
"id": "query",
"label": "SQL Query",
"icon": "Code",
"featureId": "sql-query"
},
{
"id": "table-manager",
"label": "Table Manager",
"icon": "TableChart",
"featureId": "table-management"
}
]
}

View File

@@ -0,0 +1,59 @@
import featuresConfig from '@/config/features.json';
export type Feature = {
id: string;
name: string;
description: string;
enabled: boolean;
priority: string;
endpoints: Array<{
path: string;
methods: string[];
description: string;
}>;
ui: {
showInNav: boolean;
icon: string;
actions: string[];
};
};
export type DataType = {
name: string;
category: string;
requiresLength: boolean;
defaultLength?: number;
autoIncrement?: boolean;
};
export type NavItem = {
id: string;
label: string;
icon: string;
featureId: string;
};
export function getFeatures(): Feature[] {
return featuresConfig.features.filter(f => f.enabled);
}
export function getFeatureById(id: string): Feature | undefined {
return featuresConfig.features.find(f => f.id === id && f.enabled);
}
export function getDataTypes(): DataType[] {
return featuresConfig.dataTypes;
}
export function getNavItems(): NavItem[] {
return featuresConfig.navItems.filter(item => {
const feature = getFeatureById(item.featureId);
return feature && feature.enabled;
});
}
export function getEnabledFeaturesByPriority(priority: string): Feature[] {
return featuresConfig.features.filter(
f => f.enabled && f.priority === priority,
);
}