Skip to content

Commit bcc8803

Browse files
committed
regen migrations
1 parent 5e3645e commit bcc8803

10 files changed

Lines changed: 17163 additions & 42 deletions

File tree

apps/sim/app/api/webhooks/trigger/[path]/route.test.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ vi.mock('@/serializer', () => ({
7474
*/
7575
interface TestWebhook {
7676
id: string
77-
provider: string
77+
provider: string | null
7878
path: string
7979
isActive: boolean
8080
providerConfig?: Record<string, unknown>
@@ -542,6 +542,24 @@ describe('Webhook Trigger API Route', () => {
542542
expect(text).toMatch(/not found/i)
543543
})
544544

545+
it('returns 500 without dispatching when a persisted webhook has no provider', async () => {
546+
testData.webhooks.push({
547+
id: 'missing-provider-webhook',
548+
provider: null,
549+
path: 'missing-provider-path',
550+
isActive: true,
551+
workflowId: 'test-workflow-id',
552+
})
553+
554+
const response = await POST(createMockRequest('POST', { event: 'test' }), {
555+
params: Promise.resolve({ path: 'missing-provider-path' }),
556+
})
557+
558+
expect(response.status).toBe(500)
559+
await expect(response.json()).resolves.toEqual({ error: 'Webhook provider is missing' })
560+
expect(dispatchResolvedWebhookTargetMock).not.toHaveBeenCalled()
561+
})
562+
545563
it('returns a stable retryable response when the webhook admission gate is full', async () => {
546564
tryAdmitMock.mockReturnValueOnce(null)
547565

apps/sim/app/api/webhooks/trigger/[path]/route.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -137,12 +137,27 @@ async function handleWebhookPost(
137137
let billingBlocked = false
138138

139139
for (const { webhook: foundWebhook, workflow: foundWorkflow } of webhooksForPath) {
140+
const provider = foundWebhook.provider
141+
if (!provider) {
142+
const missingProviderResponse = NextResponse.json(
143+
{ error: 'Webhook provider is missing' },
144+
{ status: 500 }
145+
)
146+
if (webhooksForPath.length > 1) {
147+
logger.error(
148+
`[${requestId}] Webhook ${foundWebhook.id} has no provider, continuing to next`
149+
)
150+
continue
151+
}
152+
return missingProviderResponse
153+
}
154+
140155
// Generic ("custom") webhooks are an unauthenticated programmatic execution
141156
// surface, so they fall under the same paid-plan gate as the API. Provider
142157
// webhooks (slack, github, ...) are unaffected.
143158
if (
144-
foundWebhook.provider === 'generic' &&
145-
!(await isWorkspaceApiExecutionEntitled(foundWorkflow.workspaceId))
159+
provider === 'generic' &&
160+
!(await isWorkspaceApiExecutionEntitled(foundWorkflow.workspaceId ?? undefined))
146161
) {
147162
logger.warn(`[${requestId}] Generic webhook blocked: workspace on free plan`)
148163
billingBlocked = true
@@ -165,7 +180,7 @@ async function handleWebhookPost(
165180
return authError
166181
}
167182

168-
const reachabilityResponse = handleProviderReachabilityTest(foundWebhook, body, requestId)
183+
const reachabilityResponse = handleProviderReachabilityTest({ provider }, body, requestId)
169184
if (reachabilityResponse) {
170185
return reachabilityResponse
171186
}

apps/sim/lib/knowledge/documents/service.ts

Lines changed: 34 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -972,14 +972,18 @@ type DocumentStorageBilling =
972972
| {
973973
readonly context: StorageBillingContext
974974
readonly bytes: number
975-
readonly updatedUsage?: number
976975
}
977976
| {
978977
readonly userId: string
979978
readonly bytes: number
980979
readonly sub: HighestPrioritySubscription | null
981980
}
982981

982+
interface DocumentStorageNotification {
983+
readonly context: StorageBillingContext
984+
readonly updatedUsage: number
985+
}
986+
983987
/**
984988
* Resolves and checks one document-write storage payer. Workspace documents
985989
* use only the workspace-selected payer; workspace-less legacy documents retain
@@ -1043,12 +1047,12 @@ export async function createDocumentRecords(
10431047
requestId: string,
10441048
uploadedBy: string | null = null
10451049
): Promise<DocumentData[]> {
1046-
let storageBilling: DocumentStorageBilling | null = null
1047-
10481050
const totalBytes = documents.reduce((sum, docData) => sum + (docData.fileSize || 0), 0)
10491051
const sub = totalBytes > 0 ? await resolveQuotaSubscription(knowledgeBaseId, uploadedBy) : null
10501052

1051-
const returnData = await db.transaction(async (tx) => {
1053+
const { returnData, storageNotification } = await db.transaction(async (tx) => {
1054+
let storageNotification: DocumentStorageNotification | null = null
1055+
10521056
await tx.execute(sql`SELECT 1 FROM knowledge_base WHERE id = ${knowledgeBaseId} FOR UPDATE`)
10531057

10541058
const kb = await tx
@@ -1083,13 +1087,13 @@ export async function createDocumentRecords(
10831087
sub
10841088
)
10851089
if ('context' in preparedBilling) {
1086-
storageBilling = {
1087-
...preparedBilling,
1088-
updatedUsage: await incrementStorageUsageForBillingContextInTx(
1089-
tx,
1090-
preparedBilling.context,
1091-
preparedBilling.bytes
1092-
),
1090+
const updatedUsage = await incrementStorageUsageForBillingContextInTx(
1091+
tx,
1092+
preparedBilling.context,
1093+
preparedBilling.bytes
1094+
)
1095+
if (updatedUsage !== undefined) {
1096+
storageNotification = { context: preparedBilling.context, updatedUsage }
10931097
}
10941098
} else {
10951099
const quotaCheck = await checkAndIncrementStorageUsageInTx(
@@ -1101,7 +1105,6 @@ export async function createDocumentRecords(
11011105
if (!quotaCheck.allowed) {
11021106
throw new Error(quotaCheck.error || 'Storage limit exceeded')
11031107
}
1104-
storageBilling = preparedBilling
11051108
}
11061109
}
11071110

@@ -1191,13 +1194,13 @@ export async function createDocumentRecords(
11911194
.where(eq(knowledgeBase.id, knowledgeBaseId))
11921195
}
11931196

1194-
return returnData
1197+
return { returnData, storageNotification }
11951198
})
11961199

1197-
if (storageBilling && 'context' in storageBilling && storageBilling.updatedUsage !== undefined) {
1200+
if (storageNotification) {
11981201
void maybeNotifyStorageLimitForBillingContext(
1199-
storageBilling.context,
1200-
storageBilling.updatedUsage
1202+
storageNotification.context,
1203+
storageNotification.updatedUsage
12011204
)
12021205
}
12031206

@@ -1520,12 +1523,12 @@ export async function createSingleDocument(
15201523
...processedTags,
15211524
}
15221525

1523-
let storageBilling: DocumentStorageBilling | null = null
1524-
15251526
const sub =
15261527
documentData.fileSize > 0 ? await resolveQuotaSubscription(knowledgeBaseId, uploadedBy) : null
15271528

1528-
await db.transaction(async (tx) => {
1529+
const storageNotification = await db.transaction(async (tx) => {
1530+
let storageNotification: DocumentStorageNotification | null = null
1531+
15291532
await tx.execute(sql`SELECT 1 FROM knowledge_base WHERE id = ${knowledgeBaseId} FOR UPDATE`)
15301533

15311534
const kb = await tx
@@ -1559,13 +1562,13 @@ export async function createSingleDocument(
15591562
sub
15601563
)
15611564
if ('context' in preparedBilling) {
1562-
storageBilling = {
1563-
...preparedBilling,
1564-
updatedUsage: await incrementStorageUsageForBillingContextInTx(
1565-
tx,
1566-
preparedBilling.context,
1567-
preparedBilling.bytes
1568-
),
1565+
const updatedUsage = await incrementStorageUsageForBillingContextInTx(
1566+
tx,
1567+
preparedBilling.context,
1568+
preparedBilling.bytes
1569+
)
1570+
if (updatedUsage !== undefined) {
1571+
storageNotification = { context: preparedBilling.context, updatedUsage }
15691572
}
15701573
} else {
15711574
const quotaCheck = await checkAndIncrementStorageUsageInTx(
@@ -1577,7 +1580,6 @@ export async function createSingleDocument(
15771580
if (!quotaCheck.allowed) {
15781581
throw new Error(quotaCheck.error || 'Storage limit exceeded')
15791582
}
1580-
storageBilling = preparedBilling
15811583
}
15821584
}
15831585

@@ -1587,12 +1589,14 @@ export async function createSingleDocument(
15871589
.update(knowledgeBase)
15881590
.set({ updatedAt: now })
15891591
.where(eq(knowledgeBase.id, knowledgeBaseId))
1592+
1593+
return storageNotification
15901594
})
15911595

1592-
if (storageBilling && 'context' in storageBilling && storageBilling.updatedUsage !== undefined) {
1596+
if (storageNotification) {
15931597
void maybeNotifyStorageLimitForBillingContext(
1594-
storageBilling.context,
1595-
storageBilling.updatedUsage
1598+
storageNotification.context,
1599+
storageNotification.updatedUsage
15961600
)
15971601
}
15981602

apps/sim/lib/knowledge/documents/storage-billing.test.ts

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const {
1111
mockDecrementStorageUsageInTx,
1212
mockIncrementStorageUsage,
1313
mockIncrementStorageUsageForBillingContextInTx,
14+
mockMaybeNotifyStorageLimitForBillingContext,
1415
mockResolveStorageBillingContext,
1516
} = vi.hoisted(() => ({
1617
mockCheckStorageQuota: vi.fn(),
@@ -19,6 +20,7 @@ const {
1920
mockDecrementStorageUsageInTx: vi.fn(),
2021
mockIncrementStorageUsage: vi.fn(),
2122
mockIncrementStorageUsageForBillingContextInTx: vi.fn(),
23+
mockMaybeNotifyStorageLimitForBillingContext: vi.fn(),
2224
mockResolveStorageBillingContext: vi.fn(),
2325
}))
2426

@@ -31,10 +33,11 @@ vi.mock('@/lib/billing/storage', () => ({
3133
decrementStorageUsageInTx: mockDecrementStorageUsageInTx,
3234
incrementStorageUsage: mockIncrementStorageUsage,
3335
incrementStorageUsageForBillingContextInTx: mockIncrementStorageUsageForBillingContextInTx,
36+
maybeNotifyStorageLimitForBillingContext: mockMaybeNotifyStorageLimitForBillingContext,
3437
resolveStorageBillingContext: mockResolveStorageBillingContext,
3538
}))
3639

37-
import { createDocumentRecords } from '@/lib/knowledge/documents/service'
40+
import { createDocumentRecords, createSingleDocument } from '@/lib/knowledge/documents/service'
3841

3942
const STORAGE_CONTEXT = {
4043
workspaceId: 'workspace-1',
@@ -57,7 +60,8 @@ describe('knowledge document storage attribution', () => {
5760
])
5861
mockResolveStorageBillingContext.mockResolvedValue(STORAGE_CONTEXT)
5962
mockCheckStorageQuotaForBillingContext.mockResolvedValue({ allowed: true })
60-
mockIncrementStorageUsageForBillingContextInTx.mockResolvedValue(undefined)
63+
mockIncrementStorageUsageForBillingContextInTx.mockResolvedValue(5)
64+
mockMaybeNotifyStorageLimitForBillingContext.mockResolvedValue(undefined)
6165
})
6266

6367
it.each(['external-collaborator', 'personal-api-key-user'])(
@@ -84,11 +88,45 @@ describe('knowledge document storage attribution', () => {
8488
STORAGE_CONTEXT,
8589
5
8690
)
91+
expect(mockMaybeNotifyStorageLimitForBillingContext).toHaveBeenCalledWith(STORAGE_CONTEXT, 5)
8792
expect(mockCheckStorageQuota).not.toHaveBeenCalled()
8893
expect(mockIncrementStorageUsage).not.toHaveBeenCalled()
8994
expect(dbChainMockFns.values).toHaveBeenCalledWith([
9095
expect.objectContaining({ uploadedBy: actorUserId }),
9196
])
9297
}
9398
)
99+
100+
it('notifies the workspace payer after a single document transaction commits', async () => {
101+
let transactionCommitted = false
102+
dbChainMockFns.transaction.mockImplementationOnce(
103+
async (callback: (tx: typeof dbChainMock.db) => unknown) => {
104+
const result = await callback(dbChainMock.db)
105+
transactionCommitted = true
106+
return result
107+
}
108+
)
109+
mockMaybeNotifyStorageLimitForBillingContext.mockImplementationOnce(() => {
110+
expect(transactionCommitted).toBe(true)
111+
})
112+
113+
await createSingleDocument(
114+
{
115+
filename: 'note.txt',
116+
fileUrl: 'data:text/plain;base64,SGVsbG8=',
117+
fileSize: 5,
118+
mimeType: 'text/plain',
119+
},
120+
'knowledge-base-1',
121+
'request-1',
122+
'external-collaborator'
123+
)
124+
125+
expect(mockIncrementStorageUsageForBillingContextInTx).toHaveBeenCalledWith(
126+
expect.anything(),
127+
STORAGE_CONTEXT,
128+
5
129+
)
130+
expect(mockMaybeNotifyStorageLimitForBillingContext).toHaveBeenCalledWith(STORAGE_CONTEXT, 5)
131+
})
94132
})

apps/sim/lib/webhooks/processor.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,28 @@ describe('webhook processor execution identity', () => {
398398
workflowsPersistenceUtilsMockFns.mockBlockExistsInDeployment.mockResolvedValue(true)
399399
})
400400

401+
it('normalizes nullable persisted metadata in preprocessing correlation', async () => {
402+
const result = await checkWebhookPreprocessing(
403+
makeWorkflowRecord({ workspaceId: null }),
404+
makeWebhookRecord({ path: null, provider: null }),
405+
'request-1'
406+
)
407+
408+
expect(mockPreprocessExecution).toHaveBeenCalledWith(
409+
expect.objectContaining({
410+
workspaceId: undefined,
411+
triggerData: {
412+
correlation: expect.objectContaining({
413+
path: undefined,
414+
provider: undefined,
415+
}),
416+
},
417+
})
418+
)
419+
expect(result.correlation?.path).toBeUndefined()
420+
expect(result.correlation?.provider).toBeUndefined()
421+
})
422+
401423
it('reuses preprocessing execution identity when queueing a polling webhook', async () => {
402424
const expectedCorrelation = {
403425
executionId: 'generated-execution-id',

apps/sim/lib/webhooks/processor.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -560,7 +560,7 @@ export async function verifyProviderAuth(
560560
try {
561561
decryptedEnvVars = await getEffectiveDecryptedEnv(
562562
foundWorkflow.userId,
563-
foundWorkflow.workspaceId
563+
foundWorkflow.workspaceId ?? undefined
564564
)
565565
} catch (error) {
566566
logger.error(`[${requestId}] Failed to fetch environment variables`, {
@@ -602,8 +602,8 @@ export async function checkWebhookPreprocessing(
602602
source: 'webhook' as const,
603603
workflowId: foundWorkflow.id,
604604
webhookId: foundWebhook.id,
605-
path: foundWebhook.path,
606-
provider: foundWebhook.provider,
605+
path: foundWebhook.path ?? undefined,
606+
provider: foundWebhook.provider ?? undefined,
607607
triggerType: 'webhook',
608608
}
609609

@@ -616,7 +616,7 @@ export async function checkWebhookPreprocessing(
616616
triggerData: { correlation },
617617
checkRateLimit: true,
618618
checkDeployment: true,
619-
workspaceId: foundWorkflow.workspaceId,
619+
workspaceId: foundWorkflow.workspaceId ?? undefined,
620620
workflowRecord: foundWorkflow,
621621
})
622622

@@ -1045,7 +1045,7 @@ export async function processPolledWebhookEvent(
10451045
source: 'webhook' as const,
10461046
workflowId: foundWorkflow.id,
10471047
webhookId: foundWebhook.id,
1048-
path: foundWebhook.path ?? '',
1048+
path: foundWebhook.path ?? undefined,
10491049
provider,
10501050
triggerType: 'webhook',
10511051
} satisfies AsyncExecutionCorrelation)

0 commit comments

Comments
 (0)