curl -X POST http://localhost:3000/api/table-data \
-H "Content-Type: application/json" \
-d '{
"host": "localhost",
"port": 5432,
"user": "postgres",
"password": "mypassword",
"database": "mydb",
"schema": "public",
"table": "users",
"page": 1,
"pageSize": 50
}'
{
"columns": [
{
"name": "id",
"dataType": "integer",
"isNullable": false,
"defaultValue": "nextval('users_id_seq'::regclass)",
"isPrimaryKey": true
},
{
"name": "name",
"dataType": "character varying",
"isNullable": false,
"isPrimaryKey": false
},
{
"name": "email",
"dataType": "character varying",
"isNullable": false,
"isPrimaryKey": false
},
{
"name": "created_at",
"dataType": "timestamp without time zone",
"isNullable": true,
"defaultValue": "CURRENT_TIMESTAMP",
"isPrimaryKey": false
}
],
"rows": [
{
"id": 1,
"name": "Alice Johnson",
"email": "alice@example.com",
"created_at": "2024-01-15T10:30:00.000Z"
},
{
"id": 2,
"name": "Bob Smith",
"email": "bob@example.com",
"created_at": "2024-01-16T14:20:00.000Z"
}
],
"totalRows": 1500,
"currentPage": 1,
"pageSize": 50
}
Fetch paginated table data with sorting and column metadata
curl -X POST http://localhost:3000/api/table-data \
-H "Content-Type: application/json" \
-d '{
"host": "localhost",
"port": 5432,
"user": "postgres",
"password": "mypassword",
"database": "mydb",
"schema": "public",
"table": "users",
"page": 1,
"pageSize": 50
}'
{
"columns": [
{
"name": "id",
"dataType": "integer",
"isNullable": false,
"defaultValue": "nextval('users_id_seq'::regclass)",
"isPrimaryKey": true
},
{
"name": "name",
"dataType": "character varying",
"isNullable": false,
"isPrimaryKey": false
},
{
"name": "email",
"dataType": "character varying",
"isNullable": false,
"isPrimaryKey": false
},
{
"name": "created_at",
"dataType": "timestamp without time zone",
"isNullable": true,
"defaultValue": "CURRENT_TIMESTAMP",
"isPrimaryKey": false
}
],
"rows": [
{
"id": 1,
"name": "Alice Johnson",
"email": "alice@example.com",
"created_at": "2024-01-15T10:30:00.000Z"
},
{
"id": 2,
"name": "Bob Smith",
"email": "bob@example.com",
"created_at": "2024-01-16T14:20:00.000Z"
}
],
"totalRows": 1500,
"currentPage": 1,
"pageSize": 50
}
150Range: 1-1000asc or descDefault: ascdisable, require, prefer)integer, varchar, timestamp)curl -X POST http://localhost:3000/api/table-data \
-H "Content-Type: application/json" \
-d '{
"host": "localhost",
"port": 5432,
"user": "postgres",
"password": "mypassword",
"database": "mydb",
"schema": "public",
"table": "users",
"page": 1,
"pageSize": 50
}'
{
"columns": [
{
"name": "id",
"dataType": "integer",
"isNullable": false,
"defaultValue": "nextval('users_id_seq'::regclass)",
"isPrimaryKey": true
},
{
"name": "name",
"dataType": "character varying",
"isNullable": false,
"isPrimaryKey": false
},
{
"name": "email",
"dataType": "character varying",
"isNullable": false,
"isPrimaryKey": false
},
{
"name": "created_at",
"dataType": "timestamp without time zone",
"isNullable": true,
"defaultValue": "CURRENT_TIMESTAMP",
"isPrimaryKey": false
}
],
"rows": [
{
"id": 1,
"name": "Alice Johnson",
"email": "alice@example.com",
"created_at": "2024-01-15T10:30:00.000Z"
},
{
"id": 2,
"name": "Bob Smith",
"email": "bob@example.com",
"created_at": "2024-01-16T14:20:00.000Z"
}
],
"totalRows": 1500,
"currentPage": 1,
"pageSize": 50
}
curl -X POST http://localhost:3000/api/table-data \
-H "Content-Type: application/json" \
-d '{
"host": "localhost",
"port": 5432,
"user": "postgres",
"password": "mypassword",
"database": "mydb",
"schema": "public",
"table": "users",
"page": 1,
"pageSize": 20,
"sortColumn": "created_at",
"sortDirection": "desc"
}'
{
"columns": [...],
"rows": [
{
"id": 1500,
"name": "Zoe Williams",
"email": "zoe@example.com",
"created_at": "2024-03-20T09:15:00.000Z"
},
{
"id": 1499,
"name": "Yves Martin",
"email": "yves@example.com",
"created_at": "2024-03-19T16:45:00.000Z"
}
],
"totalRows": 1500,
"currentPage": 1,
"pageSize": 20
}
curl -X POST http://localhost:3000/api/table-data \
-H "Content-Type: application/json" \
-d '{
"host": "localhost",
"port": 5432,
"user": "postgres",
"password": "mypassword",
"database": "mydb",
"schema": "public",
"table": "users",
"page": 5,
"pageSize": 100
}'
{
"columns": [...],
"rows": [...],
"totalRows": 1500,
"currentPage": 5,
"pageSize": 100
}
{
"error": "Missing required fields"
}
{
"error": "Table has no columns or does not exist"
}
{
"error": "Invalid identifier: users; DROP TABLE users;"
}
/app/api/table-data/route.ts:47-64:
SELECT c.column_name,
c.data_type,
c.is_nullable,
c.column_default,
(tc.constraint_type = 'PRIMARY KEY') AS is_primary_key
FROM information_schema.columns c
LEFT JOIN information_schema.key_column_usage kcu
ON c.table_schema = kcu.table_schema
AND c.table_name = kcu.table_name
AND c.column_name = kcu.column_name
LEFT JOIN information_schema.table_constraints tc
ON kcu.table_schema = tc.table_schema
AND kcu.table_name = tc.table_name
AND kcu.constraint_name = tc.constraint_name
WHERE c.table_schema = $1 AND c.table_name = $2
ORDER BY c.ordinal_position
/app/api/table-data/route.ts:6-13:
function quoteIdentifier(identifier: string): string {
// Basic identifier validation and quoting to prevent injection
// Allows letters, numbers, underscore, and $; rejects others
if (!/^[A-Za-z_][A-Za-z0-9_$]*$/.test(identifier)) {
throw new Error(`Invalid identifier: ${identifier}`)
}
return '"' + identifier.replace(/""/g, '""') + '"'
}
/app/api/table-data/route.ts:102-108:
// Page and offset
const limit = Math.max(1, Math.min(1000, Number(pageSize) || 50))
const pageNum = Math.max(1, Number(page) || 1)
const offset = (pageNum - 1) * limit
// Build data query
const dataSql = `SELECT * FROM ${quoteIdentifier(schema)}.${quoteIdentifier(table)}${orderByClause} LIMIT $1 OFFSET $2`
/app/api/table-data/route.ts:88-94:
let orderByClause = ''
if (sortColumn) {
const allowed = columns.some((c) => c.name === sortColumn)
if (allowed) {
const safeDirection = String(sortDirection).toLowerCase() === 'desc' ? 'desc' : 'asc'
orderByClause = ` ORDER BY ${quoteIdentifier(sortColumn)} ${safeDirection.toUpperCase()}`
}
}
/types/database.ts:21-27:
export interface ColumnInfo {
name: string
dataType: string
isNullable: boolean
defaultValue?: string
isPrimaryKey?: boolean
}
/types/database.ts:29-35:
export interface TableData {
columns: ColumnInfo[]
rows: Record<string, any>[]
totalRows: number
currentPage: number
pageSize: number
}
COUNT(*) for every request:
SELECT COUNT(*)::bigint AS cnt FROM "public"."users"
pg_class.reltuples