mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-23 05:47:59 -05:00
updates
This commit is contained in:
@@ -7,7 +7,7 @@ import { z } from 'zod'
|
||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { TABLE_LIMITS, validateTableName, validateTableSchema } from '@/lib/table'
|
||||
import type { TableSchema } from '@/lib/table/validation'
|
||||
import type { TableSchema } from '@/lib/table/validation/validation'
|
||||
import type { TableColumnData, TableSchemaData } from './utils'
|
||||
|
||||
const logger = createLogger('TableAPI')
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { ArrowDownAZ, ArrowUpAZ, Plus, X } from 'lucide-react'
|
||||
import { Button, Combobox, Input } from '@/components/emcn'
|
||||
import type { FilterCondition } from '@/lib/table/filter-constants'
|
||||
import { useFilterBuilder } from '@/lib/table/use-filter-builder'
|
||||
import type { FilterCondition } from '@/lib/table/filters/filter-constants'
|
||||
import { useFilterBuilder } from '@/lib/table/filters/use-filter-builder'
|
||||
|
||||
/**
|
||||
* Represents a sort configuration.
|
||||
|
||||
@@ -4,9 +4,12 @@ import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { Plus, X } from 'lucide-react'
|
||||
import { Button, Combobox, type ComboboxOption, Input } from '@/components/emcn'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
import { conditionsToJsonString, jsonStringToConditions } from '@/lib/table/filter-builder-utils'
|
||||
import type { FilterCondition } from '@/lib/table/filter-constants'
|
||||
import { useFilterBuilder } from '@/lib/table/use-filter-builder'
|
||||
import {
|
||||
conditionsToJsonString,
|
||||
jsonStringToConditions,
|
||||
} from '@/lib/table/filters/filter-builder-utils'
|
||||
import type { FilterCondition } from '@/lib/table/filters/filter-constants'
|
||||
import { useFilterBuilder } from '@/lib/table/filters/use-filter-builder'
|
||||
import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/formatted-text'
|
||||
import { SubBlockInputController } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/sub-block-input-controller'
|
||||
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
SORT_DIRECTIONS,
|
||||
type SortCondition,
|
||||
sortConditionsToJsonString,
|
||||
} from '@/lib/table/filter-builder-utils'
|
||||
} from '@/lib/table/filters/filter-builder-utils'
|
||||
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
|
||||
|
||||
interface SortFormatProps {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { AlertTriangle, Wand2 } from 'lucide-react'
|
||||
import { Label, Tooltip } from '@/components/emcn/components'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
import type { FilterCondition, SortCondition } from '@/lib/table/filter-builder-utils'
|
||||
import type { FilterCondition, SortCondition } from '@/lib/table/filters/filter-builder-utils'
|
||||
import type { FieldDiffStatus } from '@/lib/workflows/diff/types'
|
||||
import {
|
||||
CheckboxList,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { TableIcon } from '@/components/icons'
|
||||
import { conditionsToFilter, sortConditionsToSort } from '@/lib/table/filter-builder-utils'
|
||||
import { conditionsToFilter, sortConditionsToSort } from '@/lib/table/filters/filter-builder-utils'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import type { TableQueryResponse } from '@/tools/table/types'
|
||||
|
||||
|
||||
@@ -1,65 +1,20 @@
|
||||
/**
|
||||
* React Query hooks for managing user-defined tables.
|
||||
*
|
||||
* Provides hooks for fetching, creating, and deleting tables within a workspace.
|
||||
* Tables are user-defined data structures that can store rows of data in JSONB format.
|
||||
*
|
||||
* @module hooks/queries/use-tables
|
||||
*/
|
||||
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import type { TableDefinition } from '@/tools/table/types'
|
||||
|
||||
/**
|
||||
* Query key factories for table-related queries.
|
||||
* Ensures consistent cache invalidation across the app.
|
||||
*/
|
||||
/**
|
||||
* Query keys for table-related queries in React Query.
|
||||
* Use these keys to ensure correct cache scoping and invalidation
|
||||
* in queries or mutations dealing with user-defined tables.
|
||||
*/
|
||||
export const tableKeys = {
|
||||
/**
|
||||
* Base key for all table queries.
|
||||
* Example: ['tables']
|
||||
*/
|
||||
all: ['tables'] as const,
|
||||
|
||||
/**
|
||||
* Key for all lists of tables.
|
||||
* Useful for cache invalidation across all table lists.
|
||||
* Example: ['tables', 'list']
|
||||
*/
|
||||
lists: () => [...tableKeys.all, 'list'] as const,
|
||||
|
||||
/**
|
||||
* Key for the list of tables in a specific workspace.
|
||||
* @param workspaceId - The workspace ID to scope the list to.
|
||||
* Example: ['tables', 'list', 'workspace_abc123']
|
||||
*/
|
||||
list: (workspaceId?: string) => [...tableKeys.lists(), workspaceId ?? ''] as const,
|
||||
|
||||
/**
|
||||
* Key for all individual table detail queries.
|
||||
* Useful for cache invalidation for all details.
|
||||
* Example: ['tables', 'detail']
|
||||
*/
|
||||
details: () => [...tableKeys.all, 'detail'] as const,
|
||||
|
||||
/**
|
||||
* Key for a specific table's detail.
|
||||
* @param tableId - The table ID to scope the detail to.
|
||||
* Example: ['tables', 'detail', 'table_abc123']
|
||||
*/
|
||||
detail: (tableId: string) => [...tableKeys.details(), tableId] as const,
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to fetch all tables for a workspace.
|
||||
*
|
||||
* @param workspaceId - The workspace ID to fetch tables for. If undefined, the query is disabled.
|
||||
* @returns React Query result containing the list of tables.
|
||||
* Fetch all tables for a workspace.
|
||||
*/
|
||||
export function useTablesList(workspaceId?: string) {
|
||||
return useQuery({
|
||||
@@ -78,17 +33,12 @@ export function useTablesList(workspaceId?: string) {
|
||||
return (response.data?.tables || []) as TableDefinition[]
|
||||
},
|
||||
enabled: Boolean(workspaceId),
|
||||
staleTime: 30 * 1000, // Cache data for 30 seconds before refetching
|
||||
staleTime: 30 * 1000,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to create a new table in a workspace.
|
||||
*
|
||||
* @param workspaceId - The workspace ID where the table will be created.
|
||||
* @returns React Query mutation object with mutationFn and onSuccess handler.
|
||||
* The mutationFn accepts table creation parameters (name, description, schema).
|
||||
* On success, invalidates the tables list query to refresh the UI.
|
||||
* Create a new table in a workspace.
|
||||
*/
|
||||
export function useCreateTable(workspaceId: string) {
|
||||
const queryClient = useQueryClient()
|
||||
@@ -113,19 +63,13 @@ export function useCreateTable(workspaceId: string) {
|
||||
return res.json()
|
||||
},
|
||||
onSuccess: () => {
|
||||
// Invalidate the tables list query to refresh the UI
|
||||
queryClient.invalidateQueries({ queryKey: tableKeys.list(workspaceId) })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to delete a table from a workspace.
|
||||
*
|
||||
* @param workspaceId - The workspace ID containing the table to delete.
|
||||
* @returns React Query mutation object with mutationFn and onSuccess handler.
|
||||
* The mutationFn accepts a tableId string.
|
||||
* On success, invalidates the tables list query to refresh the UI.
|
||||
* Delete a table from a workspace.
|
||||
*/
|
||||
export function useDeleteTable(workspaceId: string) {
|
||||
const queryClient = useQueryClient()
|
||||
@@ -147,7 +91,6 @@ export function useDeleteTable(workspaceId: string) {
|
||||
return res.json()
|
||||
},
|
||||
onSuccess: () => {
|
||||
// Invalidate the tables list query to refresh the UI
|
||||
queryClient.invalidateQueries({ queryKey: tableKeys.list(workspaceId) })
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Shared utilities for filter builder UI components.
|
||||
*
|
||||
* @module lib/table/filter-builder-utils
|
||||
* @module lib/table/filters/filter-builder-utils
|
||||
*/
|
||||
|
||||
// Re-export shared constants and types for backward compatibility
|
||||
@@ -1,11 +1,8 @@
|
||||
/**
|
||||
* Shared constants and types for table filtering and sorting.
|
||||
*
|
||||
* @module lib/table/filter-constants
|
||||
* @module lib/table/filters/filter-constants
|
||||
*
|
||||
* @remarks
|
||||
* This is the single source of truth for all filter and sort constants.
|
||||
* All components should import from here to ensure consistency.
|
||||
*/
|
||||
|
||||
/**
|
||||
9
apps/sim/lib/table/filters/index.ts
Normal file
9
apps/sim/lib/table/filters/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Filter utilities for table queries.
|
||||
*
|
||||
* @module lib/table/filters
|
||||
*/
|
||||
|
||||
export * from './filter-builder-utils'
|
||||
export * from './filter-constants'
|
||||
export * from './use-filter-builder'
|
||||
@@ -4,7 +4,7 @@
|
||||
* Provides reusable filter condition management logic shared between
|
||||
* the table data viewer's FilterBuilder and workflow block's FilterFormat.
|
||||
*
|
||||
* @module lib/table/use-filter-builder
|
||||
* @module lib/table/filters/use-filter-builder
|
||||
*/
|
||||
|
||||
import { useCallback, useMemo } from 'react'
|
||||
@@ -7,7 +7,6 @@
|
||||
*/
|
||||
|
||||
export * from './constants'
|
||||
export * from './filters'
|
||||
export * from './query-builder'
|
||||
export * from './use-filter-builder'
|
||||
export * from './validation'
|
||||
export * from './validation-helpers'
|
||||
|
||||
@@ -87,16 +87,53 @@ function buildContainmentClause(tableName: string, field: string, value: JsonVal
|
||||
return sql`${sql.raw(`${tableName}.data`)} @> ${jsonObj}::jsonb`
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a numeric comparison clause for JSONB fields.
|
||||
* Generates: `(table.data->>'field')::numeric <operator> value`
|
||||
*/
|
||||
function buildComparisonClause(
|
||||
tableName: string,
|
||||
field: string,
|
||||
operator: '>' | '>=' | '<' | '<=',
|
||||
value: number
|
||||
): SQL {
|
||||
const escapedField = field.replace(/'/g, "''")
|
||||
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%'`
|
||||
*/
|
||||
function buildContainsClause(tableName: string, field: string, value: string): SQL {
|
||||
const escapedField = field.replace(/'/g, "''")
|
||||
return sql`${sql.raw(`${tableName}.data->>'${escapedField}'`)} ILIKE ${`%${value}%`}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds SQL conditions for a single field based on the provided condition.
|
||||
*
|
||||
* Supports both simple equality checks (using JSONB containment) and complex
|
||||
* operators like comparison, membership, and pattern matching. Field names are
|
||||
* validated to prevent SQL injection, and operators are validated against an
|
||||
* allowed whitelist.
|
||||
*
|
||||
* @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 FieldCondition
|
||||
* 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.
|
||||
* @throws Error if field name is invalid or operator is not allowed
|
||||
*/
|
||||
function buildFieldCondition(
|
||||
tableName: string,
|
||||
field: string,
|
||||
condition: JsonValue | FieldCondition
|
||||
): SQL[] {
|
||||
// Validate field name to prevent SQL injection
|
||||
validateFieldName(field)
|
||||
|
||||
const conditions: SQL[] = []
|
||||
const escapedField = field.replace(/'/g, "''")
|
||||
|
||||
if (typeof condition === 'object' && condition !== null && !Array.isArray(condition)) {
|
||||
for (const [op, value] of Object.entries(condition)) {
|
||||
@@ -115,34 +152,28 @@ function buildFieldCondition(
|
||||
break
|
||||
|
||||
case '$gt':
|
||||
conditions.push(
|
||||
sql`(${sql.raw(`${tableName}.data->>'${escapedField}'`)})::numeric > ${value}`
|
||||
)
|
||||
conditions.push(buildComparisonClause(tableName, field, '>', value as number))
|
||||
break
|
||||
|
||||
case '$gte':
|
||||
conditions.push(
|
||||
sql`(${sql.raw(`${tableName}.data->>'${escapedField}'`)})::numeric >= ${value}`
|
||||
)
|
||||
conditions.push(buildComparisonClause(tableName, field, '>=', value as number))
|
||||
break
|
||||
|
||||
case '$lt':
|
||||
conditions.push(
|
||||
sql`(${sql.raw(`${tableName}.data->>'${escapedField}'`)})::numeric < ${value}`
|
||||
)
|
||||
conditions.push(buildComparisonClause(tableName, field, '<', value as number))
|
||||
break
|
||||
|
||||
case '$lte':
|
||||
conditions.push(
|
||||
sql`(${sql.raw(`${tableName}.data->>'${escapedField}'`)})::numeric <= ${value}`
|
||||
)
|
||||
conditions.push(buildComparisonClause(tableName, field, '<=', value as number))
|
||||
break
|
||||
|
||||
case '$in':
|
||||
if (Array.isArray(value) && value.length > 0) {
|
||||
if (value.length === 1) {
|
||||
// Single value then use containment clause
|
||||
conditions.push(buildContainmentClause(tableName, field, value[0]))
|
||||
} else {
|
||||
// Multiple values then use OR clause
|
||||
const inConditions = value.map((v) => buildContainmentClause(tableName, field, v))
|
||||
conditions.push(sql`(${sql.join(inConditions, sql.raw(' OR '))})`)
|
||||
}
|
||||
@@ -159,9 +190,7 @@ function buildFieldCondition(
|
||||
break
|
||||
|
||||
case '$contains':
|
||||
conditions.push(
|
||||
sql`${sql.raw(`${tableName}.data->>'${escapedField}'`)} ILIKE ${`%${value}%`}`
|
||||
)
|
||||
conditions.push(buildContainsClause(tableName, field, value as string))
|
||||
break
|
||||
|
||||
default:
|
||||
@@ -176,6 +205,28 @@ function buildFieldCondition(
|
||||
return conditions
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds SQL clauses from nested filters and joins them with the specified operator.
|
||||
*/
|
||||
function buildLogicalClause(
|
||||
subFilters: QueryFilter[],
|
||||
tableName: string,
|
||||
operator: 'OR' | 'AND'
|
||||
): SQL | undefined {
|
||||
const clauses: SQL[] = []
|
||||
for (const subFilter of subFilters) {
|
||||
const clause = buildFilterClause(subFilter, tableName)
|
||||
if (clause) {
|
||||
clauses.push(clause)
|
||||
}
|
||||
}
|
||||
|
||||
if (clauses.length === 0) return undefined
|
||||
if (clauses.length === 1) return clauses[0]
|
||||
|
||||
return sql`(${sql.join(clauses, sql.raw(` ${operator} `))})`
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a WHERE clause from a filter object.
|
||||
* Recursively processes logical operators ($or, $and) and field conditions.
|
||||
@@ -183,82 +234,31 @@ function buildFieldCondition(
|
||||
export function buildFilterClause(filter: QueryFilter, tableName: string): SQL | undefined {
|
||||
const conditions: SQL[] = []
|
||||
|
||||
/**
|
||||
* Iterate over each field and its associated condition in the filter object.
|
||||
*
|
||||
* The filter is expected to be an object where keys are either field names or logical operators
|
||||
* ('$or', '$and'), and values are the conditions to apply or arrays of nested filter objects.
|
||||
*/
|
||||
for (const [field, condition] of Object.entries(filter)) {
|
||||
// Skip undefined conditions (e.g., unused or programmatically removed filters)
|
||||
if (condition === undefined) {
|
||||
continue
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the logical OR operator: { $or: [filter1, filter2, ...] }
|
||||
* Recursively build SQL clauses for each sub-filter,
|
||||
* then join them with an OR. If there is only one sub-filter,
|
||||
* no need for OR grouping.
|
||||
*/
|
||||
if (field === '$or' && Array.isArray(condition)) {
|
||||
const orConditions: SQL[] = []
|
||||
for (const subFilter of condition) {
|
||||
const subClause = buildFilterClause(subFilter as QueryFilter, tableName)
|
||||
if (subClause) {
|
||||
orConditions.push(subClause)
|
||||
}
|
||||
}
|
||||
if (orConditions.length > 0) {
|
||||
if (orConditions.length === 1) {
|
||||
// Only one condition; no need to wrap in OR
|
||||
conditions.push(orConditions[0])
|
||||
} else {
|
||||
// Multiple conditions; join by OR
|
||||
conditions.push(sql`(${sql.join(orConditions, sql.raw(' OR '))})`)
|
||||
}
|
||||
const orClause = buildLogicalClause(condition as QueryFilter[], tableName, 'OR')
|
||||
if (orClause) {
|
||||
conditions.push(orClause)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the logical AND operator: { $and: [filter1, filter2, ...] }
|
||||
* Recursively build SQL clauses for each sub-filter,
|
||||
* then join them with an AND. If there is only one sub-filter,
|
||||
* no need for AND grouping.
|
||||
*/
|
||||
if (field === '$and' && Array.isArray(condition)) {
|
||||
const andConditions: SQL[] = []
|
||||
for (const subFilter of condition) {
|
||||
const subClause = buildFilterClause(subFilter as QueryFilter, tableName)
|
||||
if (subClause) {
|
||||
andConditions.push(subClause)
|
||||
}
|
||||
}
|
||||
if (andConditions.length > 0) {
|
||||
if (andConditions.length === 1) {
|
||||
// Only one condition; no need to wrap in AND
|
||||
conditions.push(andConditions[0])
|
||||
} else {
|
||||
// Multiple conditions; join by AND
|
||||
conditions.push(sql`(${sql.join(andConditions, sql.raw(' AND '))})`)
|
||||
}
|
||||
const andClause = buildLogicalClause(condition as QueryFilter[], tableName, 'AND')
|
||||
if (andClause) {
|
||||
conditions.push(andClause)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
/**
|
||||
* If the condition is an array, but not a logical operator,
|
||||
* skip it (invalid filter structure).
|
||||
*/
|
||||
if (Array.isArray(condition)) {
|
||||
continue
|
||||
}
|
||||
|
||||
/**
|
||||
* Build conditions for regular fields.
|
||||
* This delegates to buildFieldCondition, which handles comparisons like $eq, $gt, etc.
|
||||
*/
|
||||
const fieldConditions = buildFieldCondition(
|
||||
tableName,
|
||||
field,
|
||||
@@ -267,17 +267,27 @@ export function buildFilterClause(filter: QueryFilter, tableName: string): SQL |
|
||||
conditions.push(...fieldConditions)
|
||||
}
|
||||
|
||||
/**
|
||||
* If no conditions were built, return undefined to indicate no filter.
|
||||
* If only one condition exists, return it directly.
|
||||
* Otherwise, join all conditions using AND.
|
||||
*/
|
||||
if (conditions.length === 0) return undefined
|
||||
if (conditions.length === 1) return conditions[0]
|
||||
|
||||
return sql.join(conditions, sql.raw(' AND '))
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a single ORDER BY clause for a field.
|
||||
* Timestamp fields use direct column access, others use JSONB text extraction.
|
||||
*/
|
||||
function buildSortFieldClause(tableName: string, field: string, direction: 'asc' | 'desc'): SQL {
|
||||
const escapedField = field.replace(/'/g, "''")
|
||||
const directionSql = direction.toUpperCase()
|
||||
|
||||
if (field === 'createdAt' || field === 'updatedAt') {
|
||||
return sql.raw(`${tableName}.${escapedField} ${directionSql}`)
|
||||
}
|
||||
|
||||
return sql.raw(`${tableName}.data->>'${escapedField}' ${directionSql}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds an ORDER BY clause from a sort object.
|
||||
* Note: JSONB fields use text extraction, so numeric sorting may not work as expected.
|
||||
@@ -293,40 +303,14 @@ export function buildSortClause(
|
||||
): SQL | undefined {
|
||||
const clauses: SQL[] = []
|
||||
|
||||
/**
|
||||
* Build ORDER BY SQL clauses based on the sort object keys and directions.
|
||||
* - For `createdAt` and `updatedAt`, use the top-level table columns for proper type sorting.
|
||||
* - For all other fields, treat them as keys in the table's data JSONB column.
|
||||
* Extraction is performed with ->> to return text, which is then sorted.
|
||||
* - Field names are validated to prevent SQL injection.
|
||||
*/
|
||||
for (const [field, direction] of Object.entries(sort)) {
|
||||
// Validate field name to prevent SQL injection
|
||||
validateFieldName(field)
|
||||
|
||||
// Validate direction
|
||||
if (direction !== 'asc' && direction !== 'desc') {
|
||||
throw new Error(`Invalid sort direction "${direction}". Must be "asc" or "desc".`)
|
||||
}
|
||||
|
||||
// Escape single quotes for SQL safety (defense in depth)
|
||||
const escapedField = field.replace(/'/g, "''")
|
||||
|
||||
if (field === 'createdAt' || field === 'updatedAt') {
|
||||
// Use regular column for timestamp sorting
|
||||
clauses.push(
|
||||
direction === 'asc'
|
||||
? sql.raw(`${tableName}.${escapedField} ASC`)
|
||||
: sql.raw(`${tableName}.${escapedField} DESC`)
|
||||
)
|
||||
} else {
|
||||
// Use text extraction for JSONB field sorting
|
||||
clauses.push(
|
||||
direction === 'asc'
|
||||
? sql.raw(`${tableName}.data->>'${escapedField}' ASC`)
|
||||
: sql.raw(`${tableName}.data->>'${escapedField}' DESC`)
|
||||
)
|
||||
}
|
||||
clauses.push(buildSortFieldClause(tableName, field, direction))
|
||||
}
|
||||
|
||||
return clauses.length > 0 ? sql.join(clauses, sql.raw(', ')) : undefined
|
||||
|
||||
8
apps/sim/lib/table/validation/index.ts
Normal file
8
apps/sim/lib/table/validation/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Validation utilities for table schemas and row data.
|
||||
*
|
||||
* @module lib/table/validation
|
||||
*/
|
||||
|
||||
export * from './validation'
|
||||
export * from './validation-helpers'
|
||||
@@ -4,7 +4,7 @@
|
||||
* These helpers consolidate common validation patterns (size, schema, uniqueness)
|
||||
* into reusable functions that return formatted error responses.
|
||||
*
|
||||
* @module lib/table/validation-helpers
|
||||
* @module lib/table/validation/validation-helpers
|
||||
*/
|
||||
|
||||
import { db } from '@sim/db'
|
||||
@@ -1,11 +1,11 @@
|
||||
/**
|
||||
* Validation utilities for table schemas and row data.
|
||||
*
|
||||
* @module lib/table/validation
|
||||
* @module lib/table/validation/validation
|
||||
*/
|
||||
|
||||
import type { ColumnType } from './constants'
|
||||
import { COLUMN_TYPES, NAME_PATTERN, TABLE_LIMITS } from './constants'
|
||||
import type { ColumnType } from '../constants'
|
||||
import { COLUMN_TYPES, NAME_PATTERN, TABLE_LIMITS } from '../constants'
|
||||
|
||||
export interface ColumnDefinition {
|
||||
name: string
|
||||
Reference in New Issue
Block a user