Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 3.8k
feat(tables): add select & multi-select column types#5873
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
55bf638d270a0f1fdfa3cb5ad728f7c6dd8fce848c4db5e88a0f898a06466642fbdc9cd55f94b60b15ccd06c3993e0b68a1138baa7cd6f77136f38ab2372880c465c8228ba6953ceaafb9598ef78fb0914cb21f861ccf5b31da1b61dd7237156f056f0045109258f675af6bbff4935655b4b793e3bef6f75d514b6ab46ed6f860fc0efab2809e2f1704faaf8e09a799a0c9bba7c0c24d7bce934cc6894433fea4428d8e0746fc8374b56be6061913c2586fe2File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -15,8 +15,10 @@ import { | ||
| deleteColumn, | ||
| renameColumn, | ||
| updateColumnConstraints, | ||
| updateColumnOptions, | ||
| updateColumnType, | ||
| } from '@/lib/table' | ||
| import { columnMatchesRef } from '@/lib/table/column-keys' | ||
| import { accessError, checkAccess, normalizeColumn, rootErrorMessage } from '@/app/api/table/utils' | ||
| const logger = createLogger('TableColumnsAPI') | ||
| @@ -68,7 +70,8 @@ export const POST = withRouteHandler(async (request: NextRequest, context: Colum | ||
| msg.includes('already exists') || | ||
| msg.includes('maximum column') || | ||
| msg.includes('Invalid column') || | ||
| msg.includes('exceeds maximum') | ||
| msg.includes('exceeds maximum') || | ||
| msg.includes('option') | ||
| ) { | ||
| return NextResponse.json({ error: msg }, { status: 400 }) | ||
| } | ||
| @@ -116,9 +119,50 @@ export const PATCH = withRouteHandler(async (request: NextRequest, context: Colu | ||
| ) | ||
| } | ||
| if (updates.type) { | ||
| // A payload that repeats the current type must not go through | ||
| // `updateColumnType` — it early-returns on an unchanged type and would drop | ||
| // any `options` alongside it. Only a real type change routes there; an | ||
| // unchanged type with options routes to the options-only update. | ||
| const currentColumn = table.schema.columns.find((c) => | ||
| columnMatchesRef(c, validated.columnName) | ||
| ) | ||
| const typeChanging = updates.type !== undefined && updates.type !== currentColumn?.type | ||
| // Every write below is its own locked transaction, so any of them paired | ||
| // with a constraint write that is going to fail commits and then errors. | ||
| // Gate on the type the column ENDS UP with, not on whether the type is | ||
| // changing: an options-only update on an existing select column carries the | ||
| // same hazard as a conversion does. | ||
| const resultingType = updates.type ?? currentColumn?.type | ||
| if (updates.unique === true && resultingType === 'select') { | ||
| return NextResponse.json({ error: 'Cannot set a select column as unique' }, { status: 400 }) | ||
| } | ||
| if (typeChanging) { | ||
| updatedTable = await updateColumnType( | ||
| { tableId, columnName: updates.name ?? validated.columnName, newType: updates.type }, | ||
| { | ||
| tableId, | ||
| columnName: updates.name ?? validated.columnName, | ||
| newType: updates.type as NonNullable<typeof updates.type>, | ||
| ...(updates.options !== undefined ? { options: updates.options } : {}), | ||
| ...(updates.multiple !== undefined ? { multiple: updates.multiple } : {}), | ||
| // Forwarded so the conversion validates against the constraint this | ||
| // same request is about to set, not the column's current one. | ||
| ...(updates.required !== undefined ? { required: updates.required } : {}), | ||
| }, | ||
| requestId | ||
| ) | ||
| } else if (updates.options !== undefined || updates.multiple !== undefined) { | ||
| updatedTable = await updateColumnOptions( | ||
| { | ||
| tableId, | ||
| columnName: updates.name ?? validated.columnName, | ||
| options: updates.options ?? currentColumn?.options ?? [], | ||
| ...(updates.multiple !== undefined ? { multiple: updates.multiple } : {}), | ||
| // Forwarded so the removal guard validates against the constraint this | ||
| // same request is about to set, not the column's current one. | ||
| ...(updates.required !== undefined ? { required: updates.required } : {}), | ||
| }, | ||
| requestId | ||
| ) | ||
| } | ||
| @@ -162,7 +206,8 @@ export const PATCH = withRouteHandler(async (request: NextRequest, context: Colu | ||
| msg.includes('Invalid column') || | ||
| msg.includes('exceeds maximum') || | ||
| msg.includes('incompatible') || | ||
| msg.includes('duplicate') | ||
cursor[bot] marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. cursor[bot] marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| msg.includes('duplicate') || | ||
| msg.includes('option') | ||
| ) { | ||
| return NextResponse.json({ error: msg }, { status: 400 }) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -14,8 +14,10 @@ import { | ||
| deleteColumn, | ||
| renameColumn, | ||
| updateColumnConstraints, | ||
| updateColumnOptions, | ||
| updateColumnType, | ||
| } from '@/lib/table' | ||
| import { columnMatchesRef } from '@/lib/table/column-keys' | ||
| import { accessError, checkAccess, normalizeColumn } from '@/app/api/table/utils' | ||
| import { | ||
| checkRateLimit, | ||
| @@ -86,7 +88,15 @@ export const POST = withRouteHandler(async (request: NextRequest, context: Colum | ||
| if (validationResponse) return validationResponse | ||
| if (error instanceof Error) { | ||
| if (error.message.includes('already exists') || error.message.includes('maximum column')) { | ||
| // Same caller-error set the internal columns route maps — an invalid | ||
| // select option set is a bad request, not a server fault. | ||
| if ( | ||
| error.message.includes('already exists') || | ||
| error.message.includes('maximum column') || | ||
| error.message.includes('Invalid column') || | ||
| error.message.includes('exceeds maximum') || | ||
| error.message.includes('option') | ||
| ) { | ||
| return NextResponse.json({ error: error.message }, { status: 400 }) | ||
| } | ||
| if (error.message === 'Table not found') { | ||
| @@ -138,9 +148,50 @@ export const PATCH = withRouteHandler(async (request: NextRequest, context: Colu | ||
| ) | ||
| } | ||
| if (updates.type) { | ||
cursor[bot] marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| // A payload that repeats the current type must not go through | ||
| // `updateColumnType` — it early-returns on an unchanged type and would drop | ||
| // any `options` alongside it. Only a real type change routes there; an | ||
| // unchanged type with options routes to the options-only update. | ||
| const currentColumn = table.schema.columns.find((c) => | ||
| columnMatchesRef(c, validated.columnName) | ||
| ) | ||
| const typeChanging = updates.type !== undefined && updates.type !== currentColumn?.type | ||
| // Every write below is its own locked transaction, so any of them paired | ||
| // with a constraint write that is going to fail commits and then errors. | ||
| // Gate on the type the column ENDS UP with, not on whether the type is | ||
| // changing: an options-only update on an existing select column carries the | ||
| // same hazard as a conversion does. | ||
| const resultingType = updates.type ?? currentColumn?.type | ||
| if (updates.unique === true && resultingType === 'select') { | ||
| return NextResponse.json({ error: 'Cannot set a select column as unique' }, { status: 400 }) | ||
| } | ||
| if (typeChanging) { | ||
| updatedTable = await updateColumnType( | ||
| { tableId, columnName: updates.name ?? validated.columnName, newType: updates.type }, | ||
| { | ||
| tableId, | ||
| columnName: updates.name ?? validated.columnName, | ||
| newType: updates.type as NonNullable<typeof updates.type>, | ||
| ...(updates.options !== undefined ? { options: updates.options } : {}), | ||
| ...(updates.multiple !== undefined ? { multiple: updates.multiple } : {}), | ||
| // Forwarded so the conversion validates against the constraint this | ||
| // same request is about to set, not the column's current one. | ||
| ...(updates.required !== undefined ? { required: updates.required } : {}), | ||
| }, | ||
| requestId | ||
| ) | ||
| } else if (updates.options !== undefined || updates.multiple !== undefined) { | ||
| updatedTable = await updateColumnOptions( | ||
| { | ||
| tableId, | ||
| columnName: updates.name ?? validated.columnName, | ||
| options: updates.options ?? currentColumn?.options ?? [], | ||
| ...(updates.multiple !== undefined ? { multiple: updates.multiple } : {}), | ||
| // Forwarded so the removal guard validates against the constraint this | ||
| // same request is about to set, not the column's current one. | ||
| ...(updates.required !== undefined ? { required: updates.required } : {}), | ||
| }, | ||
| requestId | ||
| ) | ||
| } | ||
| @@ -195,7 +246,8 @@ export const PATCH = withRouteHandler(async (request: NextRequest, context: Colu | ||
| msg.includes('Invalid column') || | ||
| msg.includes('exceeds maximum') || | ||
| msg.includes('incompatible') || | ||
| msg.includes('duplicate') | ||
| msg.includes('duplicate') || | ||
| msg.includes('option') | ||
| ) { | ||
| return NextResponse.json({ error: msg }, { status: 400 }) | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.