@@ -14,6 +14,7 @@ import { userTableDefinitions, userTableRows } from '@sim/db/schema'
1414import { createLogger } from '@sim/logger'
1515import { and , count , eq , sql } from 'drizzle-orm'
1616import { columnMatchesRef , generateColumnId , getColumnId } from '@/lib/table/column-keys'
17+ import { isValueCompatibleWithColumnType } from '@/lib/table/column-types'
1718import { COLUMN_TYPES , NAME_PATTERN , TABLE_LIMITS } from '@/lib/table/constants'
1819import { assertColumnDestructive , assertSchemaMutable } from '@/lib/table/mutation-locks'
1920import type { DbTransaction } from '@/lib/table/planner'
@@ -35,11 +36,7 @@ import type {
3536 UpdateColumnOptionsData ,
3637 UpdateColumnTypeData ,
3738} from '@/lib/table/types'
38- import {
39- resolveSelectOptionId ,
40- splitMultiSelectInput ,
41- validateColumnDefinition ,
42- } from '@/lib/table/validation'
39+ import { validateColumnDefinition } from '@/lib/table/validation'
4340import { assertValidSchema , stripGroupDeps } from '@/lib/table/workflow-columns'
4441
4542const logger = createLogger ( 'TableColumnService' )
@@ -1155,6 +1152,10 @@ export function selectValueForConversion(column: ColumnDefinition, value: unknow
11551152 *
11561153 * Callers converting *away* from `select` must pass the resolved option
11571154 * name(s), not the stored ids — see {@link selectValueForConversion}.
1155+ *
1156+ * Delegates to the column-type registry (`column-types.ts`) so a new column
1157+ * type's conversion rule only needs to be added in one place — see
1158+ * `ColumnTypeDefinition.isCompatible`.
11581159 */
11591160export function isValueCompatibleWithType (
11601161 value : unknown ,
@@ -1163,59 +1164,10 @@ export function isValueCompatibleWithType(
11631164 targetMultiple = false ,
11641165 targetRequired = false
11651166) : boolean {
1166- if ( value === null || value === undefined ) return true
1167-
1168- switch ( targetType ) {
1169- case 'string' :
1170- // Arrays and objects can't become text — the write-path coercion rejects
1171- // them and would null the cell. Multi-select values are flattened before
1172- // this check, so anything still structured here is genuinely lossy.
1173- return typeof value !== 'object'
1174- case 'select' : {
1175- // A cleared select cell is written as '' — still convertible, unless the
1176- // target is required. Required only rejects null/undefined on a write, so
1177- // a required string column legitimately holds ''; the migration turns that
1178- // into null (or [] for a multi), and every later update of that row would
1179- // then fail its own required check.
1180- if ( value === '' ) return ! targetRequired
1181- // Read the value exactly as the write-path coercion will. A multi target
1182- // splits a comma-delimited string, so a multiselect → text → multiselect
1183- // round-trip (text holding this feature's own `Bug, Docs` export shape)
1184- // stays convertible instead of being rejected as one unknown option.
1185- const parts = targetMultiple
1186- ? splitMultiSelectInput ( value as JsonValue )
1187- : Array . isArray ( value )
1188- ? value
1189- : [ value ]
1190- // A single-select target can't hold several options. `updateColumnOptions`
1191- // blocks the same transition; without this the next coerce would silently
1192- // keep only the first id.
1193- if ( ! targetMultiple && parts . length > 1 ) return false
1194- return parts . every ( ( v ) => resolveSelectOptionId ( v as JsonValue , targetOptions ) !== null )
1195- }
1196- case 'number' : {
1197- if ( typeof value === 'number' ) return Number . isFinite ( value )
1198- if ( typeof value === 'string' ) {
1199- const num = Number ( value )
1200- return Number . isFinite ( num ) && value . trim ( ) !== ''
1201- }
1202- return false
1203- }
1204- case 'boolean' : {
1205- if ( typeof value === 'boolean' ) return true
1206- if ( typeof value === 'string' )
1207- return [ 'true' , 'false' , '1' , '0' ] . includes ( value . toLowerCase ( ) )
1208- if ( typeof value === 'number' ) return value === 0 || value === 1
1209- return false
1210- }
1211- case 'date' : {
1212- if ( value instanceof Date ) return ! Number . isNaN ( value . getTime ( ) )
1213- if ( typeof value === 'string' ) return ! Number . isNaN ( Date . parse ( value ) )
1214- return false
1215- }
1216- case 'json' :
1217- return true
1218- default :
1219- return false
1220- }
1167+ return isValueCompatibleWithColumnType ( value , {
1168+ type : targetType ,
1169+ options : targetOptions ,
1170+ multiple : targetMultiple ,
1171+ required : targetRequired ,
1172+ } )
12211173}
0 commit comments