Merge pull request #789 from simstudioai/staging

v0.3.11: fix force-dynamic routes, webhooks, kb search perms
This commit is contained in:
Vikhyath Mondreti
2025-07-25 12:44:19 -07:00
committed by GitHub
42 changed files with 291 additions and 92 deletions

View File

@@ -9,6 +9,8 @@ import { member } from '@/db/schema'
const logger = createLogger('UnifiedBillingAPI')
export const dynamic = 'force-dynamic'
/**
* Unified Billing Endpoint
*/

View File

@@ -14,6 +14,8 @@ import { chat } from '@/db/schema'
const logger = createLogger('ChatAPI')
export const dynamic = 'force-dynamic'
const chatSchema = z.object({
workflowId: z.string().min(1, 'Workflow ID is required'),
subdomain: z

View File

@@ -2,6 +2,9 @@ import { eq } from 'drizzle-orm'
import { NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console-logger'
export const dynamic = 'force-dynamic'
import { createErrorResponse, createSuccessResponse } from '@/app/api/workflows/utils'
import { db } from '@/db'
import { chat } from '@/db/schema'

View File

@@ -3,6 +3,9 @@ import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console-logger'
export const dynamic = 'force-dynamic'
import { decryptSecret, encryptSecret } from '@/lib/utils'
import { db } from '@/db'
import { environment } from '@/db/schema'

View File

@@ -2,6 +2,9 @@ import { and, eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console-logger'
export const dynamic = 'force-dynamic'
import { getUserEntityPermissions } from '@/lib/permissions/utils'
import { db } from '@/db'
import { workflow, workflowFolder } from '@/db/schema'

View File

@@ -8,6 +8,8 @@ import { workflowFolder } from '@/db/schema'
const logger = createLogger('FoldersAPI')
export const dynamic = 'force-dynamic'
// GET - Fetch folders for a workspace
export async function GET(request: NextRequest) {
try {

View File

@@ -4,6 +4,9 @@ import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console-logger'
import { db } from '@/db'
export const dynamic = 'force-dynamic'
import { apiKey as apiKeyTable } from '@/db/schema'
import { createErrorResponse } from '../../workflows/utils'

View File

@@ -4,6 +4,9 @@ import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console-logger'
export const dynamic = 'force-dynamic'
import { db } from '@/db'
import { document, embedding } from '@/db/schema'
import { checkChunkAccess } from '../../../../../utils'

View File

@@ -4,6 +4,9 @@ import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console-logger'
export const dynamic = 'force-dynamic'
import { estimateTokenCount } from '@/lib/tokenization/estimators'
import { getUserId } from '@/app/api/auth/oauth/utils'
import {

View File

@@ -3,6 +3,9 @@ import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console-logger'
export const dynamic = 'force-dynamic'
import { db } from '@/db'
import { document, embedding } from '@/db/schema'
import { checkDocumentAccess, checkDocumentWriteAccess, processDocumentAsync } from '../../../utils'

View File

@@ -51,6 +51,11 @@ vi.mock('@/providers/utils', () => ({
}),
}))
const mockCheckKnowledgeBaseAccess = vi.fn()
vi.mock('@/app/api/knowledge/utils', () => ({
checkKnowledgeBaseAccess: mockCheckKnowledgeBaseAccess,
}))
mockConsoleLogger()
describe('Knowledge Search API Route', () => {
@@ -132,7 +137,11 @@ describe('Knowledge Search API Route', () => {
it('should perform search successfully with single knowledge base', async () => {
mockGetUserId.mockResolvedValue('user-123')
mockDbChain.where.mockResolvedValueOnce(mockKnowledgeBases)
// Mock knowledge base access check to return success
mockCheckKnowledgeBaseAccess.mockResolvedValue({
hasAccess: true,
knowledgeBase: mockKnowledgeBases[0],
})
mockDbChain.limit.mockResolvedValueOnce(mockSearchResults)
@@ -149,6 +158,10 @@ describe('Knowledge Search API Route', () => {
const response = await POST(req)
const data = await response.json()
if (response.status !== 200) {
console.log('Test failed with response:', data)
}
expect(response.status).toBe(200)
expect(data.success).toBe(true)
expect(data.data.results).toHaveLength(2)
@@ -171,7 +184,10 @@ describe('Knowledge Search API Route', () => {
mockGetUserId.mockResolvedValue('user-123')
mockDbChain.where.mockResolvedValueOnce(multiKbs)
// Mock knowledge base access check to return success for both KBs
mockCheckKnowledgeBaseAccess
.mockResolvedValueOnce({ hasAccess: true, knowledgeBase: multiKbs[0] })
.mockResolvedValueOnce({ hasAccess: true, knowledgeBase: multiKbs[1] })
mockDbChain.limit.mockResolvedValueOnce(mockSearchResults)
@@ -201,9 +217,13 @@ describe('Knowledge Search API Route', () => {
mockGetUserId.mockResolvedValue('user-123')
mockDbChain.where.mockResolvedValueOnce(mockKnowledgeBases) // First call: get knowledge bases
// Mock knowledge base access check to return success
mockCheckKnowledgeBaseAccess.mockResolvedValue({
hasAccess: true,
knowledgeBase: mockKnowledgeBases[0],
})
mockDbChain.limit.mockResolvedValueOnce(mockSearchResults) // Second call: search results
mockDbChain.limit.mockResolvedValueOnce(mockSearchResults) // Search results
mockFetch.mockResolvedValue({
ok: true,
@@ -255,7 +275,11 @@ describe('Knowledge Search API Route', () => {
it('should return not found for non-existent knowledge base', async () => {
mockGetUserId.mockResolvedValue('user-123')
mockDbChain.where.mockResolvedValueOnce([]) // No knowledge bases found
// Mock knowledge base access check to return no access
mockCheckKnowledgeBaseAccess.mockResolvedValue({
hasAccess: false,
notFound: true,
})
const req = createMockRequest('POST', validSearchData)
const { POST } = await import('./route')
@@ -274,7 +298,10 @@ describe('Knowledge Search API Route', () => {
mockGetUserId.mockResolvedValue('user-123')
mockDbChain.where.mockResolvedValueOnce(mockKnowledgeBases) // Only kb-123 found
// Mock access check: first KB has access, second doesn't
mockCheckKnowledgeBaseAccess
.mockResolvedValueOnce({ hasAccess: true, knowledgeBase: mockKnowledgeBases[0] })
.mockResolvedValueOnce({ hasAccess: false, notFound: true })
const req = createMockRequest('POST', multiKbData)
const { POST } = await import('./route')
@@ -282,7 +309,7 @@ describe('Knowledge Search API Route', () => {
const data = await response.json()
expect(response.status).toBe(404)
expect(data.error).toBe('Knowledge bases not found: kb-missing')
expect(data.error).toBe('Knowledge bases not found or access denied: kb-missing')
})
it.concurrent('should validate search parameters', async () => {
@@ -310,9 +337,13 @@ describe('Knowledge Search API Route', () => {
mockGetUserId.mockResolvedValue('user-123')
mockDbChain.where.mockResolvedValueOnce(mockKnowledgeBases) // First call: get knowledge bases
// Mock knowledge base access check to return success
mockCheckKnowledgeBaseAccess.mockResolvedValue({
hasAccess: true,
knowledgeBase: mockKnowledgeBases[0],
})
mockDbChain.limit.mockResolvedValueOnce(mockSearchResults) // Second call: search results
mockDbChain.limit.mockResolvedValueOnce(mockSearchResults) // Search results
mockFetch.mockResolvedValue({
ok: true,
@@ -416,7 +447,13 @@ describe('Knowledge Search API Route', () => {
describe('Cost tracking', () => {
it.concurrent('should include cost information in successful search response', async () => {
mockGetUserId.mockResolvedValue('user-123')
mockDbChain.where.mockResolvedValueOnce(mockKnowledgeBases)
// Mock knowledge base access check to return success
mockCheckKnowledgeBaseAccess.mockResolvedValue({
hasAccess: true,
knowledgeBase: mockKnowledgeBases[0],
})
mockDbChain.limit.mockResolvedValueOnce(mockSearchResults)
mockFetch.mockResolvedValue({
@@ -458,7 +495,13 @@ describe('Knowledge Search API Route', () => {
const { calculateCost } = await import('@/providers/utils')
mockGetUserId.mockResolvedValue('user-123')
mockDbChain.where.mockResolvedValueOnce(mockKnowledgeBases)
// Mock knowledge base access check to return success
mockCheckKnowledgeBaseAccess.mockResolvedValue({
hasAccess: true,
knowledgeBase: mockKnowledgeBases[0],
})
mockDbChain.limit.mockResolvedValueOnce(mockSearchResults)
mockFetch.mockResolvedValue({
@@ -509,7 +552,13 @@ describe('Knowledge Search API Route', () => {
}
mockGetUserId.mockResolvedValue('user-123')
mockDbChain.where.mockResolvedValueOnce(mockKnowledgeBases)
// Mock knowledge base access check to return success
mockCheckKnowledgeBaseAccess.mockResolvedValue({
hasAccess: true,
knowledgeBase: mockKnowledgeBases[0],
})
mockDbChain.limit.mockResolvedValueOnce(mockSearchResults)
mockFetch.mockResolvedValue({

View File

@@ -1,4 +1,4 @@
import { and, eq, inArray, isNull, sql } from 'drizzle-orm'
import { and, eq, inArray, sql } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { retryWithExponentialBackoff } from '@/lib/documents/utils'
@@ -6,8 +6,9 @@ import { env } from '@/lib/env'
import { createLogger } from '@/lib/logs/console-logger'
import { estimateTokenCount } from '@/lib/tokenization/estimators'
import { getUserId } from '@/app/api/auth/oauth/utils'
import { checkKnowledgeBaseAccess } from '@/app/api/knowledge/utils'
import { db } from '@/db'
import { embedding, knowledgeBase } from '@/db/schema'
import { embedding } from '@/db/schema'
import { calculateCost } from '@/providers/utils'
const logger = createLogger('VectorSearchAPI')
@@ -261,39 +262,37 @@ export async function POST(request: NextRequest) {
? validatedData.knowledgeBaseIds
: [validatedData.knowledgeBaseIds]
const [kb, queryEmbedding] = await Promise.all([
db
.select()
.from(knowledgeBase)
.where(
and(
inArray(knowledgeBase.id, knowledgeBaseIds),
eq(knowledgeBase.userId, userId),
isNull(knowledgeBase.deletedAt)
)
),
generateSearchEmbedding(validatedData.query),
])
// Check access permissions for each knowledge base using proper workspace-based permissions
const accessibleKbIds: string[] = []
for (const kbId of knowledgeBaseIds) {
const accessCheck = await checkKnowledgeBaseAccess(kbId, userId)
if (accessCheck.hasAccess) {
accessibleKbIds.push(kbId)
}
}
if (kb.length === 0) {
if (accessibleKbIds.length === 0) {
return NextResponse.json(
{ error: 'Knowledge base not found or access denied' },
{ status: 404 }
)
}
const foundKbIds = kb.map((k) => k.id)
const missingKbIds = knowledgeBaseIds.filter((id) => !foundKbIds.includes(id))
// Generate query embedding in parallel with access checks
const queryEmbedding = await generateSearchEmbedding(validatedData.query)
if (missingKbIds.length > 0) {
// Check if any requested knowledge bases were not accessible
const inaccessibleKbIds = knowledgeBaseIds.filter((id) => !accessibleKbIds.includes(id))
if (inaccessibleKbIds.length > 0) {
return NextResponse.json(
{ error: `Knowledge bases not found: ${missingKbIds.join(', ')}` },
{ error: `Knowledge bases not found or access denied: ${inaccessibleKbIds.join(', ')}` },
{ status: 404 }
)
}
// Adaptive query strategy based on KB count and parameters
const strategy = getQueryStrategy(foundKbIds.length, validatedData.topK)
// Adaptive query strategy based on accessible KB count and parameters
const strategy = getQueryStrategy(accessibleKbIds.length, validatedData.topK)
const queryVector = JSON.stringify(queryEmbedding)
let results: any[]
@@ -301,7 +300,7 @@ export async function POST(request: NextRequest) {
if (strategy.useParallel) {
// Execute parallel queries for better performance with many KBs
const parallelResults = await executeParallelQueries(
foundKbIds,
accessibleKbIds,
queryVector,
validatedData.topK,
strategy.distanceThreshold,
@@ -311,7 +310,7 @@ export async function POST(request: NextRequest) {
} else {
// Execute single optimized query for fewer KBs
results = await executeSingleQuery(
foundKbIds,
accessibleKbIds,
queryVector,
validatedData.topK,
strategy.distanceThreshold,
@@ -350,8 +349,8 @@ export async function POST(request: NextRequest) {
similarity: 1 - result.distance,
})),
query: validatedData.query,
knowledgeBaseIds: foundKbIds,
knowledgeBaseId: foundKbIds[0],
knowledgeBaseIds: accessibleKbIds,
knowledgeBaseId: accessibleKbIds[0],
topK: validatedData.topK,
totalResults: results.length,
...(cost && tokenCount

View File

@@ -21,6 +21,8 @@ import { invitation, member, organization, user, workspace, workspaceInvitation
const logger = createLogger('OrganizationInvitationsAPI')
export const dynamic = 'force-dynamic'
interface WorkspaceInvitation {
workspaceId: string
permission: 'admin' | 'write' | 'read'

View File

@@ -7,6 +7,8 @@ import { member, user, userStats } from '@/db/schema'
const logger = createLogger('OrganizationMemberAPI')
export const dynamic = 'force-dynamic'
/**
* GET /api/organizations/[id]/members/[memberId]
* Get individual organization member details

View File

@@ -13,6 +13,8 @@ import { invitation, member, organization, user, userStats } from '@/db/schema'
const logger = createLogger('OrganizationMembersAPI')
export const dynamic = 'force-dynamic'
/**
* GET /api/organizations/[id]/members
* Get organization members with optional usage data

View File

@@ -7,6 +7,9 @@ import {
updateOrganizationSeats,
} from '@/lib/billing/validation/seat-management'
import { createLogger } from '@/lib/logs/console-logger'
export const dynamic = 'force-dynamic'
import { db } from '@/db'
import { member, organization } from '@/db/schema'

View File

@@ -7,6 +7,8 @@ import { member, permissions, user, workspace } from '@/db/schema'
const logger = createLogger('OrganizationWorkspacesAPI')
export const dynamic = 'force-dynamic'
/**
* GET /api/organizations/[id]/workspaces
* Get workspaces related to the organization with optional filtering

View File

@@ -9,6 +9,8 @@ import { invitation, member, permissions, workspaceInvitation } from '@/db/schem
const logger = createLogger('OrganizationInvitationAcceptance')
export const dynamic = 'force-dynamic'
// Accept an organization invitation and any associated workspace invitations
export async function GET(req: NextRequest) {
const invitationId = req.nextUrl.searchParams.get('id')

View File

@@ -2,6 +2,9 @@ import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console-logger'
export const dynamic = 'force-dynamic'
import { db } from '@/db'
import { workflow, workflowSchedule } from '@/db/schema'

View File

@@ -17,6 +17,8 @@ import { workflowSchedule } from '@/db/schema'
const logger = createLogger('ScheduledAPI')
export const dynamic = 'force-dynamic'
const ScheduleRequestSchema = z.object({
workflowId: z.string(),
blockId: z.string().optional(),

View File

@@ -9,6 +9,8 @@ import { customTools } from '@/db/schema'
const logger = createLogger('CustomToolsAPI')
export const dynamic = 'force-dynamic'
// Define validation schema for custom tools
const CustomToolSchema = z.object({
tools: z.array(

View File

@@ -7,6 +7,8 @@ import { isOrganizationOwnerOrAdmin } from '@/lib/permissions/utils'
const logger = createLogger('UnifiedUsageLimitsAPI')
export const dynamic = 'force-dynamic'
/**
* Unified Usage Limits Endpoint
* GET/PUT /api/usage-limits?context=user|member&userId=<id>&organizationId=<id>

View File

@@ -2,6 +2,9 @@ import { and, eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console-logger'
export const dynamic = 'force-dynamic'
import { db } from '@/db'
import { apiKey } from '@/db/schema'

View File

@@ -9,6 +9,8 @@ import { apiKey } from '@/db/schema'
const logger = createLogger('ApiKeysAPI')
export const dynamic = 'force-dynamic'
// GET /api/users/me/api-keys - Get all API keys for the current user
export async function GET(request: NextRequest) {
try {

View File

@@ -4,6 +4,9 @@ import { NextResponse } from 'next/server'
import { z } from 'zod'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console-logger'
export const dynamic = 'force-dynamic'
import { db } from '@/db'
import { settings } from '@/db/schema'

View File

@@ -3,6 +3,9 @@ import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console-logger'
export const dynamic = 'force-dynamic'
import { db } from '@/db'
import { member, organization, subscription } from '@/db/schema'

View File

@@ -2,6 +2,9 @@ import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console-logger'
export const dynamic = 'force-dynamic'
import { db } from '@/db'
import { apiKey as apiKeyTable, subscription } from '@/db/schema'
import { RateLimiter } from '@/services/queue'

View File

@@ -4,6 +4,9 @@ import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console-logger'
export const dynamic = 'force-dynamic'
import { getUserEntityPermissions } from '@/lib/permissions/utils'
import { db } from '@/db'
import { workflow, workflowBlocks, workflowEdges, workflowSubflows } from '@/db/schema'

View File

@@ -12,6 +12,8 @@ import { workflow } from '@/db/schema'
const logger = createLogger('WorkflowByIdAPI')
export const dynamic = 'force-dynamic'
const UpdateWorkflowSchema = z.object({
name: z.string().min(1, 'Name is required').optional(),
description: z.string().optional(),

View File

@@ -3,6 +3,9 @@ import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console-logger'
export const dynamic = 'force-dynamic'
import { getUserEntityPermissions } from '@/lib/permissions/utils'
import { saveWorkflowToNormalizedTables } from '@/lib/workflows/db-helpers'
import { db } from '@/db'

View File

@@ -3,6 +3,9 @@ import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console-logger'
export const dynamic = 'force-dynamic'
import { getUserEntityPermissions } from '@/lib/permissions/utils'
import { db } from '@/db'
import { workflow } from '@/db/schema'

View File

@@ -8,6 +8,8 @@ import { workflow, workflowBlocks } from '@/db/schema'
const logger = createLogger('WorkflowAPI')
export const dynamic = 'force-dynamic'
// Schema for workflow creation
const CreateWorkflowSchema = z.object({
name: z.string().min(1, 'Name is required'),

View File

@@ -3,6 +3,9 @@ import { and, eq, isNull } from 'drizzle-orm'
import { NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console-logger'
export const dynamic = 'force-dynamic'
import { getUserEntityPermissions } from '@/lib/permissions/utils'
import { db } from '@/db'
import { workflow, workspace } from '@/db/schema'

View File

@@ -3,6 +3,9 @@ import { and, eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { getUsersWithPermissions, hasWorkspaceAdminAccess } from '@/lib/permissions/utils'
export const dynamic = 'force-dynamic'
import { db } from '@/db'
import { permissions, type permissionTypeEnum } from '@/db/schema'

View File

@@ -5,6 +5,8 @@ import { hasWorkspaceAdminAccess } from '@/lib/permissions/utils'
import { db } from '@/db'
import { workspaceInvitation } from '@/db/schema'
export const dynamic = 'force-dynamic'
// DELETE /api/workspaces/invitations/[id] - Delete a workspace invitation
export async function DELETE(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const { id } = await params

View File

@@ -6,6 +6,8 @@ import { env } from '@/lib/env'
import { db } from '@/db'
import { permissions, user, workspace, workspaceInvitation } from '@/db/schema'
export const dynamic = 'force-dynamic'
// Accept an invitation via token
export async function GET(req: NextRequest) {
const token = req.nextUrl.searchParams.get('token')

View File

@@ -4,6 +4,8 @@ import { getSession } from '@/lib/auth'
import { db } from '@/db'
import { workspace, workspaceInvitation } from '@/db/schema'
export const dynamic = 'force-dynamic'
// Get invitation details by token
export async function GET(req: NextRequest) {
const token = req.nextUrl.searchParams.get('token')

View File

@@ -5,6 +5,8 @@ import { hasWorkspaceAdminAccess } from '@/lib/permissions/utils'
import { db } from '@/db'
import { permissions } from '@/db/schema'
export const dynamic = 'force-dynamic'
// DELETE /api/workspaces/members/[id] - Remove a member from a workspace
export async function DELETE(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const { id: userId } = await params

View File

@@ -7,6 +7,8 @@ import { permissions, type permissionTypeEnum, user } from '@/db/schema'
type PermissionType = (typeof permissionTypeEnum.enumValues)[number]
export const dynamic = 'force-dynamic'
// Add a member to a workspace
export async function POST(req: Request) {
const session = await getSession()

View File

@@ -3,6 +3,9 @@ import { and, desc, eq, isNull } from 'drizzle-orm'
import { NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console-logger'
export const dynamic = 'force-dynamic'
import { db } from '@/db'
import { permissions, workflow, workflowBlocks, workspace } from '@/db/schema'

View File

@@ -93,7 +93,9 @@ export class WorkflowBlockHandler implements BlockHandler {
})
const startTime = performance.now()
const result = await subExecutor.execute(executionId)
// Use the actual child workflow ID for authentication, not the execution ID
// This ensures knowledge base and other API calls can properly authenticate
const result = await subExecutor.execute(workflowId)
const duration = performance.now() - startTime
// Remove current execution from stack after completion

View File

@@ -1437,6 +1437,104 @@ export async function fetchAndProcessAirtablePayloads(
/**
* Process webhook verification and authorization
*/
/**
* Handle Microsoft Teams webhooks with immediate acknowledgment
*/
async function processMicrosoftTeamsWebhook(
foundWebhook: any,
foundWorkflow: any,
input: any,
executionId: string,
requestId: string
): Promise<NextResponse> {
logger.info(
`[${requestId}] Acknowledging Microsoft Teams webhook ${foundWebhook.id} and executing workflow ${foundWorkflow.id} asynchronously (Execution: ${executionId})`
)
// Execute workflow asynchronously without waiting for completion
executeWorkflowFromPayload(
foundWorkflow,
input,
executionId,
requestId,
foundWebhook.blockId
).catch((error) => {
// Log any errors that occur during async execution
logger.error(
`[${requestId}] Error during async workflow execution for webhook ${foundWebhook.id} (Execution: ${executionId})`,
error
)
})
// Return immediate acknowledgment for Microsoft Teams
return NextResponse.json(
{
type: 'message',
text: 'Sim Studio',
},
{ status: 200 }
)
}
/**
* Handle standard webhooks with synchronous execution
*/
async function processStandardWebhook(
foundWebhook: any,
foundWorkflow: any,
input: any,
executionId: string,
requestId: string
): Promise<NextResponse> {
logger.info(
`[${requestId}] Executing workflow ${foundWorkflow.id} for webhook ${foundWebhook.id} (Execution: ${executionId})`
)
await executeWorkflowFromPayload(
foundWorkflow,
input,
executionId,
requestId,
foundWebhook.blockId
)
// Since executeWorkflowFromPayload handles logging and errors internally,
// we just need to return a standard success response for synchronous webhooks.
// Note: The actual result isn't typically returned in the webhook response itself.
return NextResponse.json({ message: 'Webhook processed' }, { status: 200 })
}
/**
* Handle webhook processing errors with provider-specific responses
*/
function handleWebhookError(
error: any,
foundWebhook: any,
executionId: string,
requestId: string
): NextResponse {
logger.error(
`[${requestId}] Error in processWebhook for ${foundWebhook.id} (Execution: ${executionId})`,
error
)
// For Microsoft Teams outgoing webhooks, return the expected error format
if (foundWebhook.provider === 'microsoftteams') {
return NextResponse.json(
{
type: 'message',
text: 'Webhook processing failed',
},
{ status: 200 }
) // Still return 200 to prevent Teams from showing additional error messages
}
return new NextResponse(`Internal Server Error: ${error.message}`, {
status: 500,
})
}
export async function processWebhook(
foundWebhook: any,
foundWorkflow: any,
@@ -1449,11 +1547,7 @@ export async function processWebhook(
// --- Handle Airtable differently - it should always use fetchAndProcessAirtablePayloads ---
if (foundWebhook.provider === 'airtable') {
logger.info(`[${requestId}] Routing Airtable webhook through dedicated processor`)
// Use the dedicated Airtable payload fetcher and processor
await fetchAndProcessAirtablePayloads(foundWebhook, foundWorkflow, requestId)
// Return standard success response
return NextResponse.json({ message: 'Airtable webhook processed' }, { status: 200 })
}
@@ -1475,60 +1569,20 @@ export async function processWebhook(
return new NextResponse('No messages in WhatsApp payload', { status: 200 })
}
// --- Send immediate acknowledgment and execute workflow asynchronously ---
logger.info(
`[${requestId}] Acknowledging webhook ${foundWebhook.id} and executing workflow ${foundWorkflow.id} asynchronously (Execution: ${executionId})`
)
// Execute workflow asynchronously without waiting for completion
executeWorkflowFromPayload(
foundWorkflow,
input,
executionId,
requestId,
foundWebhook.blockId
).catch((error) => {
// Log any errors that occur during async execution
logger.error(
`[${requestId}] Error during async workflow execution for webhook ${foundWebhook.id} (Execution: ${executionId})`,
error
)
})
// Return immediate acknowledgment to the webhook provider
// For Microsoft Teams outgoing webhooks, return the expected response format
// --- Route to appropriate processor based on provider ---
if (foundWebhook.provider === 'microsoftteams') {
return NextResponse.json(
{
type: 'message',
text: 'Sim Studio',
},
{ status: 200 }
return await processMicrosoftTeamsWebhook(
foundWebhook,
foundWorkflow,
input,
executionId,
requestId
)
}
return NextResponse.json({ message: 'Webhook received' }, { status: 200 })
return await processStandardWebhook(foundWebhook, foundWorkflow, input, executionId, requestId)
} catch (error: any) {
// Catch errors *before* calling executeWorkflowFromPayload (e.g., auth errors)
logger.error(
`[${requestId}] Error in processWebhook *before* execution for ${foundWebhook.id} (Execution: ${executionId})`,
error
)
// For Microsoft Teams outgoing webhooks, return the expected error format
if (foundWebhook.provider === 'microsoftteams') {
return NextResponse.json(
{
type: 'message',
text: 'Request received but processing failed',
},
{ status: 200 }
) // Still return 200 to prevent Teams from showing additional error messages
}
return new NextResponse(`Internal Server Error: ${error.message}`, {
status: 500,
})
return handleWebhookError(error, foundWebhook, executionId, requestId)
}
}