Skip to content

Commit 87ac280

Browse files
Merge branch 'staging' into feat/table-trigger
2 parents 7cad96e + cc28ba8 commit 87ac280

99 files changed

Lines changed: 5740 additions & 7197 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.

.agents/skills/ship/SKILL.md

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,21 +13,20 @@ When the user runs `/ship`:
1313

1414
1. **Check git status** - See what files have changed
1515
2. **Generate a commit message** following this format: `type(scope): description`
16-
- Types: `fix`, `feat`, `improvement`, `chore`
17-
- Scope: short identifier (e.g., `undo-redo`, `api`, `ui`)
18-
- Keep it concise
19-
20-
3. **Run lint** - Run `bun run lint` from the repo root to fix formatting issues before staging
21-
16+
- Types: `fix`, `feat`, `improvement`, `chore`
17+
- Scope: short identifier (e.g., `undo-redo`, `api`, `ui`)
18+
- Keep it concise
19+
3. **Run pre-ship checks** from the repo root before staging:
20+
- `bun run lint` to fix formatting issues
21+
- `bun run check:api-validation:strict` to catch boundary contract failures before CI
2222
4. **Stage and commit** the changes with the generated message
23-
2423
5. **Push to origin** using the current branch name
25-
2624
6. **Create a PR** to staging with a description in the user's voice
2725

2826
## Commit Message Format
2927

3028
Based on the repo's commit history:
29+
3130
```
3231
fix(scope): description for bug fixes
3332
feat(scope): description for new features
@@ -61,6 +60,7 @@ Tested manually (or describe testing)
6160
## PR Creation Command
6261

6362
Use this command structure:
63+
6464
```bash
6565
gh pr create --base staging --title "COMMIT_MESSAGE" --body "PR_BODY"
6666
```
@@ -77,6 +77,7 @@ gh pr create --base staging --title "COMMIT_MESSAGE" --body "PR_BODY"
7777

7878
- Short, direct bullet points
7979
- No unnecessary explanation
80-
- "Tested manually" is acceptable for testing section
80+
- "Tested manually" is acceptable for testing section; include lint and boundary validation results when run
8181
- Checkboxes filled in appropriately
8282
- No screenshots section unless UI changes
83+

.claude/commands/ship.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ When the user runs `/ship`:
1717
- Scope: short identifier (e.g., `undo-redo`, `api`, `ui`)
1818
- Keep it concise
1919

20-
3. **Run lint** - Run `bun run lint` from the repo root to fix formatting issues before staging
20+
3. **Run pre-ship checks** from the repo root before staging:
21+
- `bun run lint` to fix formatting issues
22+
- `bun run check:api-validation:strict` to catch boundary contract failures before CI
2123

2224
4. **Stage and commit** the changes with the generated message
2325

@@ -77,6 +79,6 @@ gh pr create --base staging --title "COMMIT_MESSAGE" --body "PR_BODY"
7779

7880
- Short, direct bullet points
7981
- No unnecessary explanation
80-
- "Tested manually" is acceptable for testing section
82+
- "Tested manually" is acceptable for testing section; include lint and boundary validation results when run
8183
- Checkboxes filled in appropriately
8284
- No screenshots section unless UI changes

.cursor/commands/ship.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ When the user runs `/ship`:
1212
- Scope: short identifier (e.g., `undo-redo`, `api`, `ui`)
1313
- Keep it concise
1414

15-
3. **Run lint** - Run `bun run lint` from the repo root to fix formatting issues before staging
15+
3. **Run pre-ship checks** from the repo root before staging:
16+
- `bun run lint` to fix formatting issues
17+
- `bun run check:api-validation:strict` to catch boundary contract failures before CI
1618

1719
4. **Stage and commit** the changes with the generated message
1820

@@ -72,6 +74,6 @@ gh pr create --base staging --title "COMMIT_MESSAGE" --body "PR_BODY"
7274

7375
- Short, direct bullet points
7476
- No unnecessary explanation
75-
- "Tested manually" is acceptable for testing section
77+
- "Tested manually" is acceptable for testing section; include lint and boundary validation results when run
7678
- Checkboxes filled in appropriately
7779
- No screenshots section unless UI changes
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import { createLogger } from '@sim/logger'
2+
import { type NextRequest, NextResponse } from 'next/server'
3+
import { tableExportFormatSchema, tableIdParamsSchema } from '@/lib/api/contracts/tables'
4+
import { getValidationErrorMessage } from '@/lib/api/server'
5+
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
6+
import { generateRequestId } from '@/lib/core/utils/request'
7+
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
8+
import { queryRows } from '@/lib/table/service'
9+
import { accessError, checkAccess } from '@/app/api/table/utils'
10+
11+
const logger = createLogger('TableExport')
12+
13+
const EXPORT_BATCH_SIZE = 1000
14+
15+
type ExportFormat = 'csv' | 'json'
16+
17+
interface RouteParams {
18+
params: Promise<{ tableId: string }>
19+
}
20+
21+
/** GET /api/table/[tableId]/export - Streams the full table contents as CSV or JSON. */
22+
export const GET = withRouteHandler(async (request: NextRequest, { params }: RouteParams) => {
23+
const requestId = generateRequestId()
24+
const { tableId } = tableIdParamsSchema.parse(await params)
25+
26+
const auth = await checkSessionOrInternalAuth(request, { requireWorkflowId: false })
27+
if (!auth.success || !auth.userId) {
28+
return NextResponse.json({ error: 'Authentication required' }, { status: 401 })
29+
}
30+
31+
const { searchParams } = new URL(request.url)
32+
const formatValidation = tableExportFormatSchema.safeParse(
33+
searchParams.get('format') ?? undefined
34+
)
35+
if (!formatValidation.success) {
36+
return NextResponse.json(
37+
{ error: getValidationErrorMessage(formatValidation.error) },
38+
{ status: 400 }
39+
)
40+
}
41+
const format: ExportFormat = formatValidation.data
42+
43+
const access = await checkAccess(tableId, auth.userId, 'read')
44+
if (!access.ok) return accessError(access, requestId, tableId)
45+
const { table } = access
46+
47+
const columns = table.schema.columns
48+
const safeName = sanitizeFilename(table.name)
49+
const filename = `${safeName}.${format}`
50+
51+
const stream = new ReadableStream<Uint8Array>({
52+
async start(controller) {
53+
const encoder = new TextEncoder()
54+
try {
55+
if (format === 'csv') {
56+
controller.enqueue(encoder.encode(`${toCsvRow(columns.map((c) => c.name))}\n`))
57+
} else {
58+
controller.enqueue(encoder.encode('['))
59+
}
60+
61+
let offset = 0
62+
let firstJsonRow = true
63+
while (true) {
64+
const result = await queryRows(
65+
tableId,
66+
table.workspaceId,
67+
{ limit: EXPORT_BATCH_SIZE, offset, includeTotal: false },
68+
requestId
69+
)
70+
71+
for (const row of result.rows) {
72+
if (format === 'csv') {
73+
const values = columns.map((c) => formatCsvValue(row.data[c.name]))
74+
controller.enqueue(encoder.encode(`${toCsvRow(values)}\n`))
75+
} else {
76+
const prefix = firstJsonRow ? '' : ','
77+
firstJsonRow = false
78+
controller.enqueue(encoder.encode(prefix + JSON.stringify({ ...row.data })))
79+
}
80+
}
81+
82+
if (result.rows.length < EXPORT_BATCH_SIZE) break
83+
offset += result.rows.length
84+
}
85+
86+
if (format === 'json') controller.enqueue(encoder.encode(']'))
87+
controller.close()
88+
89+
logger.info(`[${requestId}] Exported table ${tableId}`, {
90+
format,
91+
rowCount: table.rowCount,
92+
})
93+
} catch (err) {
94+
logger.error(`[${requestId}] Export failed for table ${tableId}`, err)
95+
controller.error(err)
96+
}
97+
},
98+
})
99+
100+
return new NextResponse(stream, {
101+
status: 200,
102+
headers: {
103+
'Content-Type': format === 'csv' ? 'text/csv; charset=utf-8' : 'application/json',
104+
'Content-Disposition': `attachment; filename="${filename}"`,
105+
'Cache-Control': 'no-store',
106+
},
107+
})
108+
})
109+
110+
function sanitizeFilename(name: string): string {
111+
const cleaned = name.replace(/[^a-zA-Z0-9_-]+/g, '_').replace(/^_+|_+$/g, '')
112+
return cleaned || 'table'
113+
}
114+
115+
function formatCsvValue(value: unknown): string {
116+
if (value === null || value === undefined) return ''
117+
if (value instanceof Date) return value.toISOString()
118+
if (typeof value === 'object') return JSON.stringify(value)
119+
return String(value)
120+
}
121+
122+
function toCsvRow(values: string[]): string {
123+
return values.map(escapeCsvField).join(',')
124+
}
125+
126+
function escapeCsvField(field: string): string {
127+
if (/[",\n\r]/.test(field)) {
128+
return `"${field.replace(/"/g, '""')}"`
129+
}
130+
return field
131+
}

apps/sim/app/api/table/[tableId]/groups/[groupId]/run/route.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export const POST = withRouteHandler(async (request: NextRequest, { params }: Ro
3434
const parsed = await parseRequest(runWorkflowGroupContract, request, { params })
3535
if (!parsed.success) return parsed.response
3636
const { tableId, groupId } = parsed.data.params
37-
const { workspaceId, mode, rowIds } = parsed.data.body
37+
const { workspaceId, runMode, rowIds } = parsed.data.body
3838

3939
const result = await checkAccess(tableId, authResult.userId, 'write')
4040
if (!result.ok) return accessError(result, requestId, tableId)
@@ -48,7 +48,7 @@ export const POST = withRouteHandler(async (request: NextRequest, { params }: Ro
4848
tableId,
4949
groupId,
5050
workspaceId,
51-
mode,
51+
mode: runMode,
5252
requestId,
5353
rowIds,
5454
})

0 commit comments

Comments
 (0)