@@ -10,6 +10,17 @@ import { sql } from 'drizzle-orm'
1010import { NAME_PATTERN } from './constants'
1111import type { ColumnDefinition , ConditionOperators , Filter , JsonValue , Sort } from './types'
1212
13+ /**
14+ * Error thrown when caller-supplied filter or sort input is malformed.
15+ * Routes should map this to HTTP 400 with the message preserved.
16+ */
17+ export class TableQueryValidationError extends Error {
18+ constructor ( message : string ) {
19+ super ( message )
20+ this . name = 'TableQueryValidationError'
21+ }
22+ }
23+
1324/**
1425 * Whitelist of allowed operators for query filtering.
1526 * Only these operators can be used in filter conditions.
@@ -41,7 +52,7 @@ const ALLOWED_OPERATORS = new Set([
4152 * @param filter - Filter object with field conditions and logical operators
4253 * @param tableName - Table name for the query (e.g., 'user_table_rows')
4354 * @returns SQL WHERE clause or undefined if no filter specified
44- * @throws Error if field name is invalid or operator is not allowed
55+ * @throws { TableQueryValidationError } if field name is invalid or operator is not allowed
4556 *
4657 * @example
4758 * // Simple equality
@@ -110,7 +121,7 @@ export function buildFilterClause(filter: Filter, tableName: string): SQL | unde
110121 * @param tableName - Table name for the query (e.g., 'user_table_rows')
111122 * @param columns - Optional column definitions for type-aware sorting
112123 * @returns SQL ORDER BY clause or undefined if no sort specified
113- * @throws Error if field name is invalid
124+ * @throws { TableQueryValidationError } if field name or sort direction is invalid
114125 *
115126 * @example
116127 * buildSortClause({ name: 'asc', age: 'desc' }, 'user_table_rows')
@@ -133,7 +144,9 @@ export function buildSortClause(
133144 validateFieldName ( field )
134145
135146 if ( direction !== 'asc' && direction !== 'desc' ) {
136- throw new Error ( `Invalid sort direction "${ direction } ". Must be "asc" or "desc".` )
147+ throw new TableQueryValidationError (
148+ `Invalid sort direction "${ direction } ". Must be "asc" or "desc".`
149+ )
137150 }
138151
139152 const columnType = columnTypeMap . get ( field )
@@ -148,15 +161,15 @@ export function buildSortClause(
148161 * Field names must match the NAME_PATTERN (alphanumeric + underscore, starting with letter/underscore).
149162 *
150163 * @param field - The field name to validate
151- * @throws Error if field name is invalid
164+ * @throws { TableQueryValidationError } if field name is invalid
152165 */
153166function validateFieldName ( field : string ) : void {
154167 if ( ! field || typeof field !== 'string' ) {
155- throw new Error ( 'Field name must be a non-empty string' )
168+ throw new TableQueryValidationError ( 'Field name must be a non-empty string' )
156169 }
157170
158171 if ( ! NAME_PATTERN . test ( field ) ) {
159- throw new Error (
172+ throw new TableQueryValidationError (
160173 `Invalid field name "${ field } ". Field names must start with a letter or underscore, followed by alphanumeric characters or underscores.`
161174 )
162175 }
@@ -166,11 +179,11 @@ function validateFieldName(field: string): void {
166179 * Validates an operator to ensure it's in the allowed list.
167180 *
168181 * @param operator - The operator to validate
169- * @throws Error if operator is not allowed
182+ * @throws { TableQueryValidationError } if operator is not allowed
170183 */
171184function validateOperator ( operator : string ) : void {
172185 if ( ! ALLOWED_OPERATORS . has ( operator ) ) {
173- throw new Error (
186+ throw new TableQueryValidationError (
174187 `Invalid operator "${ operator } ". Allowed operators: ${ Array . from ( ALLOWED_OPERATORS ) . join ( ', ' ) } `
175188 )
176189 }
@@ -190,7 +203,7 @@ function validateOperator(operator: string): void {
190203 * object with operators like $eq, $gt, $in, etc.
191204 * @returns Array of SQL condition fragments. Multiple conditions are returned
192205 * when the condition object contains multiple operators.
193- * @throws Error if field name is invalid or operator is not allowed
206+ * @throws { TableQueryValidationError } if field name is invalid or operator is not allowed
194207 */
195208function buildFieldCondition (
196209 tableName : string ,
@@ -260,7 +273,9 @@ function buildFieldCondition(
260273 break
261274
262275 default :
263- // This should never happen due to validateOperator, but added for completeness
276+ // This should never happen due to validateOperator, but added for completeness.
277+ // Throw a plain Error (→ 500) since reaching this default means the switch
278+ // and ALLOWED_OPERATORS have drifted — that's a programmer error, not a caller error.
264279 throw new Error ( `Unsupported operator: ${ op } ` )
265280 }
266281 }
0 commit comments