clean comments

This commit is contained in:
Lakee Sivaraya
2026-01-16 11:50:44 -08:00
parent 26d96624af
commit 5173320bb5
28 changed files with 15 additions and 590 deletions

View File

@@ -13,47 +13,15 @@ import {
const logger = createLogger('TableDetailAPI')
/**
* Zod schema for validating get table requests.
*
* The workspaceId is required and validated against the table.
*/
const GetTableSchema = z.object({
workspaceId: z.string().min(1, 'Workspace ID is required'),
})
/**
* Route params for table detail endpoints.
*/
interface TableRouteParams {
params: Promise<{ tableId: string }>
}
/**
* GET /api/table/[tableId]?workspaceId=xxx
*
* Retrieves details for a specific table.
*
* @param request - The incoming HTTP request
* @param context - Route context containing tableId param
* @returns JSON response with table details or error
*
* @example Response:
* ```json
* {
* "success": true,
* "data": {
* "table": {
* "id": "tbl_abc123",
* "name": "customers",
* "schema": { "columns": [...] },
* "rowCount": 150,
* "maxRows": 10000
* }
* }
* }
* ```
*/
/** GET /api/table/[tableId] - Retrieves a single table's details. */
export async function GET(request: NextRequest, { params }: TableRouteParams) {
const requestId = generateRequestId()
const { tableId } = await params
@@ -70,7 +38,6 @@ export async function GET(request: NextRequest, { params }: TableRouteParams) {
workspaceId: searchParams.get('workspaceId'),
})
// Check table access (similar to knowledge base access control)
const accessCheck = await checkTableAccess(tableId, authResult.userId)
if (!accessCheck.hasAccess) {
@@ -92,7 +59,6 @@ export async function GET(request: NextRequest, { params }: TableRouteParams) {
return NextResponse.json({ error: 'Invalid workspace ID' }, { status: 400 })
}
// Get table using service layer
const table = await getTableById(tableId)
if (!table) {
@@ -139,11 +105,7 @@ export async function GET(request: NextRequest, { params }: TableRouteParams) {
}
}
/**
* DELETE /api/table/[tableId]?workspaceId=xxx
*
* Deletes a table and all its rows (hard delete, requires write access).
*/
/** DELETE /api/table/[tableId] - Deletes a table and all its rows. */
export async function DELETE(request: NextRequest, { params }: TableRouteParams) {
const requestId = generateRequestId()
const { tableId } = await params
@@ -160,7 +122,6 @@ export async function DELETE(request: NextRequest, { params }: TableRouteParams)
workspaceId: searchParams.get('workspaceId'),
})
// Check table write access (similar to knowledge base write access control)
const accessCheck = await checkTableWriteAccess(tableId, authResult.userId)
if (!accessCheck.hasAccess) {
@@ -174,7 +135,6 @@ export async function DELETE(request: NextRequest, { params }: TableRouteParams)
return NextResponse.json({ error: 'Access denied' }, { status: 403 })
}
// Security check: verify workspaceId matches the table's workspace
const isValidWorkspace = await verifyTableWorkspace(tableId, validated.workspaceId)
if (!isValidWorkspace) {
logger.warn(
@@ -183,7 +143,6 @@ export async function DELETE(request: NextRequest, { params }: TableRouteParams)
return NextResponse.json({ error: 'Invalid workspace ID' }, { status: 400 })
}
// Soft delete table using service layer
await deleteTable(tableId, requestId)
return NextResponse.json({

View File

@@ -17,61 +17,24 @@ import {
const logger = createLogger('TableRowAPI')
/**
* Zod schema for validating get row requests.
*
* The workspaceId is required and validated against the table.
*/
const GetRowSchema = z.object({
workspaceId: z.string().min(1, 'Workspace ID is required'),
})
/**
* Zod schema for validating update row requests.
*/
const UpdateRowSchema = z.object({
workspaceId: z.string().min(1, 'Workspace ID is required'),
data: z.record(z.unknown(), { required_error: 'Row data is required' }),
})
/**
* Zod schema for validating delete row requests.
*/
const DeleteRowSchema = z.object({
workspaceId: z.string().min(1, 'Workspace ID is required'),
})
/**
* Route params for single row endpoints.
*/
interface RowRouteParams {
params: Promise<{ tableId: string; rowId: string }>
}
/**
* GET /api/table/[tableId]/rows/[rowId]?workspaceId=xxx
*
* Retrieves a single row by its ID.
*
* @param request - The incoming HTTP request
* @param context - Route context containing tableId and rowId params
* @returns JSON response with row data or error
*
* @example Response:
* ```json
* {
* "success": true,
* "data": {
* "row": {
* "id": "row_abc123",
* "data": { "name": "John", "email": "john@example.com" },
* "createdAt": "2024-01-01T00:00:00.000Z",
* "updatedAt": "2024-01-01T00:00:00.000Z"
* }
* }
* }
* ```
*/
/** GET /api/table/[tableId]/rows/[rowId] - Retrieves a single row. */
export async function GET(request: NextRequest, { params }: RowRouteParams) {
const requestId = generateRequestId()
const { tableId, rowId } = await params
@@ -87,7 +50,6 @@ export async function GET(request: NextRequest, { params }: RowRouteParams) {
workspaceId: searchParams.get('workspaceId'),
})
// Check table access (centralized access control)
const accessCheck = await checkTableAccess(tableId, authResult.userId)
if (!accessCheck.hasAccess) {
@@ -101,7 +63,6 @@ export async function GET(request: NextRequest, { params }: RowRouteParams) {
return NextResponse.json({ error: 'Access denied' }, { status: 403 })
}
// Security check: verify workspaceId matches the table's workspace
const actualWorkspaceId = validated.workspaceId
const isValidWorkspace = await verifyTableWorkspace(tableId, validated.workspaceId)
if (!isValidWorkspace) {
@@ -111,7 +72,6 @@ export async function GET(request: NextRequest, { params }: RowRouteParams) {
return NextResponse.json({ error: 'Invalid workspace ID' }, { status: 400 })
}
// Get row
const [row] = await db
.select({
id: userTableRows.id,
@@ -159,11 +119,7 @@ export async function GET(request: NextRequest, { params }: RowRouteParams) {
}
}
/**
* PATCH /api/table/[tableId]/rows/[rowId]
*
* Updates an existing row with new data (full replacement, not partial).
*/
/** PATCH /api/table/[tableId]/rows/[rowId] - Updates a single row. */
export async function PATCH(request: NextRequest, { params }: RowRouteParams) {
const requestId = generateRequestId()
const { tableId, rowId } = await params
@@ -177,11 +133,9 @@ export async function PATCH(request: NextRequest, { params }: RowRouteParams) {
const body: unknown = await request.json()
const validated = UpdateRowSchema.parse(body)
// Check table write access
const accessResult = await checkAccessOrRespond(tableId, authResult.userId, requestId, 'write')
if (accessResult instanceof NextResponse) return accessResult
// Security check: verify workspaceId matches the table's workspace
const actualWorkspaceId = validated.workspaceId
const isValidWorkspace = await verifyTableWorkspace(tableId, validated.workspaceId)
if (!isValidWorkspace) {
@@ -191,7 +145,6 @@ export async function PATCH(request: NextRequest, { params }: RowRouteParams) {
return NextResponse.json({ error: 'Invalid workspace ID' }, { status: 400 })
}
// Get table definition
const table = await getTableById(tableId)
if (!table) {
return NextResponse.json({ error: 'Table not found' }, { status: 404 })
@@ -199,7 +152,6 @@ export async function PATCH(request: NextRequest, { params }: RowRouteParams) {
const rowData = validated.data as RowData
// Validate row data (size, schema, unique constraints)
const validation = await validateRowData({
rowData,
schema: table.schema as TableSchema,
@@ -208,7 +160,6 @@ export async function PATCH(request: NextRequest, { params }: RowRouteParams) {
})
if (!validation.valid) return validation.response
// Update row
const now = new Date()
const [updatedRow] = await db
@@ -257,22 +208,7 @@ export async function PATCH(request: NextRequest, { params }: RowRouteParams) {
}
}
/**
* DELETE /api/table/[tableId]/rows/[rowId]
*
* Permanently deletes a single row.
*
* @param request - The incoming HTTP request
* @param context - Route context containing tableId and rowId params
* @returns JSON response confirming deletion or error
*
* @example Request body:
* ```json
* {
* "workspaceId": "ws_123"
* }
* ```
*/
/** DELETE /api/table/[tableId]/rows/[rowId] - Deletes a single row. */
export async function DELETE(request: NextRequest, { params }: RowRouteParams) {
const requestId = generateRequestId()
const { tableId, rowId } = await params
@@ -286,11 +222,9 @@ export async function DELETE(request: NextRequest, { params }: RowRouteParams) {
const body: unknown = await request.json()
const validated = DeleteRowSchema.parse(body)
// Check table write access
const accessResult = await checkAccessOrRespond(tableId, authResult.userId, requestId, 'write')
if (accessResult instanceof NextResponse) return accessResult
// Security check: verify workspaceId matches the table's workspace
const actualWorkspaceId = validated.workspaceId
const isValidWorkspace = await verifyTableWorkspace(tableId, validated.workspaceId)
if (!isValidWorkspace) {
@@ -300,7 +234,6 @@ export async function DELETE(request: NextRequest, { params }: RowRouteParams) {
return NextResponse.json({ error: 'Invalid workspace ID' }, { status: 400 })
}
// Delete row
const [deletedRow] = await db
.delete(userTableRows)
.where(

View File

@@ -21,17 +21,11 @@ import { checkAccessOrRespond, checkAccessWithFullTable, verifyTableWorkspace }
const logger = createLogger('TableRowsAPI')
/**
* Zod schema for inserting a single row into a table.
*
* The workspaceId is required and validated against the table.
*/
const InsertRowSchema = z.object({
workspaceId: z.string().min(1, 'Workspace ID is required'),
data: z.record(z.unknown(), { required_error: 'Row data is required' }),
})
/** Zod schema for batch inserting multiple rows (max 1000 per batch) */
const BatchInsertRowsSchema = z.object({
workspaceId: z.string().min(1, 'Workspace ID is required'),
rows: z
@@ -40,9 +34,6 @@ const BatchInsertRowsSchema = z.object({
.max(1000, 'Cannot insert more than 1000 rows per batch'),
})
/**
* Zod schema for querying rows with filtering, sorting, and pagination.
*/
const QueryRowsSchema = z.object({
workspaceId: z.string().min(1, 'Workspace ID is required'),
filter: z.record(z.unknown()).optional(),
@@ -62,7 +53,6 @@ const QueryRowsSchema = z.object({
.default(0),
})
/** Zod schema for updating multiple rows by filter (max 1000 per operation) */
const UpdateRowsByFilterSchema = z.object({
workspaceId: z.string().min(1, 'Workspace ID is required'),
filter: z.record(z.unknown(), { required_error: 'Filter criteria is required' }),
@@ -75,7 +65,6 @@ const UpdateRowsByFilterSchema = z.object({
.optional(),
})
/** Zod schema for deleting multiple rows by filter (max 1000 per operation) */
const DeleteRowsByFilterSchema = z.object({
workspaceId: z.string().min(1, 'Workspace ID is required'),
filter: z.record(z.unknown(), { required_error: 'Filter criteria is required' }),
@@ -87,24 +76,10 @@ const DeleteRowsByFilterSchema = z.object({
.optional(),
})
/**
* Route params for table row endpoints.
*/
interface TableRowsRouteParams {
params: Promise<{ tableId: string }>
}
/**
* Handles batch insertion of multiple rows into a table.
*
* @param requestId - Request tracking ID for logging
* @param tableId - ID of the target table
* @param body - Validated batch insert request body
* @param userId - ID of the authenticated user
* @returns NextResponse with inserted rows or error
*
* @internal
*/
async function handleBatchInsert(
requestId: string,
tableId: string,
@@ -113,13 +88,11 @@ async function handleBatchInsert(
): Promise<NextResponse> {
const validated = BatchInsertRowsSchema.parse(body)
// Check table write access and get full table data in one query
const accessResult = await checkAccessWithFullTable(tableId, userId, requestId, 'write')
if (accessResult instanceof NextResponse) return accessResult
const table = accessResult.table
// Security check: verify workspaceId matches the table's workspace
if (validated.workspaceId !== table.workspaceId) {
logger.warn(
`[${requestId}] Workspace ID mismatch for table ${tableId}. Provided: ${validated.workspaceId}, Actual: ${table.workspaceId}`
@@ -129,7 +102,6 @@ async function handleBatchInsert(
const workspaceId = validated.workspaceId
// Check row count limit
const remainingCapacity = table.maxRows - table.rowCount
if (remainingCapacity < validated.rows.length) {
return NextResponse.json(
@@ -140,7 +112,6 @@ async function handleBatchInsert(
)
}
// Validate all rows (size, schema, unique constraints)
const validation = await validateBatchRows({
rows: validated.rows as RowData[],
schema: table.schema as TableSchema,
@@ -148,7 +119,6 @@ async function handleBatchInsert(
})
if (!validation.valid) return validation.response
// Insert all rows
const now = new Date()
const rowsToInsert = validated.rows.map((data) => ({
id: `row_${crypto.randomUUID().replace(/-/g, '')}`,
@@ -179,35 +149,7 @@ async function handleBatchInsert(
})
}
/**
* POST /api/table/[tableId]/rows
*
* Inserts a new row into the table.
* Supports both single row and batch insert (when `rows` array is provided).
*
* @param request - The incoming HTTP request
* @param context - Route context containing tableId param
* @returns JSON response with inserted row(s) or error
*
* @example Single row insert:
* ```json
* {
* "workspaceId": "ws_123",
* "data": { "name": "John", "email": "john@example.com" }
* }
* ```
*
* @example Batch insert:
* ```json
* {
* "workspaceId": "ws_123",
* "rows": [
* { "name": "John", "email": "john@example.com" },
* { "name": "Jane", "email": "jane@example.com" }
* ]
* }
* ```
*/
/** POST /api/table/[tableId]/rows - Inserts row(s). Supports single or batch insert. */
export async function POST(request: NextRequest, { params }: TableRowsRouteParams) {
const requestId = generateRequestId()
const { tableId } = await params
@@ -220,7 +162,6 @@ export async function POST(request: NextRequest, { params }: TableRowsRouteParam
const body: unknown = await request.json()
// Check if this is a batch insert
if (
typeof body === 'object' &&
body !== null &&
@@ -235,10 +176,8 @@ export async function POST(request: NextRequest, { params }: TableRowsRouteParam
)
}
// Single row insert
const validated = InsertRowSchema.parse(body)
// Check table write access and get full table data in one query
const accessResult = await checkAccessWithFullTable(
tableId,
authResult.userId,
@@ -249,7 +188,6 @@ export async function POST(request: NextRequest, { params }: TableRowsRouteParam
const table = accessResult.table
// Security check: verify workspaceId matches the table's workspace
if (validated.workspaceId !== table.workspaceId) {
logger.warn(
`[${requestId}] Workspace ID mismatch for table ${tableId}. Provided: ${validated.workspaceId}, Actual: ${table.workspaceId}`
@@ -260,7 +198,6 @@ export async function POST(request: NextRequest, { params }: TableRowsRouteParam
const workspaceId = validated.workspaceId
const rowData = validated.data as RowData
// Validate row data (size, schema, unique constraints)
const validation = await validateRowData({
rowData,
schema: table.schema as TableSchema,
@@ -268,7 +205,6 @@ export async function POST(request: NextRequest, { params }: TableRowsRouteParam
})
if (!validation.valid) return validation.response
// Check row count limit
if (table.rowCount >= table.maxRows) {
return NextResponse.json(
{ error: `Table row limit reached (${table.maxRows} rows max)` },
@@ -276,7 +212,6 @@ export async function POST(request: NextRequest, { params }: TableRowsRouteParam
)
}
// Insert row
const rowId = `row_${crypto.randomUUID().replace(/-/g, '')}`
const now = new Date()
@@ -320,20 +255,7 @@ export async function POST(request: NextRequest, { params }: TableRowsRouteParam
}
}
/**
* GET /api/table/[tableId]/rows?workspaceId=xxx&filter=...&sort=...&limit=100&offset=0
*
* Queries rows from the table with filtering, sorting, and pagination.
*
* @param request - The incoming HTTP request with query params
* @param context - Route context containing tableId param
* @returns JSON response with matching rows and pagination info
*
* @example Query with filter:
* ```
* GET /api/table/tbl_123/rows?filter={"status":{"eq":"active"}}&limit=50&offset=0
* ```
*/
/** GET /api/table/[tableId]/rows - Queries rows with filtering, sorting, and pagination. */
export async function GET(request: NextRequest, { params }: TableRowsRouteParams) {
const requestId = generateRequestId()
const { tableId } = await params
@@ -373,7 +295,6 @@ export async function GET(request: NextRequest, { params }: TableRowsRouteParams
offset,
})
// Check table access with full table data (includes schema for type-aware sorting)
const accessResult = await checkAccessWithFullTable(
tableId,
authResult.userId,
@@ -384,7 +305,6 @@ export async function GET(request: NextRequest, { params }: TableRowsRouteParams
const { table } = accessResult
// Security check: verify workspaceId matches the table's workspace
const isValidWorkspace = await verifyTableWorkspace(tableId, validated.workspaceId)
if (!isValidWorkspace) {
logger.warn(
@@ -393,13 +313,11 @@ export async function GET(request: NextRequest, { params }: TableRowsRouteParams
return NextResponse.json({ error: 'Invalid workspace ID' }, { status: 400 })
}
// Build base where conditions
const baseConditions = [
eq(userTableRows.tableId, tableId),
eq(userTableRows.workspaceId, validated.workspaceId),
]
// Add filter conditions if provided
if (validated.filter) {
const filterClause = buildFilterClause(validated.filter as Filter, 'user_table_rows')
if (filterClause) {
@@ -407,7 +325,6 @@ export async function GET(request: NextRequest, { params }: TableRowsRouteParams
}
}
// Build query with combined conditions
let query = db
.select({
id: userTableRows.id,
@@ -418,7 +335,6 @@ export async function GET(request: NextRequest, { params }: TableRowsRouteParams
.from(userTableRows)
.where(and(...baseConditions))
// Apply sorting with type-aware column handling
if (validated.sort) {
const schema = table.schema as TableSchema
const sortClause = buildSortClause(validated.sort, 'user_table_rows', schema.columns)
@@ -429,7 +345,6 @@ export async function GET(request: NextRequest, { params }: TableRowsRouteParams
query = query.orderBy(userTableRows.createdAt) as typeof query
}
// Get total count with same filters (without pagination)
const countQuery = db
.select({ count: sql<number>`count(*)` })
.from(userTableRows)
@@ -437,7 +352,6 @@ export async function GET(request: NextRequest, { params }: TableRowsRouteParams
const [{ count: totalCount }] = await countQuery
// Apply pagination
const rows = await query.limit(validated.limit).offset(validated.offset)
logger.info(
@@ -472,23 +386,7 @@ export async function GET(request: NextRequest, { params }: TableRowsRouteParams
}
}
/**
* PUT /api/table/[tableId]/rows
*
* Updates multiple rows matching filter criteria.
*
* @param request - The incoming HTTP request with filter and update data
* @param context - Route context containing tableId param
* @returns JSON response with count of updated rows
*
* @example Update all rows where status is "pending":
* ```json
* {
* "filter": { "status": { "eq": "pending" } },
* "data": { "status": "processed" }
* }
* ```
*/
/** PUT /api/table/[tableId]/rows - Updates rows matching filter criteria. */
export async function PUT(request: NextRequest, { params }: TableRowsRouteParams) {
const requestId = generateRequestId()
const { tableId } = await params
@@ -502,7 +400,6 @@ export async function PUT(request: NextRequest, { params }: TableRowsRouteParams
const body: unknown = await request.json()
const validated = UpdateRowsByFilterSchema.parse(body)
// Check table write access and get full table data in one query
const accessResult = await checkAccessWithFullTable(
tableId,
authResult.userId,
@@ -513,7 +410,6 @@ export async function PUT(request: NextRequest, { params }: TableRowsRouteParams
const table = accessResult.table
// Security check: verify workspaceId matches the table's workspace
if (validated.workspaceId !== table.workspaceId) {
logger.warn(
`[${requestId}] Workspace ID mismatch for table ${tableId}. Provided: ${validated.workspaceId}, Actual: ${table.workspaceId}`
@@ -523,7 +419,6 @@ export async function PUT(request: NextRequest, { params }: TableRowsRouteParams
const updateData = validated.data as RowData
// Validate new data size
const sizeValidation = validateRowSize(updateData)
if (!sizeValidation.valid) {
return NextResponse.json(
@@ -532,19 +427,16 @@ export async function PUT(request: NextRequest, { params }: TableRowsRouteParams
)
}
// Build base where conditions
const baseConditions = [
eq(userTableRows.tableId, tableId),
eq(userTableRows.workspaceId, validated.workspaceId),
]
// Add filter conditions
const filterClause = buildFilterClause(validated.filter as Filter, 'user_table_rows')
if (filterClause) {
baseConditions.push(filterClause)
}
// First, get the rows that match the filter to validate against schema
let matchingRowsQuery = db
.select({
id: userTableRows.id,
@@ -572,12 +464,10 @@ export async function PUT(request: NextRequest, { params }: TableRowsRouteParams
)
}
// Log warning for large operations but allow them
if (matchingRows.length > TABLE_LIMITS.MAX_BULK_OPERATION_SIZE) {
logger.warn(`[${requestId}] Updating ${matchingRows.length} rows. This may take some time.`)
}
// Validate that merged data matches schema for each row
for (const row of matchingRows) {
const existingData = row.data as RowData
const mergedData = { ...existingData, ...updateData }
@@ -594,10 +484,8 @@ export async function PUT(request: NextRequest, { params }: TableRowsRouteParams
}
}
// Check unique constraints if any unique columns exist
const uniqueColumns = getUniqueColumns(table.schema as TableSchema)
if (uniqueColumns.length > 0) {
// Fetch all rows (not just matching ones) to check for uniqueness
const allRows = await db
.select({
id: userTableRows.id,
@@ -606,7 +494,6 @@ export async function PUT(request: NextRequest, { params }: TableRowsRouteParams
.from(userTableRows)
.where(eq(userTableRows.tableId, tableId))
// Validate each updated row for unique constraints
for (const row of matchingRows) {
const existingData = row.data as RowData
const mergedData = { ...existingData, ...updateData }
@@ -614,7 +501,7 @@ export async function PUT(request: NextRequest, { params }: TableRowsRouteParams
mergedData,
table.schema as TableSchema,
allRows.map((r) => ({ id: r.id, data: r.data as RowData })),
row.id // Exclude the current row being updated
row.id
)
if (!uniqueValidation.valid) {
@@ -630,13 +517,11 @@ export async function PUT(request: NextRequest, { params }: TableRowsRouteParams
}
}
// Update rows by merging existing data with new data in a transaction
const now = new Date()
await db.transaction(async (trx) => {
let totalUpdated = 0
// Process updates in batches
for (let i = 0; i < matchingRows.length; i += TABLE_LIMITS.UPDATE_BATCH_SIZE) {
const batch = matchingRows.slice(i, i + TABLE_LIMITS.UPDATE_BATCH_SIZE)
const updatePromises = batch.map((row) => {
@@ -684,22 +569,7 @@ export async function PUT(request: NextRequest, { params }: TableRowsRouteParams
}
}
/**
* DELETE /api/table/[tableId]/rows
*
* Deletes multiple rows matching filter criteria.
*
* @param request - The incoming HTTP request with filter criteria
* @param context - Route context containing tableId param
* @returns JSON response with count of deleted rows
*
* @example Delete all rows where seen is false:
* ```json
* {
* "filter": { "seen": { "eq": false } }
* }
* ```
*/
/** DELETE /api/table/[tableId]/rows - Deletes rows matching filter criteria. */
export async function DELETE(request: NextRequest, { params }: TableRowsRouteParams) {
const requestId = generateRequestId()
const { tableId } = await params
@@ -713,11 +583,9 @@ export async function DELETE(request: NextRequest, { params }: TableRowsRoutePar
const body: unknown = await request.json()
const validated = DeleteRowsByFilterSchema.parse(body)
// Check table write access
const accessResult = await checkAccessOrRespond(tableId, authResult.userId, requestId, 'write')
if (accessResult instanceof NextResponse) return accessResult
// Security check: verify workspaceId matches the table's workspace
const isValidWorkspace = await verifyTableWorkspace(tableId, validated.workspaceId)
if (!isValidWorkspace) {
logger.warn(
@@ -726,19 +594,16 @@ export async function DELETE(request: NextRequest, { params }: TableRowsRoutePar
return NextResponse.json({ error: 'Invalid workspace ID' }, { status: 400 })
}
// Build base where conditions
const baseConditions = [
eq(userTableRows.tableId, tableId),
eq(userTableRows.workspaceId, validated.workspaceId),
]
// Add filter conditions
const filterClause = buildFilterClause(validated.filter as Filter, 'user_table_rows')
if (filterClause) {
baseConditions.push(filterClause)
}
// Get matching rows first (for reporting and limit enforcement)
let matchingRowsQuery = db
.select({ id: userTableRows.id })
.from(userTableRows)
@@ -763,18 +628,15 @@ export async function DELETE(request: NextRequest, { params }: TableRowsRoutePar
)
}
// Log warning for large operations but allow them
if (matchingRows.length > TABLE_LIMITS.DELETE_BATCH_SIZE) {
logger.warn(`[${requestId}] Deleting ${matchingRows.length} rows. This may take some time.`)
}
// Delete the matching rows in a transaction to ensure atomicity
const rowIds = matchingRows.map((r) => r.id)
await db.transaction(async (trx) => {
let totalDeleted = 0
// Delete rows in batches to avoid stack overflow
for (let i = 0; i < rowIds.length; i += TABLE_LIMITS.DELETE_BATCH_SIZE) {
const batch = rowIds.slice(i, i + TABLE_LIMITS.DELETE_BATCH_SIZE)
await trx.delete(userTableRows).where(

View File

@@ -12,25 +12,16 @@ import { checkAccessOrRespond, getTableById, verifyTableWorkspace } from '../../
const logger = createLogger('TableUpsertAPI')
/** Zod schema for upsert requests - inserts new row or updates if unique fields match */
const UpsertRowSchema = z.object({
workspaceId: z.string().min(1, 'Workspace ID is required'),
data: z.record(z.unknown(), { required_error: 'Row data is required' }),
})
/**
* Route params for upsert endpoint.
*/
interface UpsertRouteParams {
params: Promise<{ tableId: string }>
}
/**
* POST /api/table/[tableId]/rows/upsert
*
* Inserts or updates a row based on unique column constraints.
* Requires at least one unique column in the table schema.
*/
/** POST /api/table/[tableId]/rows/upsert - Inserts or updates based on unique columns. */
export async function POST(request: NextRequest, { params }: UpsertRouteParams) {
const requestId = generateRequestId()
const { tableId } = await params
@@ -44,11 +35,9 @@ export async function POST(request: NextRequest, { params }: UpsertRouteParams)
const body: unknown = await request.json()
const validated = UpsertRowSchema.parse(body)
// Check table write access
const accessResult = await checkAccessOrRespond(tableId, authResult.userId, requestId, 'write')
if (accessResult instanceof NextResponse) return accessResult
// Security check: If workspaceId is provided, verify it matches the table's workspace
const actualWorkspaceId = validated.workspaceId || accessResult.table.workspaceId
if (validated.workspaceId) {
const isValidWorkspace = await verifyTableWorkspace(tableId, validated.workspaceId)
@@ -60,7 +49,6 @@ export async function POST(request: NextRequest, { params }: UpsertRouteParams)
}
}
// Get table definition
const table = await getTableById(tableId)
if (!table) {
return NextResponse.json({ error: 'Table not found' }, { status: 404 })
@@ -69,16 +57,14 @@ export async function POST(request: NextRequest, { params }: UpsertRouteParams)
const schema = table.schema as TableSchema
const rowData = validated.data as RowData
// Validate row data (size and schema only - unique constraints handled by upsert logic)
const validation = await validateRowData({
rowData,
schema,
tableId,
checkUnique: false, // Upsert uses unique columns differently - to find existing rows
checkUnique: false,
})
if (!validation.valid) return validation.response
// Get unique columns for upsert matching
const uniqueColumns = getUniqueColumns(schema)
if (uniqueColumns.length === 0) {
@@ -91,7 +77,6 @@ export async function POST(request: NextRequest, { params }: UpsertRouteParams)
)
}
// Build filter to find existing row by unique fields
const uniqueFilters = uniqueColumns.map((col) => {
const value = rowData[col.name]
if (value === undefined || value === null) {
@@ -100,7 +85,6 @@ export async function POST(request: NextRequest, { params }: UpsertRouteParams)
return sql`${userTableRows.data}->>${col.name} = ${String(value)}`
})
// Filter out null conditions (for optional unique fields that weren't provided)
const validUniqueFilters = uniqueFilters.filter((f): f is Exclude<typeof f, null> => f !== null)
if (validUniqueFilters.length === 0) {
@@ -112,7 +96,6 @@ export async function POST(request: NextRequest, { params }: UpsertRouteParams)
)
}
// Find existing row with matching unique field(s)
const [existingRow] = await db
.select()
.from(userTableRows)
@@ -127,10 +110,8 @@ export async function POST(request: NextRequest, { params }: UpsertRouteParams)
const now = new Date()
// Perform upsert in a transaction to ensure atomicity
const result = await db.transaction(async (trx) => {
if (existingRow) {
// Update existing row
const [updatedRow] = await trx
.update(userTableRows)
.set({
@@ -146,7 +127,6 @@ export async function POST(request: NextRequest, { params }: UpsertRouteParams)
}
}
// Insert new row
const [insertedRow] = await trx
.insert(userTableRows)
.values({

View File

@@ -11,11 +11,6 @@ import { normalizeColumn } from './utils'
const logger = createLogger('TableAPI')
/**
* Zod schema for validating a table column definition.
*
* Columns must have a name, type, and optional required/unique flags.
*/
const ColumnSchema = z.object({
name: z
.string()
@@ -37,11 +32,6 @@ const ColumnSchema = z.object({
unique: z.boolean().optional().default(false),
})
/**
* Zod schema for validating create table requests.
*
* Requires a name, schema with columns, and workspace ID.
*/
const CreateTableSchema = z.object({
name: z
.string()
@@ -73,30 +63,15 @@ const CreateTableSchema = z.object({
workspaceId: z.string().min(1, 'Workspace ID is required'),
})
/**
* Zod schema for validating list tables requests.
*/
const ListTablesSchema = z.object({
workspaceId: z.string().min(1, 'Workspace ID is required'),
})
/**
* Result of a workspace access check.
*/
interface WorkspaceAccessResult {
/** Whether the user has any access to the workspace */
hasAccess: boolean
/** Whether the user can write (modify tables) in the workspace */
canWrite: boolean
}
/**
* Checks if a user has access to a workspace and determines their permission level.
*
* @param workspaceId - The workspace to check access for
* @param userId - The user requesting access
* @returns Access result with read and write permissions
*/
async function checkWorkspaceAccess(
workspaceId: string,
userId: string
@@ -114,12 +89,10 @@ async function checkWorkspaceAccess(
return { hasAccess: false, canWrite: false }
}
// Owner has full access
if (workspaceData.ownerId === userId) {
return { hasAccess: true, canWrite: true }
}
// Check permissions
const [permission] = await db
.select({
permissionType: permissions.permissionType,
@@ -146,29 +119,7 @@ async function checkWorkspaceAccess(
}
}
/**
* POST /api/table
*
* Creates a new user-defined table in a workspace.
*
* @param request - The incoming HTTP request containing table definition
* @returns JSON response with the created table or error
*
* @example Request body:
* ```json
* {
* "name": "customers",
* "description": "Customer records",
* "workspaceId": "ws_123",
* "schema": {
* "columns": [
* { "name": "email", "type": "string", "required": true, "unique": true },
* { "name": "name", "type": "string", "required": true }
* ]
* }
* }
* ```
*/
/** POST /api/table - Creates a new user-defined table. */
export async function POST(request: NextRequest) {
const requestId = generateRequestId()
@@ -181,7 +132,6 @@ export async function POST(request: NextRequest) {
const body: unknown = await request.json()
const params = CreateTableSchema.parse(body)
// Check workspace access
const { hasAccess, canWrite } = await checkWorkspaceAccess(
params.workspaceId,
authResult.userId
@@ -191,12 +141,10 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ error: 'Access denied' }, { status: 403 })
}
// Normalize schema to ensure all fields have explicit defaults
const normalizedSchema: TableSchema = {
columns: params.schema.columns.map(normalizeColumn),
}
// Create table using service layer
const table = await createTable(
{
name: params.name,
@@ -238,7 +186,6 @@ export async function POST(request: NextRequest) {
)
}
// Handle service layer errors with specific messages
if (error instanceof Error) {
if (
error.message.includes('Invalid table name') ||
@@ -255,14 +202,7 @@ export async function POST(request: NextRequest) {
}
}
/**
* GET /api/table?workspaceId=xxx
*
* Lists all tables in a workspace.
*
* @param request - The incoming HTTP request with workspaceId query param
* @returns JSON response with array of tables or error
*/
/** GET /api/table - Lists all tables in a workspace. */
export async function GET(request: NextRequest) {
const requestId = generateRequestId()
@@ -285,14 +225,12 @@ export async function GET(request: NextRequest) {
const params = validation.data
// Check workspace access
const { hasAccess } = await checkWorkspaceAccess(params.workspaceId, authResult.userId)
if (!hasAccess) {
return NextResponse.json({ error: 'Access denied' }, { status: 403 })
}
// Get tables using service layer
const tables = await listTables(params.workspaceId)
logger.info(`[${requestId}] Listed ${tables.length} tables in workspace ${params.workspaceId}`)

View File

@@ -10,9 +10,7 @@ const logger = createLogger('TableUtils')
type PermissionLevel = 'read' | 'write' | 'admin'
/**
* @deprecated Use TableDefinition from '@/lib/table' instead
*/
/** @deprecated Use TableDefinition from '@/lib/table' instead */
export type TableData = TableDefinition
export interface TableAccessResult {

View File

@@ -9,13 +9,6 @@ interface TableActionBarProps {
onClearSelection: () => void
}
/**
* Action bar displayed when rows are selected in the table.
*
* Shows the count of selected rows and provides actions for
* bulk operations like deletion.
*
*/
export function TableActionBar({ selectedCount, onDelete, onClearSelection }: TableActionBarProps) {
return (
<div className='flex h-[36px] shrink-0 items-center justify-between border-[var(--border)] border-b bg-[var(--surface-4)] px-[16px]'>

View File

@@ -9,7 +9,6 @@ import { useFilterBuilder } from '@/lib/table/filters/use-builder'
import { filterRulesToFilter, sortRuleToSort } from '@/lib/table/filters/utils'
import type { ColumnDefinition, Filter, Sort } from '@/lib/table/types'
/** Query result containing API-ready filter and sort objects. */
export interface BuilderQueryResult {
filter: Filter | null
sort: Sort | null
@@ -24,7 +23,6 @@ interface TableQueryBuilderProps {
isLoading?: boolean
}
/** Visual query builder for filtering and sorting table data. */
export function TableQueryBuilder({
columns,
onApply,

View File

@@ -1,7 +1,3 @@
/**
* Modal for viewing cell details.
*/
import { Copy, X } from 'lucide-react'
import { Badge, Button, Modal, ModalBody, ModalContent } from '@/components/emcn'
import type { CellViewerData } from '../types'
@@ -13,12 +9,6 @@ interface CellViewerModalProps {
copied: boolean
}
/**
* Displays cell value details in a modal.
*
* @param props - Component props
* @returns Cell viewer modal or null if no cell is selected
*/
export function CellViewerModal({ cellViewer, onClose, onCopy, copied }: CellViewerModalProps) {
if (!cellViewer) return null

View File

@@ -1,7 +1,3 @@
/**
* Table data viewer sub-components.
*/
export * from './cell-viewer-modal'
export * from './row-context-menu'
export * from './schema-viewer-modal'

View File

@@ -1,7 +1,3 @@
/**
* Context menu for row actions.
*/
import { Edit, Trash2 } from 'lucide-react'
import {
Popover,
@@ -19,12 +15,6 @@ interface RowContextMenuProps {
onDelete: () => void
}
/**
* Renders a context menu for row actions.
*
* @param props - Component props
* @returns Row context menu popover
*/
export function RowContextMenu({ contextMenu, onClose, onEdit, onDelete }: RowContextMenuProps) {
return (
<Popover

View File

@@ -1,7 +1,3 @@
/**
* Modal for viewing table schema.
*/
import { Info, X } from 'lucide-react'
import {
Badge,
@@ -25,12 +21,6 @@ interface SchemaViewerModalProps {
columns: ColumnDefinition[]
}
/**
* Displays the table schema in a modal.
*
* @param props - Component props
* @returns Schema viewer modal
*/
export function SchemaViewerModal({ isOpen, onClose, columns }: SchemaViewerModalProps) {
return (
<Modal open={isOpen} onOpenChange={onClose}>

View File

@@ -1,7 +1,3 @@
/**
* Table body placeholder states (loading and empty).
*/
import { Plus } from 'lucide-react'
import { Button, TableCell, TableRow } from '@/components/emcn'
import { Skeleton } from '@/components/ui/skeleton'
@@ -11,9 +7,6 @@ interface TableLoadingRowsProps {
columns: ColumnDefinition[]
}
/**
* Renders skeleton rows while table data is loading.
*/
export function TableLoadingRows({ columns }: TableLoadingRowsProps) {
return (
<>
@@ -56,9 +49,6 @@ interface TableEmptyRowsProps {
onAddRow: () => void
}
/**
* Renders an empty state when no rows are present.
*/
export function TableEmptyRows({ columnCount, hasFilter, onAddRow }: TableEmptyRowsProps) {
return (
<TableRow>

View File

@@ -1,7 +1,3 @@
/**
* Cell value renderer for different column types.
*/
import type { ColumnDefinition } from '@/lib/table'
import { STRING_TRUNCATE_LENGTH } from '../constants'
import type { CellViewerData } from '../types'
@@ -12,12 +8,6 @@ interface TableCellRendererProps {
onCellClick: (columnName: string, value: unknown, type: CellViewerData['type']) => void
}
/**
* Renders a cell value with appropriate formatting based on column type.
*
* @param props - Component props
* @returns Formatted cell content
*/
export function TableCellRenderer({ value, column, onCellClick }: TableCellRendererProps) {
const isNull = value === null || value === undefined

View File

@@ -1,7 +1,3 @@
/**
* Header bar for the table data viewer.
*/
import { Info, RefreshCw } from 'lucide-react'
import { Badge, Button, Tooltip } from '@/components/emcn'
import { Skeleton } from '@/components/ui/skeleton'
@@ -15,12 +11,6 @@ interface TableHeaderBarProps {
onRefresh: () => void
}
/**
* Renders the header bar with navigation, title, and actions.
*
* @param props - Component props
* @returns Table header bar
*/
export function TableHeaderBar({
tableName,
totalCount,

View File

@@ -1,7 +1,3 @@
/**
* Pagination controls for the table.
*/
import { Button } from '@/components/emcn'
interface TablePaginationProps {
@@ -12,12 +8,6 @@ interface TablePaginationProps {
onNextPage: () => void
}
/**
* Renders pagination controls for navigating table pages.
*
* @param props - Component props
* @returns Table pagination controls or null if only one page
*/
export function TablePagination({
currentPage,
totalPages,

View File

@@ -1,5 +1,2 @@
/** Number of rows to fetch per page */
export const ROWS_PER_PAGE = 100
/** Maximum length for string display before truncation */
export const STRING_TRUNCATE_LENGTH = 50

View File

@@ -1,7 +1,3 @@
/**
* Custom hooks for the table data viewer.
*/
export * from './use-context-menu'
export * from './use-row-selection'
export * from './use-table-data'

View File

@@ -8,11 +8,6 @@ interface UseContextMenuReturn {
closeContextMenu: () => void
}
/**
* Manages context menu state for row interactions.
*
* @returns Context menu state and handlers
*/
export function useContextMenu(): UseContextMenuReturn {
const [contextMenu, setContextMenu] = useState<ContextMenuState>({
isOpen: false,

View File

@@ -1,7 +1,3 @@
/**
* Hook for managing row selection state.
*/
import { useCallback, useEffect, useState } from 'react'
import type { TableRow } from '@/lib/table'
@@ -12,19 +8,9 @@ interface UseRowSelectionReturn {
clearSelection: () => void
}
/**
* Manages row selection state and provides selection handlers.
*
* @param rows - The current rows to select from
* @returns Selection state and handlers
*/
export function useRowSelection(rows: TableRow[]): UseRowSelectionReturn {
const [selectedRows, setSelectedRows] = useState<Set<string>>(new Set())
/**
* Filter out selected rows that are no longer in the current row set.
* This handles pagination, filtering, and data refresh scenarios.
*/
useEffect(() => {
setSelectedRows((prev) => {
if (prev.size === 0) return prev
@@ -37,9 +23,6 @@ export function useRowSelection(rows: TableRow[]): UseRowSelectionReturn {
})
}, [rows])
/**
* Toggles selection of all visible rows.
*/
const handleSelectAll = useCallback(() => {
if (selectedRows.size === rows.length) {
setSelectedRows(new Set())
@@ -48,9 +31,6 @@ export function useRowSelection(rows: TableRow[]): UseRowSelectionReturn {
}
}, [rows, selectedRows.size])
/**
* Toggles selection of a single row.
*/
const handleSelectRow = useCallback((rowId: string) => {
setSelectedRows((prev) => {
const newSet = new Set(prev)
@@ -63,9 +43,6 @@ export function useRowSelection(rows: TableRow[]): UseRowSelectionReturn {
})
}, [])
/**
* Clears all selections.
*/
const clearSelection = useCallback(() => {
setSelectedRows(new Set())
}, [])

View File

@@ -20,7 +20,6 @@ interface UseTableDataReturn {
refetchRows: () => void
}
/** Fetches table metadata and rows with filtering/sorting/pagination. */
export function useTableData({
workspaceId,
tableId,

View File

@@ -28,7 +28,6 @@ import {
import { useContextMenu, useRowSelection, useTableData } from './hooks'
import type { CellViewerData } from './types'
/** Table data viewer with filtering, sorting, pagination, and CRUD operations. */
export function TableDataViewer() {
const params = useParams()
const router = useRouter()
@@ -67,30 +66,18 @@ export function TableDataViewer() {
const hasSelection = selectedCount > 0
const isAllSelected = rows.length > 0 && selectedCount === rows.length
/**
* Navigates back to the tables list.
*/
const handleNavigateBack = useCallback(() => {
router.push(`/workspace/${workspaceId}/tables`)
}, [router, workspaceId])
/**
* Opens the schema viewer modal.
*/
const handleShowSchema = useCallback(() => {
setShowSchemaModal(true)
}, [])
/**
* Opens the add row modal.
*/
const handleAddRow = useCallback(() => {
setShowAddModal(true)
}, [])
/**
* Applies new query options and resets pagination.
*/
const handleApplyQueryOptions = useCallback(
(options: QueryOptions) => {
setQueryOptions(options)
@@ -100,16 +87,10 @@ export function TableDataViewer() {
[refetchRows]
)
/**
* Opens the delete modal for selected rows.
*/
const handleDeleteSelected = useCallback(() => {
setDeletingRows(Array.from(selectedRows))
}, [selectedRows])
/**
* Handles edit action from context menu.
*/
const handleContextMenuEdit = useCallback(() => {
if (contextMenu.row) {
setEditingRow(contextMenu.row)
@@ -117,9 +98,6 @@ export function TableDataViewer() {
closeContextMenu()
}, [contextMenu.row, closeContextMenu])
/**
* Handles delete action from context menu.
*/
const handleContextMenuDelete = useCallback(() => {
if (contextMenu.row) {
setDeletingRows([contextMenu.row.id])
@@ -127,9 +105,6 @@ export function TableDataViewer() {
closeContextMenu()
}, [contextMenu.row, closeContextMenu])
/**
* Copies the current cell value to clipboard.
*/
const handleCopyCellValue = useCallback(async () => {
if (cellViewer) {
let text: string
@@ -146,9 +121,6 @@ export function TableDataViewer() {
}
}, [cellViewer])
/**
* Opens the cell viewer modal.
*/
const handleCellClick = useCallback(
(columnName: string, value: unknown, type: CellViewerData['type']) => {
setCellViewer({ columnName, value, type })

View File

@@ -1,29 +1,13 @@
/**
* Type definitions for the table data viewer.
*/
import type { TableRow } from '@/lib/table'
/**
* Data for the cell viewer modal.
*/
export interface CellViewerData {
/** Name of the column being viewed */
columnName: string
/** Value being displayed */
value: unknown
/** Display type for formatting */
type: 'json' | 'text' | 'date' | 'boolean' | 'number'
}
/**
* State for the right-click context menu.
*/
export interface ContextMenuState {
/** Whether the menu is visible */
isOpen: boolean
/** Screen position of the menu */
position: { x: number; y: number }
/** Row the menu was opened on */
row: TableRow | null
}

View File

@@ -1,9 +1,3 @@
/**
* Gets the badge variant for a column type.
*
* @param type - The column type
* @returns Badge variant name
*/
export function getTypeBadgeVariant(
type: string
): 'green' | 'blue' | 'purple' | 'orange' | 'teal' | 'gray' {

View File

@@ -37,13 +37,9 @@ const COLUMN_TYPE_OPTIONS: Array<{ value: ColumnDefinition['type']; label: strin
]
interface ColumnWithId extends ColumnDefinition {
/** Stable ID for React key */
id: string
}
/**
* Creates an empty column definition with default values and a stable ID.
*/
function createEmptyColumn(): ColumnWithId {
return { id: nanoid(), name: '', type: 'string', required: true, unique: false }
}
@@ -59,31 +55,16 @@ export function CreateTableModal({ isOpen, onClose }: CreateTableModalProps) {
const createTable = useCreateTable(workspaceId)
/**
* Adds a new empty column to the schema.
*/
const handleAddColumn = () => {
setColumns([...columns, createEmptyColumn()])
}
/**
* Removes a column from the schema by ID.
*
* @param columnId - ID of the column to remove
*/
const handleRemoveColumn = (columnId: string) => {
if (columns.length > 1) {
setColumns(columns.filter((col) => col.id !== columnId))
}
}
/**
* Updates a column field by ID.
*
* @param columnId - ID of the column to update
* @param field - Field name to update
* @param value - New value for the field
*/
const handleColumnChange = (
columnId: string,
field: keyof ColumnDefinition,
@@ -92,9 +73,6 @@ export function CreateTableModal({ isOpen, onClose }: CreateTableModalProps) {
setColumns(columns.map((col) => (col.id === columnId ? { ...col, [field]: value } : col)))
}
/**
* Validates and submits the form to create the table.
*/
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setError(null)
@@ -140,9 +118,6 @@ export function CreateTableModal({ isOpen, onClose }: CreateTableModalProps) {
}
}
/**
* Resets all form fields to their initial state.
*/
const resetForm = () => {
setTableName('')
setDescription('')
@@ -150,9 +125,6 @@ export function CreateTableModal({ isOpen, onClose }: CreateTableModalProps) {
setError(null)
}
/**
* Handles modal close and resets form state.
*/
const handleClose = () => {
resetForm()
onClose()
@@ -282,23 +254,13 @@ export function CreateTableModal({ isOpen, onClose }: CreateTableModalProps) {
)
}
/**
* Props for the ColumnRow component.
*/
interface ColumnRowProps {
/** The column definition with stable ID */
column: ColumnWithId
/** Whether the remove button should be enabled */
isRemovable: boolean
/** Callback when a column field changes */
onChange: (columnId: string, field: keyof ColumnDefinition, value: string | boolean) => void
/** Callback to remove this column */
onRemove: (columnId: string) => void
}
/**
* A single row in the column definition list.
*/
function ColumnRow({ column, isRemovable, onChange, onRemove }: ColumnRowProps) {
return (
<div className='flex items-center gap-[10px]'>

View File

@@ -36,12 +36,6 @@ interface TableCardProps {
workspaceId: string
}
/**
* Card component for displaying a table summary.
*
* Shows table name, column/row counts, description, and provides
* actions for viewing schema and deleting the table.
*/
export function TableCard({ table, workspaceId }: TableCardProps) {
const router = useRouter()
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false)

View File

@@ -1,6 +1,3 @@
/**
* Tables layout - applies sidebar padding for all table routes.
*/
export default function TablesLayout({ children }: { children: React.ReactNode }) {
return (
<div className='flex h-full flex-1 flex-col overflow-hidden pl-[var(--sidebar-width)]'>

View File

@@ -11,20 +11,6 @@ import { useDebounce } from '@/hooks/use-debounce'
import { CreateTableModal } from './components/create-table-modal'
import { TableCard } from './components/table-card'
/**
* Tables page component that displays a list of all tables in a workspace.
*
* @remarks
* This component provides functionality to:
* - View all tables in the workspace
* - Search tables by name or description
* - Create new tables (with write permission)
*
* @example
* ```tsx
* <Tables />
* ```
*/
export function Tables() {
const params = useParams()
const workspaceId = params.workspaceId as string
@@ -118,9 +104,6 @@ export function Tables() {
)
}
/**
* Loading skeleton component for table cards.
*/
function LoadingSkeletons() {
return (
<>
@@ -153,12 +136,6 @@ function LoadingSkeletons() {
)
}
/**
* Error state component displayed when table loading fails.
*
* @param props - Component props
* @param props.error - The error that occurred
*/
function ErrorState({ error }: { error: unknown }) {
return (
<div className='col-span-full flex h-64 items-center justify-center rounded-[4px] bg-[var(--surface-3)] dark:bg-[var(--surface-4)]'>
@@ -172,12 +149,6 @@ function ErrorState({ error }: { error: unknown }) {
)
}
/**
* Empty state component displayed when no tables exist or match the search.
*
* @param props - Component props
* @param props.hasSearchQuery - Whether a search query is active
*/
function EmptyState({ hasSearchQuery }: { hasSearchQuery: boolean }) {
return (
<div className='col-span-full flex h-64 items-center justify-center rounded-[4px] bg-[var(--surface-3)] dark:bg-[var(--surface-4)]'>