Skip to content

Commit 80642d5

Browse files
j15zclaude
andcommitted
refactor(tables): migrate column type-conversion compatibility to the registry
isValueCompatibleWithType's switch — the gate updateColumnType checks before committing a column's type change — is replaced by a delegate to the registry's isValueCompatibleWithColumnType, so a new column type's conversion rules only need to be declared once. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 parent b167896 commit 80642d5

1 file changed

Lines changed: 12 additions & 60 deletions

File tree

apps/sim/lib/table/columns/service.ts

Lines changed: 12 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { userTableDefinitions, userTableRows } from '@sim/db/schema'
1414
import { createLogger } from '@sim/logger'
1515
import { and, count, eq, sql } from 'drizzle-orm'
1616
import { columnMatchesRef, generateColumnId, getColumnId } from '@/lib/table/column-keys'
17+
import { isValueCompatibleWithColumnType } from '@/lib/table/column-types'
1718
import { COLUMN_TYPES, NAME_PATTERN, TABLE_LIMITS } from '@/lib/table/constants'
1819
import { assertColumnDestructive, assertSchemaMutable } from '@/lib/table/mutation-locks'
1920
import 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'
4340
import { assertValidSchema, stripGroupDeps } from '@/lib/table/workflow-columns'
4441

4542
const 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
*/
11591160
export 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

Comments
 (0)