Skip to content

Commit ea0f98a

Browse files
committed
Merge remote-tracking branch 'origin/staging' into improvement/mship-tool-access
# Conflicts: # apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx # apps/sim/app/workspace/[workspaceId]/home/hooks/stream/handle-tool-event.ts # apps/sim/app/workspace/[workspaceId]/home/hooks/stream/stream-helpers.ts
2 parents 48f6a8a + e5f3965 commit ea0f98a

171 files changed

Lines changed: 30010 additions & 1317 deletions

File tree

Some content is hidden

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

.gitignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99
# bun specific
1010
bun-debug.log*
1111

12+
# cursor debug logs
13+
.cursor/debug-*.log
14+
1215
# this repo uses bun.lock; package-lock.json files are accidental
1316
package-lock.json
1417

@@ -44,6 +47,11 @@ dump.rdb
4447
.env.test
4548
.env.production
4649

50+
# editor swap files
51+
*.swp
52+
*.swo
53+
*.swn
54+
4755
# vercel
4856
.vercel
4957

apps/docs/components/icons.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1958,6 +1958,24 @@ export function WhatsAppIcon(props: SVGProps<SVGSVGElement>) {
19581958
)
19591959
}
19601960

1961+
export function SportmonksIcon(props: SVGProps<SVGSVGElement>) {
1962+
return (
1963+
<svg
1964+
{...props}
1965+
viewBox='0 0 25 24'
1966+
fill='none'
1967+
fillRule='evenodd'
1968+
xmlns='http://www.w3.org/2000/svg'
1969+
>
1970+
<path
1971+
d='M11.857 8.546c1.893 0 3.517.678 4.872 2.033 1.355 1.336 2.032 2.96 2.032 4.872 0 1.91-.677 3.535-2.032 4.871-1.355 1.355-2.979 2.032-4.872 2.032H1V17.093h10.857c.446 0 .825-.157 1.142-.473.334-.334.5-.724.5-1.17 0-.445-.166-.835-.5-1.17a1.558 1.558 0 00-1.142-.472H7.905c-1.912 0-3.537-.677-4.873-2.032C1.678 10.421 1 8.796 1 6.903 1 4.993 1.678 3.368 3.033 2.032 4.368.678 5.992 0 7.905 0h10.188V5.263H7.904a1.65 1.65 0 00-1.17.473 1.586 1.586 0 00-.472 1.169c0 .445.157.835.473 1.17.334.315.724.472 1.17.472h3.952z'
1972+
fill='currentColor'
1973+
/>
1974+
<circle cx='21.27' cy='20.123' r='2.732' fill='#FF0F50' />
1975+
</svg>
1976+
)
1977+
}
1978+
19611979
export function SquareIcon(props: SVGProps<SVGSVGElement>) {
19621980
return (
19631981
<svg {...props} viewBox='0 0 501.42 501.42' xmlns='http://www.w3.org/2000/svg'>

apps/docs/components/ui/icon-mapping.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@ import {
195195
SixtyfourIcon,
196196
SlackIcon,
197197
SmtpIcon,
198+
SportmonksIcon,
198199
SQSIcon,
199200
SquareIcon,
200201
SshIcon,
@@ -449,6 +450,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
449450
sixtyfour: SixtyfourIcon,
450451
slack: SlackIcon,
451452
smtp: SmtpIcon,
453+
sportmonks: SportmonksIcon,
452454
sqs: SQSIcon,
453455
square: SquareIcon,
454456
ssh: SshIcon,

apps/docs/content/docs/en/integrations/meta.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,7 @@
196196
"sixtyfour",
197197
"slack",
198198
"smtp",
199+
"sportmonks",
199200
"sqs",
200201
"square",
201202
"ssh",

apps/docs/content/docs/en/integrations/sportmonks.mdx

Lines changed: 1517 additions & 0 deletions
Large diffs are not rendered by default.

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

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,15 @@ const {
1313
mockDispatchAfterBatchInsert,
1414
mockMarkTableImporting,
1515
mockReleaseImportClaim,
16+
mockGetMaxRowsPerTable,
1617
} = vi.hoisted(() => ({
1718
mockCheckAccess: vi.fn(),
1819
mockImportAppendRows: vi.fn(),
1920
mockImportReplaceRows: vi.fn(),
2021
mockDispatchAfterBatchInsert: vi.fn(),
2122
mockMarkTableImporting: vi.fn(),
2223
mockReleaseImportClaim: vi.fn(),
24+
mockGetMaxRowsPerTable: vi.fn(),
2325
}))
2426

2527
vi.mock('@sim/utils/id', () => ({
@@ -65,6 +67,13 @@ vi.mock('@/lib/table/rows/service', () => ({
6567
dispatchAfterBatchInsert: mockDispatchAfterBatchInsert,
6668
}))
6769

70+
/** The append pre-check reads the workspace's current plan row limit, not the frozen `table.maxRows`. */
71+
vi.mock('@/lib/table/billing', () => ({
72+
getMaxRowsPerTable: mockGetMaxRowsPerTable,
73+
wouldExceedRowLimit: (limit: number, current: number, added: number) =>
74+
limit >= 0 && current + added > limit,
75+
}))
76+
6877
import { POST } from '@/app/api/table/[tableId]/import/route'
6978

7079
function createCsvFile(contents: string, name = 'data.csv', type = 'text/csv'): File {
@@ -167,6 +176,7 @@ describe('POST /api/table/[tableId]/import', () => {
167176
mockImportReplaceRows.mockResolvedValue({ deletedCount: 0, insertedCount: 0 })
168177
mockMarkTableImporting.mockResolvedValue(true)
169178
mockReleaseImportClaim.mockResolvedValue(undefined)
179+
mockGetMaxRowsPerTable.mockResolvedValue(1_000_000)
170180
})
171181

172182
it('returns 401 when the user is not authenticated', async () => {
@@ -288,11 +298,9 @@ describe('POST /api/table/[tableId]/import', () => {
288298
expect(mockImportAppendRows).toHaveBeenCalledTimes(1)
289299
})
290300

291-
it('rejects append when it would exceed maxRows', async () => {
292-
mockCheckAccess.mockResolvedValueOnce({
293-
ok: true,
294-
table: buildTable({ rowCount: 99, maxRows: 100 }),
295-
})
301+
it('rejects append when it would exceed the current plan row limit', async () => {
302+
mockCheckAccess.mockResolvedValueOnce({ ok: true, table: buildTable({ rowCount: 99 }) })
303+
mockGetMaxRowsPerTable.mockResolvedValueOnce(100)
296304
const response = await callPost(
297305
createFormData(createCsvFile('name,age\nAlice,30\nBob,40'), { mode: 'append' })
298306
)

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

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,15 @@ import {
2525
createCsvParser,
2626
dispatchAfterBatchInsert,
2727
generateColumnId,
28+
getMaxRowsPerTable,
2829
inferColumnType,
2930
markTableJobRunning,
3031
releaseJobClaim,
3132
sanitizeName,
3233
type TableDefinition,
3334
type TableSchema,
3435
validateMapping,
36+
wouldExceedRowLimit,
3537
} from '@/lib/table'
3638
import { importAppendRows, importReplaceRows } from '@/lib/table/import-data'
3739
import {
@@ -264,11 +266,12 @@ export const POST = withRouteHandler(async (request: NextRequest, { params }: Ro
264266
claimedImportId = syncImportId
265267

266268
if (mode === 'append') {
267-
if (prospectiveTable.rowCount + coerced.length > prospectiveTable.maxRows) {
268-
const deficit = prospectiveTable.rowCount + coerced.length - prospectiveTable.maxRows
269+
const maxRows = await getMaxRowsPerTable(workspaceId)
270+
if (wouldExceedRowLimit(maxRows, prospectiveTable.rowCount, coerced.length)) {
271+
const deficit = prospectiveTable.rowCount + coerced.length - maxRows
269272
return NextResponse.json(
270273
{
271-
error: `Append would exceed table row limit (${prospectiveTable.maxRows}). Currently ${prospectiveTable.rowCount} rows, ${coerced.length} new rows, ${deficit} over.`,
274+
error: `Append would exceed table row limit (${maxRows}). Currently ${prospectiveTable.rowCount} rows, ${coerced.length} new rows, ${deficit} over.`,
272275
},
273276
{ status: 400 }
274277
)

apps/sim/app/api/table/import-async/route.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
3838

3939
const parsed = await parseRequest(importTableAsyncContract, request, {})
4040
if (!parsed.success) return parsed.response
41-
const { workspaceId, fileKey, fileName } = parsed.data.body
41+
const { workspaceId, fileKey, fileName, deleteSourceFile } = parsed.data.body
4242

4343
const permission = await getUserEntityPermissions(userId, 'workspace', workspaceId)
4444
if (permission !== 'write' && permission !== 'admin') {
@@ -84,7 +84,6 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
8484
schema: { columns: [{ name: 'column_1', type: 'string' }] },
8585
workspaceId,
8686
userId,
87-
maxRows: planLimits.maxRowsPerTable,
8887
maxTables: planLimits.maxTables,
8988
jobStatus: 'running',
9089
jobType: 'import',
@@ -111,6 +110,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
111110
fileName,
112111
delimiter,
113112
mode: 'create',
113+
deleteSourceFile,
114114
}
115115
if (isTriggerDevEnabled) {
116116
// Trigger.dev runs the import outside the web container, so it survives app deploys.

apps/sim/app/api/table/import-csv/route.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,12 @@ vi.mock('@/app/api/table/utils', async () => {
3939
{ error: error.message },
4040
{ status: error.code === 'FILE_TOO_LARGE' ? 413 : 400 }
4141
),
42+
rowWriteErrorResponse: (error: unknown) => {
43+
const message = error instanceof Error ? error.message : String(error)
44+
return message.includes('row limit')
45+
? NextResponse.json({ error: message }, { status: 400 })
46+
: null
47+
},
4248
}
4349
})
4450
vi.mock('@/lib/workspaces/permissions/utils', () => permissionsMock)
@@ -175,6 +181,17 @@ describe('POST /api/table/import-csv', () => {
175181
expect(mockCreateTable).not.toHaveBeenCalled()
176182
})
177183

184+
it('returns 400 with the reason when an insert exceeds the plan row limit', async () => {
185+
mockBatchInsertRows.mockRejectedValueOnce(
186+
new Error('This table has reached its row limit (1,000 rows) on your current plan.')
187+
)
188+
const response = await POST(makeRequest(uploadParts(csvWithRows(250))))
189+
const data = await response.json()
190+
191+
expect(response.status).toBe(400)
192+
expect(data.error).toMatch(/row limit/)
193+
})
194+
178195
it('rolls back the created table when a batch insert fails mid-stream', async () => {
179196
mockBatchInsertRows
180197
.mockResolvedValueOnce(Array.from({ length: 100 }, () => ({ id: 'row' })))

apps/sim/app/api/table/import-csv/route.ts

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import {
3030
csvProxyBodyCapResponse,
3131
multipartErrorResponse,
3232
normalizeColumn,
33+
rowWriteErrorResponse,
3334
} from '@/app/api/table/utils'
3435

3536
const logger = createLogger('TableImportCSV')
@@ -105,12 +106,18 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
105106
headerToColumn: Map<string, string>
106107
}
107108

108-
const insertRows = async (rows: Record<string, unknown>[], state: ImportState) => {
109+
const insertRows = async (
110+
rows: Record<string, unknown>[],
111+
state: ImportState,
112+
currentRowCount: number
113+
) => {
109114
if (rows.length === 0) return 0
110115
const coerced = coerceRowsForTable(rows, state.schema, state.headerToColumn)
111116
const result = await batchInsertRows(
112117
{ tableId: state.table.id, rows: coerced, workspaceId, userId },
113-
state.table,
118+
// The created table's rowCount is frozen at 0; pass the running total so the
119+
// per-batch capacity check sees cumulative rows, not an always-empty table.
120+
{ ...state.table, rowCount: currentRowCount },
114121
generateId().slice(0, 8)
115122
)
116123
return result.length
@@ -132,7 +139,6 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
132139
schema,
133140
workspaceId,
134141
userId,
135-
maxRows: planLimits.maxRowsPerTable,
136142
maxTables: planLimits.maxTables,
137143
},
138144
requestId
@@ -153,13 +159,13 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
153159
sample.push(record)
154160
if (sample.length >= CSV_SCHEMA_SAMPLE_SIZE) {
155161
state = await buildTable(sample)
156-
inserted += await insertRows(sample, state)
162+
inserted += await insertRows(sample, state, inserted)
157163
}
158164
continue
159165
}
160166
batch.push(record)
161167
if (batch.length >= CSV_MAX_BATCH_SIZE) {
162-
inserted += await insertRows(batch, state)
168+
inserted += await insertRows(batch, state, inserted)
163169
batch = []
164170
}
165171
}
@@ -169,9 +175,9 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
169175
return NextResponse.json({ error: 'CSV file has no data rows' }, { status: 400 })
170176
}
171177
state = await buildTable(sample)
172-
inserted += await insertRows(sample, state)
178+
inserted += await insertRows(sample, state, inserted)
173179
} else {
174-
inserted += await insertRows(batch, state)
180+
inserted += await insertRows(batch, state, inserted)
175181
}
176182
} catch (streamError) {
177183
if (state) await deleteTable(state.table.id, requestId).catch(() => {})
@@ -200,9 +206,13 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
200206
} catch (error) {
201207
if (isMultipartError(error)) return multipartErrorResponse(error)
202208

203-
const message = toError(error).message
204209
logger.error(`[${requestId}] CSV import failed:`, error)
205210

211+
// Row-write failures (e.g. the plan row-limit check) map to a 400 with the real reason.
212+
const rowWriteError = rowWriteErrorResponse(error)
213+
if (rowWriteError) return rowWriteError
214+
215+
const message = toError(error).message
206216
const isClientError =
207217
message.includes('maximum table limit') ||
208218
message.includes('CSV file has no') ||

0 commit comments

Comments
 (0)