diff --git a/apps/sim/app/api/table/[tableId]/rows/route.ts b/apps/sim/app/api/table/[tableId]/rows/route.ts index 1fc2eb789..183b33ba7 100644 --- a/apps/sim/app/api/table/[tableId]/rows/route.ts +++ b/apps/sim/app/api/table/[tableId]/rows/route.ts @@ -6,7 +6,7 @@ import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { checkHybridAuth } from '@/lib/auth/hybrid' import { generateRequestId } from '@/lib/core/utils/request' -import type { QueryFilter, RowData, TableSchema } from '@/lib/table' +import type { Filter, RowData, TableSchema } from '@/lib/table' import { getUniqueColumns, TABLE_LIMITS, @@ -409,7 +409,7 @@ export async function GET(request: NextRequest, { params }: TableRowsRouteParams // Add filter conditions if provided if (validated.filter) { - const filterClause = buildFilterClause(validated.filter as QueryFilter, 'user_table_rows') + const filterClause = buildFilterClause(validated.filter as Filter, 'user_table_rows') if (filterClause) { baseConditions.push(filterClause) } @@ -546,7 +546,7 @@ export async function PUT(request: NextRequest, { params }: TableRowsRouteParams ] // Add filter conditions - const filterClause = buildFilterClause(validated.filter as QueryFilter, 'user_table_rows') + const filterClause = buildFilterClause(validated.filter as Filter, 'user_table_rows') if (filterClause) { baseConditions.push(filterClause) } @@ -740,7 +740,7 @@ export async function DELETE(request: NextRequest, { params }: TableRowsRoutePar ] // Add filter conditions - const filterClause = buildFilterClause(validated.filter as QueryFilter, 'user_table_rows') + const filterClause = buildFilterClause(validated.filter as Filter, 'user_table_rows') if (filterClause) { baseConditions.push(filterClause) } diff --git a/apps/sim/lib/table/query-builder.test.ts b/apps/sim/lib/table/query-builder.test.ts index 9569ebfad..c468d13ff 100644 --- a/apps/sim/lib/table/query-builder.test.ts +++ b/apps/sim/lib/table/query-builder.test.ts @@ -7,7 +7,7 @@ */ import { describe, expect, it } from 'vitest' import { buildFilterClause, buildSortClause } from './query-builder' -import type { QueryFilter } from './types' +import type { Filter } from './types' describe('Query Builder', () => { describe('buildFilterClause', () => { @@ -19,84 +19,84 @@ describe('Query Builder', () => { }) it('should handle simple equality filter', () => { - const filter: QueryFilter = { name: 'John' } + const filter: Filter = { name: 'John' } const result = buildFilterClause(filter, tableName) expect(result).toBeDefined() }) it('should handle $eq operator', () => { - const filter: QueryFilter = { status: { $eq: 'active' } } + const filter: Filter = { status: { $eq: 'active' } } const result = buildFilterClause(filter, tableName) expect(result).toBeDefined() }) it('should handle $ne operator', () => { - const filter: QueryFilter = { status: { $ne: 'deleted' } } + const filter: Filter = { status: { $ne: 'deleted' } } const result = buildFilterClause(filter, tableName) expect(result).toBeDefined() }) it('should handle $gt operator', () => { - const filter: QueryFilter = { age: { $gt: 18 } } + const filter: Filter = { age: { $gt: 18 } } const result = buildFilterClause(filter, tableName) expect(result).toBeDefined() }) it('should handle $gte operator', () => { - const filter: QueryFilter = { age: { $gte: 18 } } + const filter: Filter = { age: { $gte: 18 } } const result = buildFilterClause(filter, tableName) expect(result).toBeDefined() }) it('should handle $lt operator', () => { - const filter: QueryFilter = { age: { $lt: 65 } } + const filter: Filter = { age: { $lt: 65 } } const result = buildFilterClause(filter, tableName) expect(result).toBeDefined() }) it('should handle $lte operator', () => { - const filter: QueryFilter = { age: { $lte: 65 } } + const filter: Filter = { age: { $lte: 65 } } const result = buildFilterClause(filter, tableName) expect(result).toBeDefined() }) it('should handle $in operator with single value', () => { - const filter: QueryFilter = { status: { $in: ['active'] } } + const filter: Filter = { status: { $in: ['active'] } } const result = buildFilterClause(filter, tableName) expect(result).toBeDefined() }) it('should handle $in operator with multiple values', () => { - const filter: QueryFilter = { status: { $in: ['active', 'pending'] } } + const filter: Filter = { status: { $in: ['active', 'pending'] } } const result = buildFilterClause(filter, tableName) expect(result).toBeDefined() }) it('should handle $nin operator', () => { - const filter: QueryFilter = { status: { $nin: ['deleted', 'archived'] } } + const filter: Filter = { status: { $nin: ['deleted', 'archived'] } } const result = buildFilterClause(filter, tableName) expect(result).toBeDefined() }) it('should handle $contains operator', () => { - const filter: QueryFilter = { name: { $contains: 'john' } } + const filter: Filter = { name: { $contains: 'john' } } const result = buildFilterClause(filter, tableName) expect(result).toBeDefined() }) it('should handle $or logical operator', () => { - const filter: QueryFilter = { + const filter: Filter = { $or: [{ status: 'active' }, { status: 'pending' }], } const result = buildFilterClause(filter, tableName) @@ -105,7 +105,7 @@ describe('Query Builder', () => { }) it('should handle $and logical operator', () => { - const filter: QueryFilter = { + const filter: Filter = { $and: [{ status: 'active' }, { age: { $gt: 18 } }], } const result = buildFilterClause(filter, tableName) @@ -114,7 +114,7 @@ describe('Query Builder', () => { }) it('should handle multiple conditions combined with AND', () => { - const filter: QueryFilter = { + const filter: Filter = { status: 'active', age: { $gt: 18 }, } @@ -124,7 +124,7 @@ describe('Query Builder', () => { }) it('should handle nested $or and $and', () => { - const filter: QueryFilter = { + const filter: Filter = { $or: [{ $and: [{ status: 'active' }, { verified: true }] }, { role: 'admin' }], } const result = buildFilterClause(filter, tableName) @@ -133,40 +133,40 @@ describe('Query Builder', () => { }) it('should throw error for invalid field name', () => { - const filter: QueryFilter = { 'invalid-field': 'value' } + const filter: Filter = { 'invalid-field': 'value' } expect(() => buildFilterClause(filter, tableName)).toThrow('Invalid field name') }) it('should throw error for invalid operator', () => { - const filter = { name: { $invalid: 'value' } } as unknown as QueryFilter + const filter = { name: { $invalid: 'value' } } as unknown as Filter expect(() => buildFilterClause(filter, tableName)).toThrow('Invalid operator') }) it('should skip undefined values', () => { - const filter: QueryFilter = { name: undefined, status: 'active' } + const filter: Filter = { name: undefined, status: 'active' } const result = buildFilterClause(filter, tableName) expect(result).toBeDefined() }) it('should handle boolean values', () => { - const filter: QueryFilter = { active: true } + const filter: Filter = { active: true } const result = buildFilterClause(filter, tableName) expect(result).toBeDefined() }) it('should handle null values', () => { - const filter: QueryFilter = { deleted_at: null } + const filter: Filter = { deleted_at: null } const result = buildFilterClause(filter, tableName) expect(result).toBeDefined() }) it('should handle numeric values', () => { - const filter: QueryFilter = { count: 42 } + const filter: Filter = { count: 42 } const result = buildFilterClause(filter, tableName) expect(result).toBeDefined() @@ -236,13 +236,13 @@ describe('Query Builder', () => { const validNames = ['name', 'user_id', '_private', 'Count123', 'a'] for (const name of validNames) { - const filter: QueryFilter = { [name]: 'value' } + const filter: Filter = { [name]: 'value' } expect(() => buildFilterClause(filter, tableName)).not.toThrow() } }) it('should reject field names starting with number', () => { - const filter: QueryFilter = { '123name': 'value' } + const filter: Filter = { '123name': 'value' } expect(() => buildFilterClause(filter, tableName)).toThrow('Invalid field name') }) @@ -250,7 +250,7 @@ describe('Query Builder', () => { const invalidNames = ['field-name', 'field.name', 'field name', 'field@name'] for (const name of invalidNames) { - const filter: QueryFilter = { [name]: 'value' } + const filter: Filter = { [name]: 'value' } expect(() => buildFilterClause(filter, tableName)).toThrow('Invalid field name') } }) @@ -259,7 +259,7 @@ describe('Query Builder', () => { const sqlInjectionAttempts = ["'; DROP TABLE users; --", 'name OR 1=1', 'name; DELETE FROM'] for (const attempt of sqlInjectionAttempts) { - const filter: QueryFilter = { [attempt]: 'value' } + const filter: Filter = { [attempt]: 'value' } expect(() => buildFilterClause(filter, tableName)).toThrow('Invalid field name') } }) diff --git a/apps/sim/lib/table/query-builder.ts b/apps/sim/lib/table/query-builder.ts index 441c9808c..37cae0fc1 100644 --- a/apps/sim/lib/table/query-builder.ts +++ b/apps/sim/lib/table/query-builder.ts @@ -8,7 +8,7 @@ import type { SQL } from 'drizzle-orm' import { sql } from 'drizzle-orm' import { NAME_PATTERN } from './constants' -import type { FilterOperators, JsonValue, QueryFilter } from './types' +import type { Filter, FilterOperator, JsonValue } from './types' /** * Whitelist of allowed operators for query filtering. @@ -92,7 +92,7 @@ function buildContainsClause(tableName: string, field: string, value: string): S * * @param tableName - The name of the table to query (used for SQL table reference) * @param field - The field name to filter on (must match NAME_PATTERN) - * @param condition - Either a simple value (for equality) or a FilterOperators + * @param condition - Either a simple value (for equality) or a FilterOperator * object with operators like $eq, $gt, $in, etc. * @returns Array of SQL condition fragments. Multiple conditions are returned * when the condition object contains multiple operators. @@ -101,7 +101,7 @@ function buildContainsClause(tableName: string, field: string, value: string): S function buildFieldCondition( tableName: string, field: string, - condition: JsonValue | FilterOperators + condition: JsonValue | FilterOperator ): SQL[] { validateFieldName(field) @@ -181,7 +181,7 @@ function buildFieldCondition( * Builds SQL clauses from nested filters and joins them with the specified operator. */ function buildLogicalClause( - subFilters: QueryFilter[], + subFilters: Filter[], tableName: string, operator: 'OR' | 'AND' ): SQL | undefined { @@ -203,7 +203,7 @@ function buildLogicalClause( * Builds a WHERE clause from a filter object. * Recursively processes logical operators ($or, $and) and field conditions. */ -export function buildFilterClause(filter: QueryFilter, tableName: string): SQL | undefined { +export function buildFilterClause(filter: Filter, tableName: string): SQL | undefined { const conditions: SQL[] = [] for (const [field, condition] of Object.entries(filter)) { @@ -212,7 +212,7 @@ export function buildFilterClause(filter: QueryFilter, tableName: string): SQL | } if (field === '$or' && Array.isArray(condition)) { - const orClause = buildLogicalClause(condition as QueryFilter[], tableName, 'OR') + const orClause = buildLogicalClause(condition as Filter[], tableName, 'OR') if (orClause) { conditions.push(orClause) } @@ -220,7 +220,7 @@ export function buildFilterClause(filter: QueryFilter, tableName: string): SQL | } if (field === '$and' && Array.isArray(condition)) { - const andClause = buildLogicalClause(condition as QueryFilter[], tableName, 'AND') + const andClause = buildLogicalClause(condition as Filter[], tableName, 'AND') if (andClause) { conditions.push(andClause) } @@ -234,7 +234,7 @@ export function buildFilterClause(filter: QueryFilter, tableName: string): SQL | const fieldConditions = buildFieldCondition( tableName, field, - condition as JsonValue | FilterOperators + condition as JsonValue | FilterOperator ) conditions.push(...fieldConditions) } diff --git a/apps/sim/lib/table/types.ts b/apps/sim/lib/table/types.ts index 5ee1f42e9..f995edfd8 100644 --- a/apps/sim/lib/table/types.ts +++ b/apps/sim/lib/table/types.ts @@ -8,30 +8,28 @@ import type { COLUMN_TYPES } from './constants' /** Primitive values that can be stored in table columns */ export type ColumnValue = string | number | boolean | null | Date - -/** JSON-compatible value for complex column types */ export type JsonValue = ColumnValue | JsonValue[] | { [key: string]: JsonValue } -/** Row data structure for insert/update operations */ +/** Row data structure for insert/update operations + * key is the column name and value is the value of the column + * value is a JSON-compatible value */ export type RowData = Record /** Sort direction for query operations */ export type SortDirection = 'asc' | 'desc' -/** Sort specification mapping column names to sort direction */ -export type SortSpec = Record +/** Sort specification mapping column names to sort direction + * "asc" for ascending and "desc" for descending + * key is the column name and value is the sort direction */ +export type Sort = Record /** * Column definition within a table schema. */ export interface ColumnDefinition { - /** Column name (must match NAME_PATTERN) */ name: string - /** Data type for the column */ type: (typeof COLUMN_TYPES)[number] - /** Whether the column is required (non-null) */ required?: boolean - /** Whether the column must have unique values */ unique?: boolean } @@ -39,7 +37,6 @@ export interface ColumnDefinition { * Table schema definition containing column specifications. */ export interface TableSchema { - /** Array of column definitions */ columns: ColumnDefinition[] } @@ -47,23 +44,14 @@ export interface TableSchema { * Complete table definition including metadata. */ export interface TableDefinition { - /** Unique table identifier */ id: string - /** Human-readable table name */ name: string - /** Optional table description */ description?: string | null - /** Table schema with column definitions */ schema: TableSchema - /** Current number of rows in the table */ rowCount: number - /** Maximum allowed rows (from TABLE_LIMITS) */ maxRows: number - /** Workspace the table belongs to */ workspaceId: string - /** ISO timestamp of creation */ createdAt: Date | string - /** ISO timestamp of last update */ updatedAt: Date | string } @@ -71,13 +59,9 @@ export interface TableDefinition { * Row stored in a user-defined table. */ export interface TableRow { - /** Unique row identifier */ id: string - /** Row data as key-value pairs */ data: RowData - /** ISO timestamp of creation */ createdAt: Date | string - /** ISO timestamp of last update */ updatedAt: Date | string } @@ -85,47 +69,33 @@ export interface TableRow { * Filter operator conditions for query filtering. * Supports MongoDB-style query operators. */ -export interface FilterOperators { - /** Equal to */ +export interface FilterOperator { $eq?: ColumnValue - /** Not equal to */ $ne?: ColumnValue - /** Greater than (numbers only) */ $gt?: number - /** Greater than or equal (numbers only) */ $gte?: number - /** Less than (numbers only) */ $lt?: number - /** Less than or equal (numbers only) */ $lte?: number - /** Value in array */ $in?: ColumnValue[] - /** Value not in array */ $nin?: ColumnValue[] - /** String contains (case-insensitive) */ $contains?: string } /** - * Query filter for table rows. + * Filter for querying table rows. * Keys are column names, values are either direct values or filter operators. */ -export interface QueryFilter { - /** Logical OR of multiple filters */ - $or?: QueryFilter[] - /** Logical AND of multiple filters */ - $and?: QueryFilter[] - /** Column filters */ - [key: string]: ColumnValue | FilterOperators | QueryFilter[] | undefined +export interface Filter { + $or?: Filter[] + $and?: Filter[] + [key: string]: ColumnValue | FilterOperator | Filter[] | undefined } /** - * Result of a validation operation. + * Result of a validation operation. The list of errors are used to display to the user. */ export interface ValidationResult { - /** Whether validation passed */ valid: boolean - /** Array of error messages (empty if valid) */ errors: string[] } @@ -133,13 +103,9 @@ export interface ValidationResult { * Options for querying table rows. */ export interface QueryOptions { - /** Filter criteria */ - filter?: QueryFilter - /** Sort specification */ - sort?: SortSpec - /** Maximum rows to return */ + filter?: Filter + sort?: Sort limit?: number - /** Number of rows to skip */ offset?: number } @@ -163,9 +129,7 @@ export interface QueryResult { * Result of a bulk operation (update/delete by filter). */ export interface BulkOperationResult { - /** Number of rows affected */ affectedCount: number - /** IDs of affected rows */ affectedRowIds: string[] } @@ -189,11 +153,8 @@ export interface CreateTableData { * Data required to insert a row. */ export interface InsertRowData { - /** Table ID */ tableId: string - /** Row data */ data: RowData - /** Workspace ID */ workspaceId: string } @@ -201,11 +162,8 @@ export interface InsertRowData { * Data required for batch row insertion. */ export interface BatchInsertData { - /** Table ID */ tableId: string - /** Array of row data */ rows: RowData[] - /** Workspace ID */ workspaceId: string } @@ -213,13 +171,9 @@ export interface BatchInsertData { * Data required to update a row. */ export interface UpdateRowData { - /** Table ID */ tableId: string - /** Row ID to update */ rowId: string - /** Full row data replacement */ data: RowData - /** Workspace ID */ workspaceId: string } @@ -227,15 +181,10 @@ export interface UpdateRowData { * Data required for bulk update by filter. */ export interface BulkUpdateData { - /** Table ID */ tableId: string - /** Filter to match rows */ - filter: QueryFilter - /** Data to apply to matched rows */ + filter: Filter data: RowData - /** Maximum rows to update */ limit?: number - /** Workspace ID */ workspaceId: string } @@ -243,12 +192,8 @@ export interface BulkUpdateData { * Data required for bulk delete by filter. */ export interface BulkDeleteData { - /** Table ID */ tableId: string - /** Filter to match rows */ - filter: QueryFilter - /** Maximum rows to delete */ + filter: Filter limit?: number - /** Workspace ID */ workspaceId: string } diff --git a/apps/sim/tools/table/types.ts b/apps/sim/tools/table/types.ts index cd7a5092a..a5e32ccda 100644 --- a/apps/sim/tools/table/types.ts +++ b/apps/sim/tools/table/types.ts @@ -1,6 +1,6 @@ import type { ColumnDefinition, - QueryFilter, + Filter, RowData, TableDefinition, TableRow, @@ -40,7 +40,7 @@ export interface TableRowDeleteParams { export interface TableRowQueryParams { tableId: string - filter?: QueryFilter + filter?: Filter sort?: Record limit?: number offset?: number @@ -107,7 +107,7 @@ export interface TableBatchInsertResponse extends ToolResponse { export interface TableUpdateByFilterParams { tableId: string - filter: QueryFilter + filter: Filter data: RowData limit?: number _context?: ToolExecutionContext @@ -115,7 +115,7 @@ export interface TableUpdateByFilterParams { export interface TableDeleteByFilterParams { tableId: string - filter: QueryFilter + filter: Filter limit?: number _context?: ToolExecutionContext }