mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
fix(webhooks): cast json provider_config for atomic jsonb merge (#5249)
updateWebhookProviderConfig built a DB-side merge with jsonb operators
(COALESCE(provider_config, '{}'::jsonb) || $1::jsonb), but the
provider_config column is json, not jsonb. Postgres cannot apply jsonb
merge operators to a json column, so every polling state write failed
with "could not convert type jsonb to json" — silently breaking
historyId/lastCheckedTimestamp/pageToken/lastSeenGuids persistence for
all polling webhooks (Gmail, RSS, Google Sheets/Drive, Outlook, IMAP)
since the atomic-merge change landed.
Cast the column to jsonb for the || / - merge and cast the result back
to json for storage, matching the existing pattern in subscription.ts.
This commit is contained in:
@@ -7,7 +7,7 @@ const { mockUpdate, mockSet, mockWhere, sqlCalls } = vi.hoisted(() => ({
|
||||
mockUpdate: vi.fn(),
|
||||
mockSet: vi.fn(),
|
||||
mockWhere: vi.fn(),
|
||||
sqlCalls: [] as Array<{ values: unknown[] }>,
|
||||
sqlCalls: [] as Array<{ strings: readonly string[]; values: unknown[] }>,
|
||||
}))
|
||||
|
||||
vi.mock('@sim/db', () => ({ db: { update: mockUpdate } }))
|
||||
@@ -23,8 +23,8 @@ vi.mock('@sim/db/schema', () => ({
|
||||
workflowDeploymentVersion: {},
|
||||
}))
|
||||
vi.mock('drizzle-orm', () => ({
|
||||
sql: (_strings: readonly string[], ...values: unknown[]) => {
|
||||
const node = { values }
|
||||
sql: (strings: readonly string[], ...values: unknown[]) => {
|
||||
const node = { strings, values }
|
||||
sqlCalls.push(node)
|
||||
return node
|
||||
},
|
||||
@@ -50,6 +50,10 @@ function allInterpolatedValues(): unknown[] {
|
||||
return sqlCalls.flatMap((c) => c.values)
|
||||
}
|
||||
|
||||
function allSqlText(): string {
|
||||
return sqlCalls.map((c) => c.strings.join('')).join(' ')
|
||||
}
|
||||
|
||||
describe('updateWebhookProviderConfig (atomic jsonb merge)', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
@@ -77,4 +81,14 @@ describe('updateWebhookProviderConfig (atomic jsonb merge)', () => {
|
||||
expect(allInterpolatedValues()).toContain(JSON.stringify({ historyId: 'h1' }))
|
||||
expect(allInterpolatedValues().some((v) => Array.isArray(v))).toBe(false)
|
||||
})
|
||||
|
||||
it('casts the json column to jsonb for the merge and back to json for storage', async () => {
|
||||
await updateWebhookProviderConfig('wh-1', { historyId: 'h1', cleared: undefined }, logger)
|
||||
|
||||
const sqlText = allSqlText()
|
||||
// Column (interpolated as a value) is cast to jsonb: `COALESCE(<col>::jsonb, ...)`
|
||||
expect(sqlText).toContain('COALESCE(::jsonb')
|
||||
// Merge runs in jsonb space, result cast back to the json column: `(<expr>)::json`
|
||||
expect(sqlText).toContain(')::json')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -148,8 +148,13 @@ export async function runWithConcurrency(
|
||||
}
|
||||
|
||||
/**
|
||||
* Read-merge-write pattern for updating provider-specific config fields.
|
||||
* Atomically merge provider-specific config fields into `webhook.provider_config`.
|
||||
* Each provider passes its specific state updates (historyId, lastSeenGuids, etc.).
|
||||
*
|
||||
* The column is `json` (not `jsonb`), which has no merge operators, so the existing
|
||||
* value is cast to `jsonb` for the `||`/`-` merge and the result cast back to `json`
|
||||
* for storage. Casting is required — a bare `jsonb` expression cannot be assigned to
|
||||
* the `json` column.
|
||||
*/
|
||||
export async function updateWebhookProviderConfig(
|
||||
webhookId: string,
|
||||
@@ -164,12 +169,13 @@ export async function updateWebhookProviderConfig(
|
||||
else defined[key] = value
|
||||
}
|
||||
|
||||
const merged = sql`COALESCE(${webhook.providerConfig}, '{}'::jsonb) || ${JSON.stringify(defined)}::jsonb`
|
||||
const merged = sql`COALESCE(${webhook.providerConfig}::jsonb, '{}'::jsonb) || ${JSON.stringify(defined)}::jsonb`
|
||||
const nextConfig = removedKeys.length > 0 ? sql`(${merged}) - ${removedKeys}::text[]` : merged
|
||||
|
||||
await db
|
||||
.update(webhook)
|
||||
.set({
|
||||
providerConfig: removedKeys.length > 0 ? sql`(${merged}) - ${removedKeys}::text[]` : merged,
|
||||
providerConfig: sql`(${nextConfig})::json`,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(webhook.id, webhookId))
|
||||
|
||||
Reference in New Issue
Block a user