Skip to content

Commit e4ded13

Browse files
committed
feat(tables): preview referenced rows inline
1 parent f011e66 commit e4ded13

55 files changed

Lines changed: 5131 additions & 260 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/docs/openapi-v2-tables.json

Lines changed: 69 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4987,7 +4987,8 @@
49874987
"date",
49884988
"ttl",
49894989
"json",
4990-
"select"
4990+
"select",
4991+
"reference"
49914992
],
49924993
"description": "Data type of values stored in the column."
49934994
},
@@ -5036,6 +5037,12 @@
50365037
"description": "ISO 4217 code for a currency column, normalized to uppercase.",
50375038
"type": "string",
50385039
"pattern": "^[A-Za-z]{3}$"
5040+
},
5041+
"referenceTableId": {
5042+
"description": "Target table whose row IDs are stored by a reference column.",
5043+
"type": "string",
5044+
"minLength": 1,
5045+
"maxLength": 128
50395046
}
50405047
},
50415048
"required": ["name", "type", "required", "unique"],
@@ -5274,7 +5281,8 @@
52745281
"date",
52755282
"ttl",
52765283
"json",
5277-
"select"
5284+
"select",
5285+
"reference"
52785286
],
52795287
"description": "Column data type."
52805288
},
@@ -5316,6 +5324,12 @@
53165324
"description": "ISO 4217 code for currency columns.",
53175325
"type": "string",
53185326
"pattern": "^[A-Za-z]{3}$"
5327+
},
5328+
"referenceTableId": {
5329+
"description": "Target table for reference columns.",
5330+
"type": "string",
5331+
"minLength": 1,
5332+
"maxLength": 128
53195333
}
53205334
},
53215335
"required": ["name", "type"],
@@ -5462,7 +5476,8 @@
54625476
"date",
54635477
"ttl",
54645478
"json",
5465-
"select"
5479+
"select",
5480+
"reference"
54665481
],
54675482
"description": "Data type of values stored in the column."
54685483
},
@@ -5511,6 +5526,12 @@
55115526
"description": "ISO 4217 code for a currency column, normalized to uppercase.",
55125527
"type": "string",
55135528
"pattern": "^[A-Za-z]{3}$"
5529+
},
5530+
"referenceTableId": {
5531+
"description": "Target table whose row IDs are stored by a reference column.",
5532+
"type": "string",
5533+
"minLength": 1,
5534+
"maxLength": 128
55145535
}
55155536
},
55165537
"required": ["name", "type", "required", "unique"],
@@ -5571,7 +5592,8 @@
55715592
"date",
55725593
"ttl",
55735594
"json",
5574-
"select"
5595+
"select",
5596+
"reference"
55755597
],
55765598
"description": "Column data type."
55775599
},
@@ -5614,6 +5636,12 @@
56145636
"type": "string",
56155637
"pattern": "^[A-Za-z]{3}$"
56165638
},
5639+
"referenceTableId": {
5640+
"description": "Target table for reference columns.",
5641+
"type": "string",
5642+
"minLength": 1,
5643+
"maxLength": 128
5644+
},
56175645
"position": {
56185646
"description": "Zero-based insertion position for the column.",
56195647
"type": "integer",
@@ -5669,7 +5697,17 @@
56695697
"type": {
56705698
"description": "Replacement column data type.",
56715699
"type": "string",
5672-
"enum": ["string", "number", "currency", "boolean", "date", "ttl", "json", "select"]
5700+
"enum": [
5701+
"string",
5702+
"number",
5703+
"currency",
5704+
"boolean",
5705+
"date",
5706+
"ttl",
5707+
"json",
5708+
"select",
5709+
"reference"
5710+
]
56735711
},
56745712
"required": {
56755713
"description": "Whether inserts must supply a value for this column.",
@@ -5709,6 +5747,12 @@
57095747
"description": "Replacement ISO 4217 code for a currency column.",
57105748
"type": "string",
57115749
"pattern": "^[A-Za-z]{3}$"
5750+
},
5751+
"referenceTableId": {
5752+
"description": "Replacement target table for a reference column.",
5753+
"type": "string",
5754+
"minLength": 1,
5755+
"maxLength": 128
57125756
}
57135757
},
57145758
"additionalProperties": false,
@@ -7441,7 +7485,8 @@
74417485
"date",
74427486
"ttl",
74437487
"json",
7444-
"select"
7488+
"select",
7489+
"reference"
74457490
],
74467491
"description": "Data type of values stored in the column."
74477492
},
@@ -7490,6 +7535,12 @@
74907535
"description": "ISO 4217 code for a currency column, normalized to uppercase.",
74917536
"type": "string",
74927537
"pattern": "^[A-Za-z]{3}$"
7538+
},
7539+
"referenceTableId": {
7540+
"description": "Target table whose row IDs are stored by a reference column.",
7541+
"type": "string",
7542+
"minLength": 1,
7543+
"maxLength": 128
74937544
}
74947545
},
74957546
"required": ["name", "type", "required", "unique"],
@@ -7650,7 +7701,8 @@
76507701
"date",
76517702
"ttl",
76527703
"json",
7653-
"select"
7704+
"select",
7705+
"reference"
76547706
],
76557707
"description": "Output column data type."
76567708
},
@@ -7800,7 +7852,8 @@
78007852
"date",
78017853
"ttl",
78027854
"json",
7803-
"select"
7855+
"select",
7856+
"reference"
78047857
],
78057858
"description": "Output column data type."
78067859
},
@@ -7927,7 +7980,8 @@
79277980
"date",
79287981
"ttl",
79297982
"json",
7930-
"select"
7983+
"select",
7984+
"reference"
79317985
],
79327986
"description": "Data type of values stored in the column."
79337987
},
@@ -7976,6 +8030,12 @@
79768030
"description": "ISO 4217 code for a currency column, normalized to uppercase.",
79778031
"type": "string",
79788032
"pattern": "^[A-Za-z]{3}$"
8033+
},
8034+
"referenceTableId": {
8035+
"description": "Target table whose row IDs are stored by a reference column.",
8036+
"type": "string",
8037+
"minLength": 1,
8038+
"maxLength": 128
79798039
}
79808040
},
79818041
"required": ["name", "type", "required", "unique"],

apps/sim/app/api/table/[tableId]/columns/route.test.ts

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,13 @@ vi.mock('@/lib/table/wire', () => ({
5555
vi.mock('@/app/api/table/utils', () => ({
5656
accessError: () => new Response('denied', { status: 403 }),
5757
checkAccess: mockCheckAccess,
58+
orchestrationErrorResponse: (error: unknown) =>
59+
error instanceof OrchestrationError
60+
? NextResponse.json(
61+
{ error: error.message },
62+
{ status: statusForOrchestrationError(error.code) }
63+
)
64+
: null,
5865
orchestrationOutcomeErrorResponse: (
5966
outcome: { error?: string; errorCode?: OrchestrationErrorCode },
6067
fallback: string
@@ -73,7 +80,7 @@ import {
7380
type OrchestrationErrorCode,
7481
statusForOrchestrationError,
7582
} from '@/lib/core/orchestration/types'
76-
import { PATCH } from '@/app/api/table/[tableId]/columns/route'
83+
import { PATCH, POST } from '@/app/api/table/[tableId]/columns/route'
7784

7885
const WORKSPACE_ID = '11111111-1111-4111-8111-111111111111'
7986

@@ -88,6 +95,49 @@ function patch(updates: Record<string, unknown>) {
8895
)
8996
}
9097

98+
function post(column: Record<string, unknown>) {
99+
return POST(
100+
new NextRequest('http://localhost/api/table/t1/columns', {
101+
method: 'POST',
102+
body: JSON.stringify({ workspaceId: WORKSPACE_ID, column }),
103+
headers: { 'content-type': 'application/json' },
104+
}),
105+
{ params: Promise.resolve({ tableId: 't1' }) }
106+
)
107+
}
108+
109+
describe('POST /api/table/[tableId]/columns — Reference feature gate', () => {
110+
beforeEach(() => {
111+
vi.clearAllMocks()
112+
hybridAuthMockFns.mockCheckSessionOrInternalAuth.mockResolvedValue({
113+
success: true,
114+
userId: 'user-1',
115+
authType: 'session',
116+
})
117+
mockCheckAccess.mockResolvedValue({
118+
ok: true,
119+
table: { workspaceId: WORKSPACE_ID, schema: { columns: [] } },
120+
})
121+
})
122+
123+
it('returns 403 when Reference columns are disabled', async () => {
124+
mockAddTableColumn.mockRejectedValue(
125+
new OrchestrationError('forbidden', 'Reference columns are not enabled for this deployment')
126+
)
127+
128+
const response = await post({
129+
name: 'Account',
130+
type: 'reference',
131+
referenceTableId: 'tbl_accounts',
132+
})
133+
134+
expect(response.status).toBe(403)
135+
expect(await response.json()).toEqual({
136+
error: 'Reference columns are not enabled for this deployment',
137+
})
138+
})
139+
})
140+
91141
describe('PATCH /api/table/[tableId]/columns — pre-flight guards', () => {
92142
beforeEach(() => {
93143
vi.clearAllMocks()

apps/sim/app/api/table/[tableId]/columns/route.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { normalizeColumn } from '@/lib/table/wire'
1717
import {
1818
accessError,
1919
checkAccess,
20+
orchestrationErrorResponse,
2021
orchestrationOutcomeErrorResponse,
2122
rootErrorMessage,
2223
tableLockErrorResponse,
@@ -69,6 +70,9 @@ export const POST = withRouteHandler(async (request: NextRequest, context: Colum
6970
return validationErrorResponse(error, 'Invalid request data')
7071
}
7172

73+
const classified = orchestrationErrorResponse(error)
74+
if (classified) return classified
75+
7276
const msg = rootErrorMessage(error)
7377
if (
7478
msg.includes('already exists') ||

apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/column-config-sidebar/column-config-sidebar.test.tsx

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
88
interface ComboboxOption {
99
label: string
1010
value: string
11+
disabled?: boolean
1112
}
1213

1314
interface ComboboxProps {
@@ -16,6 +17,7 @@ interface ComboboxProps {
1617
placeholder?: string
1718
searchable?: boolean
1819
searchPlaceholder?: string
20+
disabled?: boolean
1921
onChange?: (value: string) => void
2022
}
2123

@@ -137,6 +139,7 @@ describe('ColumnConfigSidebar', () => {
137139
existingColumn={null}
138140
workspaceId='workspace-1'
139141
tableId='table-current'
142+
referenceColumnsEnabled
140143
/>
141144
)
142145
})
@@ -173,6 +176,7 @@ describe('ColumnConfigSidebar', () => {
173176
existingColumn={null}
174177
workspaceId='workspace-1'
175178
tableId='table-current'
179+
referenceColumnsEnabled
176180
/>
177181
)
178182
})
@@ -198,6 +202,7 @@ describe('ColumnConfigSidebar', () => {
198202
}}
199203
workspaceId='workspace-1'
200204
tableId='table-current'
205+
referenceColumnsEnabled
201206
/>
202207
)
203208
})
@@ -214,6 +219,32 @@ describe('ColumnConfigSidebar', () => {
214219
})
215220
})
216221

222+
it('keeps an existing Reference column readable but not retargetable when disabled', async () => {
223+
await act(async () => {
224+
root.render(
225+
<ColumnConfigSidebar
226+
config={{ mode: 'edit', columnName: 'col-reference' }}
227+
onClose={vi.fn()}
228+
existingColumn={{
229+
id: 'col-reference',
230+
name: 'Related row',
231+
type: 'reference',
232+
referenceTableId: 'table-current',
233+
}}
234+
workspaceId='workspace-1'
235+
tableId='table-current'
236+
referenceColumnsEnabled={false}
237+
/>
238+
)
239+
})
240+
241+
expect(mockUseTablesList).toHaveBeenCalledWith('workspace-1', 'active', { enabled: false })
242+
expect(findCombobox('Select table')?.disabled).toBe(true)
243+
expect(findCombobox('Select type')?.options).toContainEqual(
244+
expect.objectContaining({ value: 'reference', disabled: true })
245+
)
246+
})
247+
217248
it('keeps Select options in the edit sidebar', async () => {
218249
await act(async () => {
219250
root.render(
@@ -228,6 +259,7 @@ describe('ColumnConfigSidebar', () => {
228259
}}
229260
workspaceId='workspace-1'
230261
tableId='table-current'
262+
referenceColumnsEnabled
231263
/>
232264
)
233265
})

0 commit comments

Comments
 (0)