mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
simplify comments
This commit is contained in:
@@ -142,15 +142,7 @@ export async function GET(request: NextRequest, { params }: TableRouteParams) {
|
||||
/**
|
||||
* DELETE /api/table/[tableId]?workspaceId=xxx
|
||||
*
|
||||
* Deletes a table.
|
||||
*
|
||||
* @param request - The incoming HTTP request
|
||||
* @param context - Route context containing tableId param
|
||||
* @returns JSON response confirming deletion or error
|
||||
*
|
||||
* @remarks
|
||||
* This performs a hard delete, removing the table and its rows.
|
||||
* The operation requires write access to the table.
|
||||
* Deletes a table and all its rows (hard delete, requires write access).
|
||||
*/
|
||||
export async function DELETE(request: NextRequest, { params }: TableRouteParams) {
|
||||
const requestId = generateRequestId()
|
||||
|
||||
@@ -162,22 +162,7 @@ export async function GET(request: NextRequest, { params }: RowRouteParams) {
|
||||
/**
|
||||
* PATCH /api/table/[tableId]/rows/[rowId]
|
||||
*
|
||||
* Updates an existing row with new data.
|
||||
*
|
||||
* @param request - The incoming HTTP request with update data
|
||||
* @param context - Route context containing tableId and rowId params
|
||||
* @returns JSON response with updated row or error
|
||||
*
|
||||
* @remarks
|
||||
* The entire row data must be provided; this is a full replacement,
|
||||
* not a partial update.
|
||||
*
|
||||
* @example Request body:
|
||||
* ```json
|
||||
* {
|
||||
* "data": { "name": "Jane", "email": "jane@example.com" }
|
||||
* }
|
||||
* ```
|
||||
* Updates an existing row with new data (full replacement, not partial).
|
||||
*/
|
||||
export async function PATCH(request: NextRequest, { params }: RowRouteParams) {
|
||||
const requestId = generateRequestId()
|
||||
|
||||
@@ -36,12 +36,7 @@ const InsertRowSchema = z.object({
|
||||
data: z.record(z.unknown(), { required_error: 'Row data is required' }),
|
||||
})
|
||||
|
||||
/**
|
||||
* Zod schema for batch inserting multiple rows.
|
||||
*
|
||||
* @remarks
|
||||
* Maximum 1000 rows per batch for performance and safety.
|
||||
*/
|
||||
/** 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
|
||||
@@ -72,12 +67,7 @@ const QueryRowsSchema = z.object({
|
||||
.default(0),
|
||||
})
|
||||
|
||||
/**
|
||||
* Zod schema for updating multiple rows by filter criteria.
|
||||
*
|
||||
* @remarks
|
||||
* Maximum 1000 rows can be updated per operation for safety.
|
||||
*/
|
||||
/** 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' }),
|
||||
@@ -90,12 +80,7 @@ const UpdateRowsByFilterSchema = z.object({
|
||||
.optional(),
|
||||
})
|
||||
|
||||
/**
|
||||
* Zod schema for deleting multiple rows by filter criteria.
|
||||
*
|
||||
* @remarks
|
||||
* Maximum 1000 rows can be deleted per operation for safety.
|
||||
*/
|
||||
/** 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' }),
|
||||
@@ -408,7 +393,6 @@ export async function GET(request: NextRequest, { params }: TableRowsRouteParams
|
||||
}
|
||||
|
||||
// Security check: verify workspaceId matches the table's workspace
|
||||
const actualWorkspaceId = validated.workspaceId
|
||||
const isValidWorkspace = await verifyTableWorkspace(tableId, validated.workspaceId)
|
||||
if (!isValidWorkspace) {
|
||||
logger.warn(
|
||||
@@ -420,7 +404,7 @@ export async function GET(request: NextRequest, { params }: TableRowsRouteParams
|
||||
// Build base where conditions
|
||||
const baseConditions = [
|
||||
eq(userTableRows.tableId, tableId),
|
||||
eq(userTableRows.workspaceId, actualWorkspaceId),
|
||||
eq(userTableRows.workspaceId, validated.workspaceId),
|
||||
]
|
||||
|
||||
// Add filter conditions if provided
|
||||
@@ -544,7 +528,6 @@ export async function PUT(request: NextRequest, { params }: TableRowsRouteParams
|
||||
return NextResponse.json({ error: 'Invalid workspace ID' }, { status: 400 })
|
||||
}
|
||||
|
||||
const actualWorkspaceId = validated.workspaceId
|
||||
const updateData = validated.data as RowData
|
||||
|
||||
// Validate new data size
|
||||
@@ -559,7 +542,7 @@ export async function PUT(request: NextRequest, { params }: TableRowsRouteParams
|
||||
// Build base where conditions
|
||||
const baseConditions = [
|
||||
eq(userTableRows.tableId, tableId),
|
||||
eq(userTableRows.workspaceId, actualWorkspaceId),
|
||||
eq(userTableRows.workspaceId, validated.workspaceId),
|
||||
]
|
||||
|
||||
// Add filter conditions
|
||||
@@ -742,7 +725,6 @@ export async function DELETE(request: NextRequest, { params }: TableRowsRoutePar
|
||||
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) {
|
||||
logger.warn(
|
||||
@@ -754,7 +736,7 @@ export async function DELETE(request: NextRequest, { params }: TableRowsRoutePar
|
||||
// Build base where conditions
|
||||
const baseConditions = [
|
||||
eq(userTableRows.tableId, tableId),
|
||||
eq(userTableRows.workspaceId, actualWorkspaceId),
|
||||
eq(userTableRows.workspaceId, validated.workspaceId),
|
||||
]
|
||||
|
||||
// Add filter conditions
|
||||
@@ -805,7 +787,7 @@ export async function DELETE(request: NextRequest, { params }: TableRowsRoutePar
|
||||
await trx.delete(userTableRows).where(
|
||||
and(
|
||||
eq(userTableRows.tableId, tableId),
|
||||
eq(userTableRows.workspaceId, actualWorkspaceId),
|
||||
eq(userTableRows.workspaceId, validated.workspaceId),
|
||||
sql`${userTableRows.id} = ANY(ARRAY[${sql.join(
|
||||
batch.map((id) => sql`${id}`),
|
||||
sql`, `
|
||||
|
||||
@@ -12,16 +12,7 @@ import { checkAccessOrRespond, getTableById, verifyTableWorkspace } from '../../
|
||||
|
||||
const logger = createLogger('TableUpsertAPI')
|
||||
|
||||
/**
|
||||
* Zod schema for validating upsert (insert or update) requests.
|
||||
*
|
||||
* If a row with matching unique field(s) exists, it will be updated.
|
||||
* Otherwise, a new row will be inserted.
|
||||
*
|
||||
* @remarks
|
||||
* The workspaceId is optional for backward compatibility but
|
||||
* is validated via table access checks when provided.
|
||||
*/
|
||||
/** 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' }),
|
||||
@@ -38,49 +29,7 @@ interface UpsertRouteParams {
|
||||
* POST /api/table/[tableId]/rows/upsert
|
||||
*
|
||||
* Inserts or updates a row based on unique column constraints.
|
||||
* If a row with matching unique field(s) exists, it will be updated;
|
||||
* otherwise, a new row will be inserted.
|
||||
*
|
||||
* @param request - The incoming HTTP request with row data
|
||||
* @param context - Route context containing tableId param
|
||||
* @returns JSON response with upserted row and operation type
|
||||
*
|
||||
* @remarks
|
||||
* Requires at least one unique column in the table schema.
|
||||
* The operation is determined by checking existing rows against
|
||||
* the unique field values provided in the request.
|
||||
*
|
||||
* @example Request body:
|
||||
* ```json
|
||||
* {
|
||||
* "workspaceId": "ws_123",
|
||||
* "data": { "email": "john@example.com", "name": "John Doe" }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @example Response (insert):
|
||||
* ```json
|
||||
* {
|
||||
* "success": true,
|
||||
* "data": {
|
||||
* "row": { ... },
|
||||
* "operation": "insert",
|
||||
* "message": "Row inserted successfully"
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @example Response (update):
|
||||
* ```json
|
||||
* {
|
||||
* "success": true,
|
||||
* "data": {
|
||||
* "row": { ... },
|
||||
* "operation": "update",
|
||||
* "message": "Row updated successfully"
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export async function POST(request: NextRequest, { params }: UpsertRouteParams) {
|
||||
const requestId = generateRequestId()
|
||||
|
||||
@@ -8,6 +8,30 @@ import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
|
||||
|
||||
const logger = createLogger('TableUtils')
|
||||
|
||||
/** Permission hierarchy: read < write < admin */
|
||||
type PermissionLevel = 'read' | 'write' | 'admin'
|
||||
|
||||
/**
|
||||
* Checks if a user's permission meets or exceeds the required level.
|
||||
*/
|
||||
function hasPermissionLevel(
|
||||
userPermission: 'read' | 'write' | 'admin' | null,
|
||||
requiredLevel: PermissionLevel
|
||||
): boolean {
|
||||
if (userPermission === null) return false
|
||||
|
||||
switch (requiredLevel) {
|
||||
case 'read':
|
||||
return true
|
||||
case 'write':
|
||||
return userPermission === 'write' || userPermission === 'admin'
|
||||
case 'admin':
|
||||
return userPermission === 'admin'
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async function getTableRowCount(tableId: string): Promise<number> {
|
||||
const [result] = await db
|
||||
.select({ count: count() })
|
||||
@@ -121,28 +145,7 @@ async function checkTableAccessInternal(
|
||||
// Case 2: Check workspace permissions
|
||||
const userPermission = await getUserEntityPermissions(userId, 'workspace', tableData.workspaceId)
|
||||
|
||||
if (userPermission === null) {
|
||||
return { hasAccess: false }
|
||||
}
|
||||
|
||||
// Determine if user has sufficient permission level
|
||||
const hasAccess = (() => {
|
||||
switch (requiredLevel) {
|
||||
case 'read':
|
||||
// Any permission level grants read access
|
||||
return true
|
||||
case 'write':
|
||||
// Write or admin permission required
|
||||
return userPermission === 'write' || userPermission === 'admin'
|
||||
case 'admin':
|
||||
// Only admin permission grants admin access
|
||||
return userPermission === 'admin'
|
||||
default:
|
||||
return false
|
||||
}
|
||||
})()
|
||||
|
||||
if (hasAccess) {
|
||||
if (hasPermissionLevel(userPermission, requiredLevel)) {
|
||||
return { hasAccess: true, table: tableData }
|
||||
}
|
||||
|
||||
@@ -299,26 +302,7 @@ export async function checkAccessWithFullTable(
|
||||
// Case 2: Check workspace permissions
|
||||
const userPermission = await getUserEntityPermissions(userId, 'workspace', table.workspaceId)
|
||||
|
||||
if (userPermission === null) {
|
||||
logger.warn(`[${requestId}] User ${userId} denied ${level} access to table ${tableId}`)
|
||||
return NextResponse.json({ error: 'Access denied' }, { status: 403 })
|
||||
}
|
||||
|
||||
// Determine if user has sufficient permission level
|
||||
const hasAccess = (() => {
|
||||
switch (level) {
|
||||
case 'read':
|
||||
return true
|
||||
case 'write':
|
||||
return userPermission === 'write' || userPermission === 'admin'
|
||||
case 'admin':
|
||||
return userPermission === 'admin'
|
||||
default:
|
||||
return false
|
||||
}
|
||||
})()
|
||||
|
||||
if (!hasAccess) {
|
||||
if (!hasPermissionLevel(userPermission, level)) {
|
||||
logger.warn(`[${requestId}] User ${userId} denied ${level} access to table ${tableId}`)
|
||||
return NextResponse.json({ error: 'Access denied' }, { status: 403 })
|
||||
}
|
||||
|
||||
@@ -3,6 +3,173 @@ import { conditionsToFilter, sortConditionsToSort } from '@/lib/table/filters/bu
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import type { TableQueryResponse } from '@/tools/table/types'
|
||||
|
||||
/**
|
||||
* Parses a JSON string with helpful error messages.
|
||||
*
|
||||
* Handles common issues like unquoted block references in JSON values.
|
||||
*
|
||||
* @param value - The value to parse (string or already-parsed object)
|
||||
* @param fieldName - Name of the field for error messages
|
||||
* @returns Parsed JSON value
|
||||
* @throws Error with helpful hints if JSON is invalid
|
||||
*/
|
||||
function parseJSON(value: string | unknown, fieldName: string): unknown {
|
||||
if (typeof value !== 'string') return value
|
||||
|
||||
try {
|
||||
return JSON.parse(value)
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : String(error)
|
||||
|
||||
// Check if the error might be due to unquoted string values
|
||||
// This happens when users write {"field": <ref>} instead of {"field": "<ref>"}
|
||||
const unquotedValueMatch = value.match(
|
||||
/:\s*([a-zA-Z][a-zA-Z0-9_\s]*[a-zA-Z0-9]|[a-zA-Z])\s*[,}]/
|
||||
)
|
||||
|
||||
let hint =
|
||||
'Make sure all property names are in double quotes (e.g., {"name": "value"} not {name: "value"}).'
|
||||
|
||||
if (unquotedValueMatch) {
|
||||
hint =
|
||||
'It looks like a string value is not quoted. When using block references in JSON, wrap them in double quotes: {"field": "<blockName.output>"} not {"field": <blockName.output>}.'
|
||||
}
|
||||
|
||||
throw new Error(`Invalid JSON in ${fieldName}: ${errorMsg}. ${hint}`)
|
||||
}
|
||||
}
|
||||
|
||||
/** Raw params from block UI before JSON parsing and type conversion */
|
||||
interface TableBlockParams {
|
||||
operation: string
|
||||
tableId?: string
|
||||
rowId?: string
|
||||
data?: string | unknown
|
||||
rows?: string | unknown
|
||||
filter?: string | unknown
|
||||
sort?: string | unknown
|
||||
limit?: string
|
||||
offset?: string
|
||||
builderMode?: string
|
||||
filterBuilder?: unknown
|
||||
sortBuilder?: unknown
|
||||
bulkFilterMode?: string
|
||||
bulkFilterBuilder?: unknown
|
||||
}
|
||||
|
||||
/** Normalized params after parsing, ready for tool request body */
|
||||
interface ParsedParams {
|
||||
tableId?: string
|
||||
rowId?: string
|
||||
data?: unknown
|
||||
rows?: unknown
|
||||
filter?: unknown
|
||||
sort?: unknown
|
||||
limit?: number
|
||||
offset?: number
|
||||
}
|
||||
|
||||
/** Transforms raw block params into tool request params for each operation */
|
||||
const paramTransformers: Record<string, (params: TableBlockParams) => ParsedParams> = {
|
||||
insert_row: (params) => ({
|
||||
tableId: params.tableId,
|
||||
data: parseJSON(params.data, 'Row Data'),
|
||||
}),
|
||||
|
||||
upsert_row: (params) => ({
|
||||
tableId: params.tableId,
|
||||
data: parseJSON(params.data, 'Row Data'),
|
||||
}),
|
||||
|
||||
batch_insert_rows: (params) => ({
|
||||
tableId: params.tableId,
|
||||
rows: parseJSON(params.rows, 'Rows Data'),
|
||||
}),
|
||||
|
||||
update_row: (params) => ({
|
||||
tableId: params.tableId,
|
||||
rowId: params.rowId,
|
||||
data: parseJSON(params.data, 'Row Data'),
|
||||
}),
|
||||
|
||||
update_rows_by_filter: (params) => {
|
||||
let filter: unknown
|
||||
if (params.bulkFilterMode === 'builder' && params.bulkFilterBuilder) {
|
||||
filter =
|
||||
conditionsToFilter(params.bulkFilterBuilder as Parameters<typeof conditionsToFilter>[0]) ||
|
||||
undefined
|
||||
} else if (params.filter) {
|
||||
filter = parseJSON(params.filter, 'Filter')
|
||||
}
|
||||
|
||||
return {
|
||||
tableId: params.tableId,
|
||||
filter,
|
||||
data: parseJSON(params.data, 'Row Data'),
|
||||
limit: params.limit ? Number.parseInt(params.limit) : undefined,
|
||||
}
|
||||
},
|
||||
|
||||
delete_row: (params) => ({
|
||||
tableId: params.tableId,
|
||||
rowId: params.rowId,
|
||||
}),
|
||||
|
||||
delete_rows_by_filter: (params) => {
|
||||
let filter: unknown
|
||||
if (params.bulkFilterMode === 'builder' && params.bulkFilterBuilder) {
|
||||
filter =
|
||||
conditionsToFilter(params.bulkFilterBuilder as Parameters<typeof conditionsToFilter>[0]) ||
|
||||
undefined
|
||||
} else if (params.filter) {
|
||||
filter = parseJSON(params.filter, 'Filter')
|
||||
}
|
||||
|
||||
return {
|
||||
tableId: params.tableId,
|
||||
filter,
|
||||
limit: params.limit ? Number.parseInt(params.limit) : undefined,
|
||||
}
|
||||
},
|
||||
|
||||
get_row: (params) => ({
|
||||
tableId: params.tableId,
|
||||
rowId: params.rowId,
|
||||
}),
|
||||
|
||||
get_schema: (params) => ({
|
||||
tableId: params.tableId,
|
||||
}),
|
||||
|
||||
query_rows: (params) => {
|
||||
let filter: unknown
|
||||
if (params.builderMode === 'builder' && params.filterBuilder) {
|
||||
filter =
|
||||
conditionsToFilter(params.filterBuilder as Parameters<typeof conditionsToFilter>[0]) ||
|
||||
undefined
|
||||
} else if (params.filter) {
|
||||
filter = parseJSON(params.filter, 'Filter')
|
||||
}
|
||||
|
||||
let sort: unknown
|
||||
if (params.builderMode === 'builder' && params.sortBuilder) {
|
||||
sort =
|
||||
sortConditionsToSort(params.sortBuilder as Parameters<typeof sortConditionsToSort>[0]) ||
|
||||
undefined
|
||||
} else if (params.sort) {
|
||||
sort = parseJSON(params.sort, 'Sort')
|
||||
}
|
||||
|
||||
return {
|
||||
tableId: params.tableId,
|
||||
filter,
|
||||
sort,
|
||||
limit: params.limit ? Number.parseInt(params.limit) : 100,
|
||||
offset: params.offset ? Number.parseInt(params.offset) : 0,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export const TableBlock: BlockConfig<TableQueryResponse> = {
|
||||
type: 'table',
|
||||
name: 'Table',
|
||||
@@ -353,154 +520,10 @@ Return ONLY the sort JSON:`,
|
||||
},
|
||||
params: (params) => {
|
||||
const { operation, ...rest } = params
|
||||
const transformer = paramTransformers[operation]
|
||||
|
||||
/**
|
||||
* Helper to parse JSON with better error messages.
|
||||
* Also handles common issues with block references in JSON.
|
||||
*/
|
||||
const parseJSON = (value: string | any, fieldName: string): any => {
|
||||
if (typeof value !== 'string') return value
|
||||
|
||||
try {
|
||||
return JSON.parse(value)
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : String(error)
|
||||
|
||||
// Check if the error might be due to unquoted string values (common when block references are resolved)
|
||||
// This happens when users write {"field": <ref>} instead of {"field": "<ref>"}
|
||||
const unquotedValueMatch = value.match(
|
||||
/:\s*([a-zA-Z][a-zA-Z0-9_\s]*[a-zA-Z0-9]|[a-zA-Z])\s*[,}]/
|
||||
)
|
||||
|
||||
let hint =
|
||||
'Make sure all property names are in double quotes (e.g., {"name": "value"} not {name: "value"}).'
|
||||
|
||||
if (unquotedValueMatch) {
|
||||
hint =
|
||||
'It looks like a string value is not quoted. When using block references in JSON, wrap them in double quotes: {"field": "<blockName.output>"} not {"field": <blockName.output>}.'
|
||||
}
|
||||
|
||||
throw new Error(`Invalid JSON in ${fieldName}: ${errorMsg}. ${hint}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Insert Row
|
||||
if (operation === 'insert_row') {
|
||||
const data = parseJSON(rest.data, 'Row Data')
|
||||
return {
|
||||
tableId: rest.tableId,
|
||||
data,
|
||||
}
|
||||
}
|
||||
|
||||
// Upsert Row
|
||||
if (operation === 'upsert_row') {
|
||||
const data = parseJSON(rest.data, 'Row Data')
|
||||
return {
|
||||
tableId: rest.tableId,
|
||||
data,
|
||||
}
|
||||
}
|
||||
|
||||
// Batch Insert Rows
|
||||
if (operation === 'batch_insert_rows') {
|
||||
const rows = parseJSON(rest.rows, 'Rows Data')
|
||||
return {
|
||||
tableId: rest.tableId,
|
||||
rows,
|
||||
}
|
||||
}
|
||||
|
||||
// Update Row by ID
|
||||
if (operation === 'update_row') {
|
||||
const data = parseJSON(rest.data, 'Row Data')
|
||||
return {
|
||||
tableId: rest.tableId,
|
||||
rowId: rest.rowId,
|
||||
data,
|
||||
}
|
||||
}
|
||||
|
||||
// Update Rows by Filter
|
||||
if (operation === 'update_rows_by_filter') {
|
||||
let filter: any
|
||||
if (rest.bulkFilterMode === 'builder' && rest.bulkFilterBuilder) {
|
||||
filter = conditionsToFilter(rest.bulkFilterBuilder as any) || undefined
|
||||
} else if (rest.filter) {
|
||||
filter = parseJSON(rest.filter, 'Filter')
|
||||
}
|
||||
const data = parseJSON(rest.data, 'Row Data')
|
||||
return {
|
||||
tableId: rest.tableId,
|
||||
filter,
|
||||
data,
|
||||
limit: rest.limit ? Number.parseInt(rest.limit as string) : undefined,
|
||||
}
|
||||
}
|
||||
|
||||
// Delete Row by ID
|
||||
if (operation === 'delete_row') {
|
||||
return {
|
||||
tableId: rest.tableId,
|
||||
rowId: rest.rowId,
|
||||
}
|
||||
}
|
||||
|
||||
// Delete Rows by Filter
|
||||
if (operation === 'delete_rows_by_filter') {
|
||||
let filter: any
|
||||
if (rest.bulkFilterMode === 'builder' && rest.bulkFilterBuilder) {
|
||||
filter = conditionsToFilter(rest.bulkFilterBuilder as any) || undefined
|
||||
} else if (rest.filter) {
|
||||
filter = parseJSON(rest.filter, 'Filter')
|
||||
}
|
||||
return {
|
||||
tableId: rest.tableId,
|
||||
filter,
|
||||
limit: rest.limit ? Number.parseInt(rest.limit as string) : undefined,
|
||||
}
|
||||
}
|
||||
|
||||
// Get Row by ID
|
||||
if (operation === 'get_row') {
|
||||
return {
|
||||
tableId: rest.tableId,
|
||||
rowId: rest.rowId,
|
||||
}
|
||||
}
|
||||
|
||||
// Get Schema
|
||||
if (operation === 'get_schema') {
|
||||
return {
|
||||
tableId: rest.tableId,
|
||||
}
|
||||
}
|
||||
|
||||
// Query Rows
|
||||
if (operation === 'query_rows') {
|
||||
let filter: any
|
||||
if (rest.builderMode === 'builder' && rest.filterBuilder) {
|
||||
// Convert builder conditions to filter object
|
||||
filter = conditionsToFilter(rest.filterBuilder as any) || undefined
|
||||
} else if (rest.filter) {
|
||||
filter = parseJSON(rest.filter, 'Filter')
|
||||
}
|
||||
|
||||
let sort: any
|
||||
if (rest.builderMode === 'builder' && rest.sortBuilder) {
|
||||
// Convert sort builder conditions to sort object
|
||||
sort = sortConditionsToSort(rest.sortBuilder as any) || undefined
|
||||
} else if (rest.sort) {
|
||||
sort = parseJSON(rest.sort, 'Sort')
|
||||
}
|
||||
|
||||
return {
|
||||
tableId: rest.tableId,
|
||||
filter,
|
||||
sort,
|
||||
limit: rest.limit ? Number.parseInt(rest.limit as string) : 100,
|
||||
offset: rest.offset ? Number.parseInt(rest.offset as string) : 0,
|
||||
}
|
||||
if (transformer) {
|
||||
return transformer(rest as TableBlockParams)
|
||||
}
|
||||
|
||||
return rest
|
||||
|
||||
@@ -5,12 +5,7 @@
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* Available comparison operators for filter conditions.
|
||||
*
|
||||
* @remarks
|
||||
* These operators map to the query builder operators in query-builder.ts
|
||||
*/
|
||||
/** Comparison operators for filter conditions (maps to query-builder.ts) */
|
||||
export const COMPARISON_OPERATORS = [
|
||||
{ value: 'eq', label: 'equals' },
|
||||
{ value: 'ne', label: 'not equals' },
|
||||
@@ -38,12 +33,7 @@ export const SORT_DIRECTIONS = [
|
||||
{ value: 'desc', label: 'descending' },
|
||||
] as const
|
||||
|
||||
/**
|
||||
* Represents a single filter condition.
|
||||
*
|
||||
* @remarks
|
||||
* Used by filter builder UI components to construct filter queries.
|
||||
*/
|
||||
/** Single filter condition used by filter builder UI */
|
||||
export interface FilterCondition {
|
||||
/** Unique identifier for the condition (used as React key) */
|
||||
id: string
|
||||
|
||||
@@ -59,19 +59,13 @@ function validateOperator(operator: string): void {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a JSONB containment clause using GIN index.
|
||||
* Generates: `table.data @> '{"field": value}'::jsonb`
|
||||
*/
|
||||
/** Builds JSONB containment clause: `data @> '{"field": value}'::jsonb` (uses GIN index) */
|
||||
function buildContainmentClause(tableName: string, field: string, value: JsonValue): SQL {
|
||||
const jsonObj = JSON.stringify({ [field]: value })
|
||||
return sql`${sql.raw(`${tableName}.data`)} @> ${jsonObj}::jsonb`
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a numeric comparison clause for JSONB fields.
|
||||
* Generates: `(table.data->>'field')::numeric <operator> value`
|
||||
*/
|
||||
/** Builds numeric comparison: `(data->>'field')::numeric <op> value` (cannot use GIN index) */
|
||||
function buildComparisonClause(
|
||||
tableName: string,
|
||||
field: string,
|
||||
@@ -82,10 +76,7 @@ function buildComparisonClause(
|
||||
return sql`(${sql.raw(`${tableName}.data->>'${escapedField}'`)})::numeric ${sql.raw(operator)} ${value}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a case-insensitive pattern matching clause for JSONB text fields.
|
||||
* Generates: `table.data->>'field' ILIKE '%value%'`
|
||||
*/
|
||||
/** Builds case-insensitive pattern match: `data->>'field' ILIKE '%value%'` */
|
||||
function buildContainsClause(tableName: string, field: string, value: string): SQL {
|
||||
const escapedField = field.replace(/'/g, "''")
|
||||
return sql`${sql.raw(`${tableName}.data->>'${escapedField}'`)} ILIKE ${`%${value}%`}`
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
/**
|
||||
* Table service layer for business logic operations.
|
||||
* Table service layer for internal programmatic access.
|
||||
*
|
||||
* This module provides the core business logic for user-defined tables,
|
||||
* extracted from API route handlers for better testability and reuse.
|
||||
* Use this for: workflow executor, background jobs, testing business logic.
|
||||
* Use API routes for: HTTP requests, frontend clients.
|
||||
*
|
||||
* Note: API routes have their own implementations for HTTP-specific concerns.
|
||||
*
|
||||
* @module lib/table/service
|
||||
*/
|
||||
|
||||
@@ -34,7 +34,7 @@ export const tableBatchInsertRowsTool: ToolConfig<
|
||||
body: (params: TableBatchInsertParams) => {
|
||||
const workspaceId = params._context?.workspaceId
|
||||
if (!workspaceId) {
|
||||
throw new Error('workspaceId is required in execution context')
|
||||
throw new Error('Workspace ID is required in execution context')
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -37,7 +37,7 @@ export const tableCreateTool: ToolConfig<TableCreateParams, TableCreateResponse>
|
||||
body: (params) => {
|
||||
const workspaceId = params._context?.workspaceId
|
||||
if (!workspaceId) {
|
||||
throw new Error('workspaceId is required in execution context')
|
||||
throw new Error('Workspace ID is required in execution context')
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -31,7 +31,7 @@ export const tableDeleteRowTool: ToolConfig<TableRowDeleteParams, TableDeleteRes
|
||||
body: (params: TableRowDeleteParams) => {
|
||||
const workspaceId = params._context?.workspaceId
|
||||
if (!workspaceId) {
|
||||
throw new Error('workspaceId is required in execution context')
|
||||
throw new Error('Workspace ID is required in execution context')
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -41,7 +41,7 @@ export const tableDeleteRowsByFilterTool: ToolConfig<
|
||||
body: (params: TableDeleteByFilterParams) => {
|
||||
const workspaceId = params._context?.workspaceId
|
||||
if (!workspaceId) {
|
||||
throw new Error('workspaceId is required in execution context')
|
||||
throw new Error('Workspace ID is required in execution context')
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -26,7 +26,7 @@ export const tableGetRowTool: ToolConfig<TableRowGetParams, TableRowResponse> =
|
||||
url: (params: TableRowGetParams) => {
|
||||
const workspaceId = params._context?.workspaceId
|
||||
if (!workspaceId) {
|
||||
throw new Error('workspaceId is required in execution context')
|
||||
throw new Error('Workspace ID is required in execution context')
|
||||
}
|
||||
|
||||
return `/api/table/${params.tableId}/rows/${params.rowId}?workspaceId=${encodeURIComponent(workspaceId)}`
|
||||
|
||||
@@ -20,7 +20,7 @@ export const tableGetSchemaTool: ToolConfig<TableGetSchemaParams, TableGetSchema
|
||||
url: (params: TableGetSchemaParams) => {
|
||||
const workspaceId = params._context?.workspaceId
|
||||
if (!workspaceId) {
|
||||
throw new Error('workspaceId is required in execution context')
|
||||
throw new Error('Workspace ID is required in execution context')
|
||||
}
|
||||
|
||||
return `/api/table/${params.tableId}?workspaceId=${encodeURIComponent(workspaceId)}`
|
||||
|
||||
@@ -31,7 +31,7 @@ export const tableInsertRowTool: ToolConfig<TableRowInsertParams, TableRowRespon
|
||||
body: (params: TableRowInsertParams) => {
|
||||
const workspaceId = params._context?.workspaceId
|
||||
if (!workspaceId) {
|
||||
throw new Error('workspaceId is required in execution context')
|
||||
throw new Error('Workspace ID is required in execution context')
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -13,7 +13,7 @@ export const tableListTool: ToolConfig<TableListParams, TableListResponse> = {
|
||||
url: (params: TableListParams) => {
|
||||
const workspaceId = params._context?.workspaceId
|
||||
if (!workspaceId) {
|
||||
throw new Error('workspaceId is required in execution context')
|
||||
throw new Error('Workspace ID is required in execution context')
|
||||
}
|
||||
return `/api/table?workspaceId=${encodeURIComponent(workspaceId)}`
|
||||
},
|
||||
|
||||
@@ -45,7 +45,7 @@ export const tableQueryRowsTool: ToolConfig<TableRowQueryParams, TableQueryRespo
|
||||
url: (params: TableRowQueryParams) => {
|
||||
const workspaceId = params._context?.workspaceId
|
||||
if (!workspaceId) {
|
||||
throw new Error('workspaceId is required in execution context')
|
||||
throw new Error('Workspace ID is required in execution context')
|
||||
}
|
||||
|
||||
const searchParams = new URLSearchParams({
|
||||
|
||||
@@ -37,7 +37,7 @@ export const tableUpdateRowTool: ToolConfig<TableRowUpdateParams, TableRowRespon
|
||||
body: (params: TableRowUpdateParams) => {
|
||||
const workspaceId = params._context?.workspaceId
|
||||
if (!workspaceId) {
|
||||
throw new Error('workspaceId is required in execution context')
|
||||
throw new Error('Workspace ID is required in execution context')
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -47,7 +47,7 @@ export const tableUpdateRowsByFilterTool: ToolConfig<
|
||||
body: (params: TableUpdateByFilterParams) => {
|
||||
const workspaceId = params._context?.workspaceId
|
||||
if (!workspaceId) {
|
||||
throw new Error('workspaceId is required in execution context')
|
||||
throw new Error('Workspace ID is required in execution context')
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -41,7 +41,7 @@ export const tableUpsertRowTool: ToolConfig<TableRowInsertParams, TableUpsertRes
|
||||
body: (params: TableRowInsertParams) => {
|
||||
const workspaceId = params._context?.workspaceId
|
||||
if (!workspaceId) {
|
||||
throw new Error('workspaceId is required in execution context')
|
||||
throw new Error('Workspace ID is required in execution context')
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user