Skip to content

Commit a0c9bba

Browse files
fix(tables): reject the multiple flag on non-select columns
A create/add payload could persist multiple: true on a string column, where it sits inert until updateColumnType inherits it via `data.multiple ?? column.multiple` — silently turning an intended single-select into a multiselect and rewriting every cell as an array. Rejected now in both the service validator and the shared contract refine, which the internal and v1 column routes both consume.
1 parent e09a799 commit a0c9bba

3 files changed

Lines changed: 52 additions & 6 deletions

File tree

apps/sim/lib/api/contracts/tables.ts

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,15 @@ export const selectOptionsSchema = z
3939

4040
/**
4141
* Cross-field rule: a `select` column must declare a non-empty option set;
42-
* other types must not carry options. Skipped when `type` is absent (an
43-
* options-only update on an existing select column).
42+
* other types must not carry options or `multiple`. Skipped when `type` is
43+
* absent (an options-only update on an existing select column).
4444
*/
4545
function refineColumnOptions(
46-
data: { type?: (typeof COLUMN_TYPES)[number]; options?: z.infer<typeof selectOptionsSchema> },
46+
data: {
47+
type?: (typeof COLUMN_TYPES)[number]
48+
options?: z.infer<typeof selectOptionsSchema>
49+
multiple?: boolean
50+
},
4751
ctx: z.RefinementCtx
4852
): void {
4953
if (data.type === 'select') {
@@ -54,13 +58,25 @@ function refineColumnOptions(
5458
message: 'A select column must define at least one option',
5559
})
5660
}
57-
} else if (data.type !== undefined && data.options && data.options.length > 0) {
61+
return
62+
}
63+
if (data.type === undefined) return
64+
if (data.options && data.options.length > 0) {
5865
ctx.addIssue({
5966
code: 'custom',
6067
path: ['options'],
6168
message: 'options are only allowed on select columns',
6269
})
6370
}
71+
// `multiple` stored on a non-select column is inert until a later
72+
// convert-to-select inherits it, silently producing a multiselect.
73+
if (data.multiple) {
74+
ctx.addIssue({
75+
code: 'custom',
76+
path: ['multiple'],
77+
message: 'multiple is only allowed on select columns',
78+
})
79+
}
6480
}
6581

6682
/**

apps/sim/lib/table/__tests__/validation.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,27 @@ describe('Validation', () => {
8787
}
8888
})
8989

90+
it('rejects select-only fields on a non-select column', () => {
91+
// Both are inert on a string column, but `updateColumnType` inherits them
92+
// on a later convert-to-select — options would be silently replaced and
93+
// `multiple` would turn an intended single-select into a multiselect.
94+
const withOptions = validateColumnDefinition({
95+
name: 'status',
96+
type: 'string',
97+
options: [{ id: 'opt_a', name: 'Open' }],
98+
})
99+
expect(withOptions.valid).toBe(false)
100+
expect(withOptions.errors[0]).toContain('cannot define options')
101+
102+
const withMultiple = validateColumnDefinition({
103+
name: 'status',
104+
type: 'string',
105+
multiple: true,
106+
})
107+
expect(withMultiple.valid).toBe(false)
108+
expect(withMultiple.errors[0]).toContain('cannot be multiple')
109+
})
110+
90111
it('should reject empty column name', () => {
91112
const result = validateColumnDefinition({ name: '', type: 'string' })
92113
expect(result.valid).toBe(false)

apps/sim/lib/table/validation.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -775,8 +775,17 @@ export function validateColumnDefinition(column: ColumnDefinition): ValidationRe
775775
if (column.unique) {
776776
errors.push(`Column "${column.name}" of type "select" cannot be unique`)
777777
}
778-
} else if (column.options !== undefined) {
779-
errors.push(`Column "${column.name}" cannot define options for type "${column.type}"`)
778+
} else {
779+
if (column.options !== undefined) {
780+
errors.push(`Column "${column.name}" cannot define options for type "${column.type}"`)
781+
}
782+
// A stored `multiple` on a non-select column is inert until the column is
783+
// converted, at which point `updateColumnType` inherits it — silently
784+
// turning an intended single-select into a multiselect and rewriting every
785+
// cell as an array.
786+
if (column.multiple) {
787+
errors.push(`Column "${column.name}" cannot be multiple for type "${column.type}"`)
788+
}
780789
}
781790

782791
return { valid: errors.length === 0, errors }

0 commit comments

Comments
 (0)