mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-22 13:28:04 -05:00
clean comments
This commit is contained in:
@@ -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({
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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}`)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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]'>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
/**
|
||||
* Table data viewer sub-components.
|
||||
*/
|
||||
|
||||
export * from './cell-viewer-modal'
|
||||
export * from './row-context-menu'
|
||||
export * from './schema-viewer-modal'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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())
|
||||
}, [])
|
||||
|
||||
@@ -20,7 +20,6 @@ interface UseTableDataReturn {
|
||||
refetchRows: () => void
|
||||
}
|
||||
|
||||
/** Fetches table metadata and rows with filtering/sorting/pagination. */
|
||||
export function useTableData({
|
||||
workspaceId,
|
||||
tableId,
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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' {
|
||||
|
||||
@@ -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]'>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)]'>
|
||||
|
||||
@@ -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)]'>
|
||||
|
||||
Reference in New Issue
Block a user