Skip to content

Commit 118e4f6

Browse files
committed
updates
1 parent 292cd39 commit 118e4f6

File tree

14 files changed

+251
-11
lines changed

14 files changed

+251
-11
lines changed

apps/sim/app/api/table/route.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,14 @@ import { type NextRequest, NextResponse } from 'next/server'
66
import { z } from 'zod'
77
import { checkHybridAuth } from '@/lib/auth/hybrid'
88
import { generateRequestId } from '@/lib/core/utils/request'
9-
import { createTable, listTables, TABLE_LIMITS, type TableSchema } from '@/lib/table'
9+
import {
10+
canCreateTable,
11+
createTable,
12+
getWorkspaceTableLimits,
13+
listTables,
14+
TABLE_LIMITS,
15+
type TableSchema,
16+
} from '@/lib/table'
1017
import { normalizeColumn } from './utils'
1118

1219
const logger = createLogger('TableAPI')
@@ -141,6 +148,23 @@ export async function POST(request: NextRequest) {
141148
return NextResponse.json({ error: 'Access denied' }, { status: 403 })
142149
}
143150

151+
// Check billing plan limits
152+
const existingTables = await listTables(params.workspaceId)
153+
const { canCreate, maxTables } = await canCreateTable(params.workspaceId, existingTables.length)
154+
155+
if (!canCreate) {
156+
return NextResponse.json(
157+
{
158+
error: `Workspace has reached the maximum table limit (${maxTables}) for your plan. Please upgrade to create more tables.`,
159+
},
160+
{ status: 403 }
161+
)
162+
}
163+
164+
// Get plan-based row limits
165+
const planLimits = await getWorkspaceTableLimits(params.workspaceId)
166+
const maxRowsPerTable = planLimits.maxRowsPerTable
167+
144168
const normalizedSchema: TableSchema = {
145169
columns: params.schema.columns.map(normalizeColumn),
146170
}
@@ -152,6 +176,7 @@ export async function POST(request: NextRequest) {
152176
schema: normalizedSchema,
153177
workspaceId: params.workspaceId,
154178
userId: authResult.userId,
179+
maxRows: maxRowsPerTable,
155180
},
156181
requestId
157182
)

apps/sim/app/api/table/utils.ts

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,91 @@ import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
66

77
const logger = createLogger('TableUtils')
88

9+
export interface TableAccessResult {
10+
hasAccess: true
11+
table: TableDefinition
12+
}
13+
14+
export interface TableAccessDenied {
15+
hasAccess: false
16+
notFound?: boolean
17+
reason?: string
18+
}
19+
20+
export type TableAccessCheck = TableAccessResult | TableAccessDenied
21+
922
export type AccessResult = { ok: true; table: TableDefinition } | { ok: false; status: 404 | 403 }
1023

1124
export interface ApiErrorResponse {
1225
error: string
1326
details?: unknown
1427
}
1528

29+
/**
30+
* Check if a user has read access to a table.
31+
* Read access is granted if:
32+
* 1. User created the table, OR
33+
* 2. User has any permission on the table's workspace (read, write, or admin)
34+
*
35+
* Follows the same pattern as Knowledge Base access checks.
36+
*/
37+
export async function checkTableAccess(tableId: string, userId: string): Promise<TableAccessCheck> {
38+
const table = await getTableById(tableId)
39+
40+
if (!table) {
41+
return { hasAccess: false, notFound: true }
42+
}
43+
44+
// Case 1: User created the table
45+
if (table.createdBy === userId) {
46+
return { hasAccess: true, table }
47+
}
48+
49+
// Case 2: Table belongs to a workspace the user has permissions for
50+
const userPermission = await getUserEntityPermissions(userId, 'workspace', table.workspaceId)
51+
if (userPermission !== null) {
52+
return { hasAccess: true, table }
53+
}
54+
55+
return { hasAccess: false, reason: 'User does not have access to this table' }
56+
}
57+
58+
/**
59+
* Check if a user has write access to a table.
60+
* Write access is granted if:
61+
* 1. User created the table, OR
62+
* 2. User has write or admin permissions on the table's workspace
63+
*
64+
* Follows the same pattern as Knowledge Base write access checks.
65+
*/
66+
export async function checkTableWriteAccess(
67+
tableId: string,
68+
userId: string
69+
): Promise<TableAccessCheck> {
70+
const table = await getTableById(tableId)
71+
72+
if (!table) {
73+
return { hasAccess: false, notFound: true }
74+
}
75+
76+
// Case 1: User created the table
77+
if (table.createdBy === userId) {
78+
return { hasAccess: true, table }
79+
}
80+
81+
// Case 2: Table belongs to a workspace and user has write/admin permissions
82+
const userPermission = await getUserEntityPermissions(userId, 'workspace', table.workspaceId)
83+
if (userPermission === 'write' || userPermission === 'admin') {
84+
return { hasAccess: true, table }
85+
}
86+
87+
return { hasAccess: false, reason: 'User does not have write access to this table' }
88+
}
89+
90+
/**
91+
* @deprecated Use checkTableAccess or checkTableWriteAccess instead.
92+
* Legacy access check function for backwards compatibility.
93+
*/
1694
export async function checkAccess(
1795
tableId: string,
1896
userId: string,
@@ -48,6 +126,21 @@ export function accessError(
48126
return NextResponse.json({ error: message }, { status: result.status })
49127
}
50128

129+
/**
130+
* Converts a TableAccessDenied result to an appropriate HTTP response.
131+
* Use with checkTableAccess or checkTableWriteAccess.
132+
*/
133+
export function tableAccessError(
134+
result: TableAccessDenied,
135+
requestId: string,
136+
context?: string
137+
): NextResponse {
138+
const status = result.notFound ? 404 : 403
139+
const message = result.notFound ? 'Table not found' : (result.reason ?? 'Access denied')
140+
logger.warn(`[${requestId}] ${message}${context ? `: ${context}` : ''}`)
141+
return NextResponse.json({ error: message }, { status })
142+
}
143+
51144
export async function verifyTableWorkspace(tableId: string, workspaceId: string): Promise<boolean> {
52145
const table = await getTableById(tableId)
53146
return table?.workspaceId === workspaceId

apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/row-modal.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { useParams } from 'next/navigation'
77
import {
88
Button,
99
Checkbox,
10+
Input,
1011
Label,
1112
Modal,
1213
ModalBody,
@@ -15,7 +16,6 @@ import {
1516
ModalHeader,
1617
Textarea,
1718
} from '@/components/emcn'
18-
import { Input } from '@/components/ui/input'
1919
import type { ColumnDefinition, TableInfo, TableRow } from '@/lib/table'
2020

2121
const logger = createLogger('RowModal')

apps/sim/app/workspace/[workspaceId]/tables/components/tables-view.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@
33
import { useState } from 'react'
44
import { Database, Plus, Search } from 'lucide-react'
55
import { useParams } from 'next/navigation'
6-
import { Button, Tooltip } from '@/components/emcn'
7-
import { Input } from '@/components/ui/input'
6+
import { Button, Input, Tooltip } from '@/components/emcn'
87
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
98
import { useTablesList } from '@/hooks/queries/use-tables'
109
import { useDebounce } from '@/hooks/use-debounce'

apps/sim/blocks/blocks/table.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { TableIcon } from '@/components/icons'
2+
import { TABLE_LIMITS } from '@/lib/table'
23
import { filterRulesToFilter, sortRulesToSort } from '@/lib/table/query-builder/converters'
34
import type { BlockConfig } from '@/blocks/types'
45
import type { TableQueryResponse } from '@/tools/table/types'
@@ -277,7 +278,7 @@ Return ONLY the data JSON:`,
277278
278279
### INSTRUCTION
279280
Return ONLY a valid JSON array of objects. Each object represents one row. No explanations or markdown.
280-
Maximum 1000 rows per batch.
281+
Maximum ${TABLE_LIMITS.MAX_BATCH_INSERT_SIZE} rows per batch.
281282
282283
IMPORTANT: Reference the table schema to know which columns exist and their types.
283284

apps/sim/lib/table/billing.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/**
2+
* Billing helpers for table feature limits.
3+
*
4+
* Uses workspace billing account to determine plan-based limits.
5+
*/
6+
7+
import { createLogger } from '@sim/logger'
8+
import { getUserSubscriptionState } from '@/lib/billing/core/subscription'
9+
import { getWorkspaceBilledAccountUserId } from '@/lib/workspaces/utils'
10+
import { type PlanName, TABLE_PLAN_LIMITS, type TablePlanLimits } from './constants'
11+
12+
const logger = createLogger('TableBilling')
13+
14+
/**
15+
* Gets the table limits for a workspace based on its billing plan.
16+
*
17+
* Uses the workspace's billed account user to determine the subscription plan,
18+
* then returns the corresponding table limits.
19+
*
20+
* @param workspaceId - The workspace ID to get limits for
21+
* @returns Table limits based on the workspace's billing plan
22+
*/
23+
export async function getWorkspaceTableLimits(workspaceId: string): Promise<TablePlanLimits> {
24+
try {
25+
const billedAccountUserId = await getWorkspaceBilledAccountUserId(workspaceId)
26+
27+
if (!billedAccountUserId) {
28+
logger.warn('No billed account found for workspace, using free tier limits', { workspaceId })
29+
return TABLE_PLAN_LIMITS.free
30+
}
31+
32+
const subscriptionState = await getUserSubscriptionState(billedAccountUserId)
33+
const planName = subscriptionState.planName as PlanName
34+
35+
const limits = TABLE_PLAN_LIMITS[planName] ?? TABLE_PLAN_LIMITS.free
36+
37+
logger.info('Retrieved workspace table limits', {
38+
workspaceId,
39+
billedAccountUserId,
40+
planName,
41+
limits,
42+
})
43+
44+
return limits
45+
} catch (error) {
46+
logger.error('Error getting workspace table limits, falling back to free tier', {
47+
workspaceId,
48+
error,
49+
})
50+
return TABLE_PLAN_LIMITS.free
51+
}
52+
}
53+
54+
/**
55+
* Checks if a workspace can create more tables based on its plan limits.
56+
*
57+
* @param workspaceId - The workspace ID to check
58+
* @param currentTableCount - The current number of tables in the workspace
59+
* @returns Object with canCreate boolean and limit info
60+
*/
61+
export async function canCreateTable(
62+
workspaceId: string,
63+
currentTableCount: number
64+
): Promise<{ canCreate: boolean; maxTables: number; currentCount: number }> {
65+
const limits = await getWorkspaceTableLimits(workspaceId)
66+
67+
return {
68+
canCreate: currentTableCount < limits.maxTables,
69+
maxTables: limits.maxTables,
70+
currentCount: currentTableCount,
71+
}
72+
}
73+
74+
/**
75+
* Gets the maximum rows allowed per table for a workspace based on its plan.
76+
*
77+
* @param workspaceId - The workspace ID
78+
* @returns Maximum rows per table (-1 for unlimited)
79+
*/
80+
export async function getMaxRowsPerTable(workspaceId: string): Promise<number> {
81+
const limits = await getWorkspaceTableLimits(workspaceId)
82+
return limits.maxRowsPerTable
83+
}

apps/sim/lib/table/constants.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,35 @@ export const TABLE_LIMITS = {
2323
MAX_BULK_OPERATION_SIZE: 1000,
2424
} as const
2525

26+
/**
27+
* Plan-based table limits.
28+
*/
29+
export const TABLE_PLAN_LIMITS = {
30+
free: {
31+
maxTables: 3,
32+
maxRowsPerTable: 1000,
33+
},
34+
pro: {
35+
maxTables: 25,
36+
maxRowsPerTable: 5000,
37+
},
38+
team: {
39+
maxTables: 100,
40+
maxRowsPerTable: 10000,
41+
},
42+
enterprise: {
43+
maxTables: 10000,
44+
maxRowsPerTable: 1000000,
45+
},
46+
} as const
47+
48+
export type PlanName = keyof typeof TABLE_PLAN_LIMITS
49+
50+
export interface TablePlanLimits {
51+
maxTables: number
52+
maxRowsPerTable: number
53+
}
54+
2655
export const COLUMN_TYPES = ['string', 'number', 'boolean', 'date', 'json'] as const
2756

2857
export const NAME_PATTERN = /^[a-z_][a-z0-9_]*$/i

apps/sim/lib/table/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
* Import hooks directly from '@/lib/table/hooks' in client components.
66
*/
77

8+
export * from './billing'
89
export * from './constants'
910
export * from './llm'
1011
export * from './query-builder'

apps/sim/lib/table/service.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,14 +152,17 @@ export async function createTable(
152152
const tableId = `tbl_${crypto.randomUUID().replace(/-/g, '')}`
153153
const now = new Date()
154154

155+
// Use provided maxRows (from billing plan) or fall back to default
156+
const maxRows = data.maxRows ?? TABLE_LIMITS.MAX_ROWS_PER_TABLE
157+
155158
const newTable = {
156159
id: tableId,
157160
name: data.name,
158161
description: data.description ?? null,
159162
schema: data.schema,
160163
workspaceId: data.workspaceId,
161164
createdBy: data.userId,
162-
maxRows: TABLE_LIMITS.MAX_ROWS_PER_TABLE,
165+
maxRows,
163166
createdAt: now,
164167
updatedAt: now,
165168
}

apps/sim/lib/table/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,8 @@ export interface CreateTableData {
149149
schema: TableSchema
150150
workspaceId: string
151151
userId: string
152+
/** Optional max rows override based on billing plan. Defaults to TABLE_LIMITS.MAX_ROWS_PER_TABLE. */
153+
maxRows?: number
152154
}
153155

154156
export interface InsertRowData {

0 commit comments

Comments
 (0)