Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM oven/bun:1.3.11-alpine
FROM oven/bun:1.3.13-alpine

# Install necessary packages for development
RUN apk add --no-cache \
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/docs-embeddings.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.11
bun-version: 1.3.13

- name: Setup Node
uses: actions/setup-node@v4
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/i18n.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.11
bun-version: 1.3.13

- name: Cache Bun dependencies
uses: actions/cache@v4
Expand Down Expand Up @@ -122,7 +122,7 @@ jobs:
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.11
bun-version: 1.3.13

- name: Cache Bun dependencies
uses: actions/cache@v4
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/migrations.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.11
bun-version: 1.3.13

- name: Cache Bun dependencies
uses: actions/cache@v4
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/publish-cli.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.11
bun-version: 1.3.13

- name: Setup Node.js for npm publishing
uses: actions/setup-node@v4
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/publish-ts-sdk.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.11
bun-version: 1.3.13

- name: Setup Node.js for npm publishing
uses: actions/setup-node@v4
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.11
bun-version: 1.3.13

- name: Setup Node
uses: actions/setup-node@v4
Expand Down
619 changes: 431 additions & 188 deletions apps/docs/content/docs/en/tools/ashby.mdx

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions apps/docs/content/docs/en/triggers/ashby.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,14 @@ Trigger workflow when a candidate is hired
| ↳ `job` | object | job output from the tool |
| ↳ `id` | string | Job UUID |
| ↳ `title` | string | Job title |
| `offer` | object | offer output from the tool |
| ↳ `id` | string | Accepted offer UUID |
| ↳ `applicationId` | string | Associated application UUID |
| ↳ `acceptanceStatus` | string | Offer acceptance status |
| ↳ `offerStatus` | string | Offer process status |
| ↳ `decidedAt` | string | Offer decision timestamp \(ISO 8601\) |
| ↳ `latestVersion` | object | latestVersion output from the tool |
| ↳ `id` | string | Latest offer version UUID |


---
Expand Down
6 changes: 3 additions & 3 deletions apps/sim/app/(landing)/integrations/data/integrations.json
Original file line number Diff line number Diff line change
Expand Up @@ -1031,7 +1031,7 @@
},
{
"name": "List Applications",
"description": "Lists all applications in an Ashby organization with pagination and optional filters for status, job, candidate, and creation date."
"description": "Lists all applications in an Ashby organization with pagination and optional filters for status, job, and creation date."
},
{
"name": "Get Application",
Expand All @@ -1051,11 +1051,11 @@
},
{
"name": "Add Candidate Tag",
"description": "Adds a tag to a candidate in Ashby."
"description": "Adds a tag to a candidate in Ashby and returns the updated candidate."
},
{
"name": "Remove Candidate Tag",
"description": "Removes a tag from a candidate in Ashby."
"description": "Removes a tag from a candidate in Ashby and returns the updated candidate."
},
{
"name": "Get Offer",
Expand Down
2 changes: 2 additions & 0 deletions apps/sim/app/api/copilot/chat/stream/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ vi.mock('@/lib/copilot/request/session', () => ({
}),
encodeSSEEnvelope: (event: Record<string, unknown>) =>
new TextEncoder().encode(`data: ${JSON.stringify(event)}\n\n`),
encodeSSEComment: (comment: string) => new TextEncoder().encode(`: ${comment}\n\n`),
SSE_RESPONSE_HEADERS: {
'Content-Type': 'text/event-stream',
},
Expand Down Expand Up @@ -132,6 +133,7 @@ describe('copilot chat stream replay route', () => {
)

const chunks = await readAllChunks(response)
expect(chunks[0]).toBe(': accepted\n\n')
expect(chunks.join('')).toContain(
JSON.stringify({
status: MothershipStreamV1CompletionStatus.cancelled,
Expand Down
45 changes: 37 additions & 8 deletions apps/sim/app/api/copilot/chat/stream/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { getCopilotTracer, markSpanForError } from '@/lib/copilot/request/otel'
import {
checkForReplayGap,
createEvent,
encodeSSEComment,
encodeSSEEnvelope,
readEvents,
readFilePreviewSessions,
Expand All @@ -31,6 +32,7 @@ export const maxDuration = 3600

const logger = createLogger('CopilotChatStreamAPI')
const POLL_INTERVAL_MS = 250
const REPLAY_KEEPALIVE_INTERVAL_MS = 15_000
const MAX_STREAM_MS = 60 * 60 * 1000

function extractCanonicalRequestId(value: unknown): string {
Expand Down Expand Up @@ -266,6 +268,7 @@ async function handleResumeRequestBody({
let controllerClosed = false
let sawTerminalEvent = false
let currentRequestId = extractRunRequestId(run)
let lastWriteTime = Date.now()
// Stamp the logical request id + chat id on the resume root as soon
// as we resolve them from the run row, so TraceQL joins work on
// resume legs the same way they do on the original POST.
Expand All @@ -291,6 +294,19 @@ async function handleResumeRequestBody({
if (controllerClosed) return false
try {
controller.enqueue(encodeSSEEnvelope(payload))
lastWriteTime = Date.now()
return true
} catch {
controllerClosed = true
return false
}
}

const enqueueComment = (comment: string) => {
if (controllerClosed) return false
try {
controller.enqueue(encodeSSEComment(comment))
lastWriteTime = Date.now()
return true
} catch {
controllerClosed = true
Expand All @@ -306,22 +322,22 @@ async function handleResumeRequestBody({
const flushEvents = async () => {
const events = await readEvents(streamId, cursor)
if (events.length > 0) {
totalEventsFlushed += events.length
logger.debug('[Resume] Flushing events', {
streamId,
afterCursor: cursor,
eventCount: events.length,
})
}
for (const envelope of events) {
if (!enqueueEvent(envelope)) {
break
}
totalEventsFlushed += 1
cursor = envelope.stream.cursor ?? String(envelope.seq)
currentRequestId = extractEnvelopeRequestId(envelope) || currentRequestId
if (envelope.type === MothershipStreamV1EventType.complete) {
sawTerminalEvent = true
}
if (!enqueueEvent(envelope)) {
break
}
}
}

Expand All @@ -341,21 +357,30 @@ async function handleResumeRequestBody({
reason: options?.reason,
requestId: currentRequestId,
})) {
if (!enqueueEvent(envelope)) {
break
}
cursor = envelope.stream.cursor ?? String(envelope.seq)
if (envelope.type === MothershipStreamV1EventType.complete) {
sawTerminalEvent = true
}
if (!enqueueEvent(envelope)) {
break
}
}
}

try {
enqueueComment('accepted')

const gap = await checkForReplayGap(streamId, afterCursor, currentRequestId)
if (gap) {
for (const envelope of gap.envelopes) {
enqueueEvent(envelope)
if (!enqueueEvent(envelope)) {
break
}
cursor = envelope.stream.cursor ?? String(envelope.seq)
currentRequestId = extractEnvelopeRequestId(envelope) || currentRequestId
if (envelope.type === MothershipStreamV1EventType.complete) {
sawTerminalEvent = true
}
}
return
}
Expand Down Expand Up @@ -408,6 +433,10 @@ async function handleResumeRequestBody({
break
}

if (Date.now() - lastWriteTime >= REPLAY_KEEPALIVE_INTERVAL_MS) {
enqueueComment('keepalive')
}

await sleep(POLL_INTERVAL_MS)
}
if (!controllerClosed && Date.now() - startTime >= MAX_STREAM_MS) {
Expand Down
26 changes: 18 additions & 8 deletions apps/sim/app/api/table/[tableId]/rows/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,12 @@ const QueryRowsSchema = z.object({
.min(0, 'Offset must be 0 or greater')
.optional()
.default(0),
includeTotal: z
.preprocess(
(val) => (val === null || val === undefined || val === '' ? undefined : val === 'true'),
z.boolean().optional()
)
.default(true),
})

const nonEmptyFilter = z
Expand Down Expand Up @@ -328,6 +334,7 @@ export const GET = withRouteHandler(
const sortParam = searchParams.get('sort')
const limit = searchParams.get('limit')
const offset = searchParams.get('offset')
const includeTotalParam = searchParams.get('includeTotal')

let filter: Record<string, unknown> | undefined
let sort: Sort | undefined
Expand All @@ -349,6 +356,7 @@ export const GET = withRouteHandler(
sort,
limit,
offset,
includeTotal: includeTotalParam,
})

const accessResult = await checkAccess(tableId, authResult.userId, 'read')
Expand Down Expand Up @@ -398,17 +406,19 @@ export const GET = withRouteHandler(
query = query.orderBy(userTableRows.position) as typeof query
}

const countQuery = db
.select({ count: sql<number>`count(*)` })
.from(userTableRows)
.where(and(...baseConditions))

const [{ count: totalCount }] = await countQuery
let totalCount: number | null = null
if (validated.includeTotal) {
const [{ count }] = await db
.select({ count: sql<number>`count(*)` })
.from(userTableRows)
.where(and(...baseConditions))
totalCount = Number(count)
}

const rows = await query.limit(validated.limit).offset(validated.offset)

logger.info(
`[${requestId}] Queried ${rows.length} rows from table ${tableId} (total: ${totalCount})`
`[${requestId}] Queried ${rows.length} rows from table ${tableId} (total: ${totalCount ?? 'n/a'})`
)

return NextResponse.json({
Expand All @@ -424,7 +434,7 @@ export const GET = withRouteHandler(
r.updatedAt instanceof Date ? r.updatedAt.toISOString() : String(r.updatedAt),
})),
rowCount: rows.length,
totalCount: Number(totalCount),
totalCount,
limit: validated.limit,
offset: validated.offset,
},
Expand Down
48 changes: 38 additions & 10 deletions apps/sim/app/api/v1/tables/[tableId]/rows/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,12 @@ const QueryRowsSchema = z.object({
.optional()
)
.default(0),
includeTotal: z
.preprocess(
(val) => (val === null || val === undefined || val === '' ? undefined : val === 'true'),
z.boolean().optional()
)
.default(true),
})

const nonEmptyFilter = z
Expand Down Expand Up @@ -219,6 +225,7 @@ export const GET = withRouteHandler(
sort,
limit: searchParams.get('limit'),
offset: searchParams.get('offset'),
includeTotal: searchParams.get('includeTotal'),
})

const scopeError = checkWorkspaceScope(rateLimit, validated.workspaceId)
Expand Down Expand Up @@ -268,16 +275,37 @@ export const GET = withRouteHandler(
query = query.orderBy(userTableRows.position) as typeof query
}

const countQuery = db
.select({ count: sql<number>`count(*)` })
.from(userTableRows)
.where(and(...baseConditions))
const rowsPromise = query.limit(validated.limit).offset(validated.offset)

let totalCount: number | null = null
if (validated.includeTotal) {
const countQuery = db
.select({ count: sql<number>`count(*)` })
.from(userTableRows)
.where(and(...baseConditions))
const [countResult, rows] = await Promise.all([countQuery, rowsPromise])
totalCount = Number(countResult[0].count)
return NextResponse.json({
success: true,
data: {
rows: rows.map((r) => ({
id: r.id,
data: r.data,
position: r.position,
createdAt:
r.createdAt instanceof Date ? r.createdAt.toISOString() : String(r.createdAt),
updatedAt:
r.updatedAt instanceof Date ? r.updatedAt.toISOString() : String(r.updatedAt),
})),
rowCount: rows.length,
totalCount,
limit: validated.limit,
offset: validated.offset,
},
})
}

const [countResult, rows] = await Promise.all([
countQuery,
query.limit(validated.limit).offset(validated.offset),
])
const totalCount = countResult[0].count
const rows = await rowsPromise

return NextResponse.json({
success: true,
Expand All @@ -292,7 +320,7 @@ export const GET = withRouteHandler(
r.updatedAt instanceof Date ? r.updatedAt.toISOString() : String(r.updatedAt),
})),
rowCount: rows.length,
totalCount: Number(totalCount),
totalCount,
limit: validated.limit,
offset: validated.offset,
},
Expand Down
Loading
Loading