From 576554331575e3965d62bd1c7be8113cc897d22d Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Sat, 19 Sep 2026 18:45:58 -0700 Subject: [PATCH 1/2] improvement(utils): adopt toRecord and toRecordOrNull over inline record coercions --- apps/desktop/src/main/ipc.ts | 6 +- .../agent-group/browser-agent-icon.tsx | 4 +- .../home/hooks/stream/turn-model.ts | 4 +- apps/sim/background/webhook-execution.ts | 6 +- apps/sim/ee/scim/lib/protocol/user-patch.ts | 4 +- apps/sim/executor/execution/block-executor.ts | 4 +- apps/sim/hooks/queries/custom-tools.ts | 6 +- apps/sim/hooks/queries/organization.ts | 4 +- apps/sim/lib/auth/connectors/managed-oauth.ts | 14 ++--- apps/sim/lib/billing/core/reporting-period.ts | 4 +- apps/sim/lib/billing/webhooks/enterprise.ts | 6 +- .../sim/lib/copilot/chat/citation-evidence.ts | 4 +- .../tools/client/browser-tool-execution.ts | 6 +- .../tools/client/browser-tool-result.ts | 6 +- apps/sim/lib/internal/buffer/operations.ts | 4 +- .../operations/get-org-funding-window.ts | 13 ++-- .../cbinsights/operations/get-org-outlook.ts | 9 +-- .../operations/get-scouting-report.ts | 4 +- apps/sim/lib/internal/crowdstrike/client.ts | 4 +- .../lib/internal/crowdstrike/operations.ts | 4 +- apps/sim/lib/internal/discord/client.ts | 4 +- .../internal/microsoft-dataverse/client.ts | 4 +- .../lib/internal/microsoft-teams/client.ts | 4 +- .../internal/microsoft-teams/operations.ts | 4 +- apps/sim/lib/internal/onedrive/operations.ts | 4 +- .../lib/internal/onepassword/operations.ts | 13 ++-- apps/sim/lib/internal/pipedrive/client.ts | 7 +-- apps/sim/lib/internal/quiver/operations.ts | 4 +- apps/sim/lib/internal/resend/client.ts | 4 +- apps/sim/lib/internal/sailpoint/operations.ts | 4 +- apps/sim/lib/internal/search/operations.ts | 4 +- apps/sim/lib/internal/sendgrid/client.ts | 4 +- apps/sim/lib/internal/servicenow/client.ts | 4 +- .../internal/stagehand/schema-conversion.ts | 4 +- apps/sim/lib/internal/vanta/normalizers.ts | 4 +- apps/sim/lib/internal/vision/client.ts | 4 +- apps/sim/lib/internal/whatsapp/client.ts | 4 +- apps/sim/lib/webhooks/polling/types.ts | 4 +- apps/sim/lib/webhooks/providers/ashby.ts | 4 +- apps/sim/lib/webhooks/providers/emailbison.ts | 6 +- apps/sim/lib/webhooks/providers/github.ts | 4 +- apps/sim/lib/webhooks/providers/instantly.ts | 6 +- apps/sim/lib/webhooks/providers/jira.ts | 8 +-- apps/sim/lib/webhooks/providers/jotform.ts | 4 +- apps/sim/lib/webhooks/providers/linear.ts | 6 +- apps/sim/lib/webhooks/providers/quickbooks.ts | 12 ++-- apps/sim/lib/webhooks/providers/sendblue.ts | 4 +- apps/sim/lib/webhooks/providers/sentry.ts | 10 ++-- apps/sim/lib/webhooks/providers/slack.ts | 4 +- .../lib/webhooks/providers/twilio-voice.ts | 4 +- apps/sim/lib/webhooks/providers/twilio.ts | 6 +- .../executor/human-in-the-loop-manager.ts | 4 +- .../workflows/references/binding-targets.ts | 4 +- .../references/dependent-reconfigs.ts | 4 +- .../references/import-configuration.ts | 4 +- .../workflows/references/remap-references.ts | 10 ++-- .../workflows/sanitization/json-sanitizer.ts | 4 +- apps/sim/providers/conversation-generation.ts | 6 +- apps/sim/providers/history-adapters.ts | 4 +- .../stores/resource-list-preferences/store.ts | 4 +- apps/sim/tools/bitbucket/utils.ts | 19 +++--- apps/sim/tools/cbinsights/utils.ts | 7 --- apps/sim/tools/clickup/create_comment.ts | 4 +- apps/sim/tools/clickup/delete_time_entry.ts | 6 +- apps/sim/tools/clickup/get_running_timer.ts | 4 +- apps/sim/tools/clickup/shared.ts | 6 +- apps/sim/tools/clickup/start_timer.ts | 4 +- apps/sim/tools/clickup/stop_timer.ts | 4 +- apps/sim/tools/file/parser.ts | 4 +- apps/sim/tools/harmonic/utils.ts | 24 ++++---- apps/sim/tools/instantly/delete_leads.ts | 4 +- .../instantly/update_lead_interest_status.ts | 4 +- apps/sim/tools/instantly/utils.ts | 31 ++++------ apps/sim/tools/jotform/get_form_properties.ts | 4 +- apps/sim/tools/jotform/normalize.ts | 6 +- .../tools/jotform/update_form_properties.ts | 4 +- apps/sim/tools/jotform/utils.ts | 4 +- .../tools/smartlead/add_leads_to_campaign.ts | 4 +- apps/sim/tools/smartlead/create_lead_list.ts | 4 +- .../smartlead/get_campaign_webhook_summary.ts | 6 +- apps/sim/tools/smartlead/get_lead_list.ts | 4 +- .../tools/smartlead/list_lead_activities.ts | 4 +- apps/sim/tools/smartlead/list_lead_lists.ts | 4 +- .../sim/tools/smartlead/mark_lead_complete.ts | 6 +- .../smartlead/save_campaign_sequences.ts | 8 +-- apps/sim/tools/smartlead/update_lead_list.ts | 4 +- apps/sim/tools/trello/shared.ts | 4 +- apps/sim/tools/whatsapp/utils.ts | 4 +- apps/sim/tools/windchill/utils.ts | 4 +- apps/sim/triggers/github/utils.ts | 4 +- apps/sim/triggers/jira/utils.ts | 14 ++--- .../db/scripts/migrate-slack-custom-bots.ts | 4 +- .../commands/protocol/workflow-run-wait.ts | 4 +- .../sim-setup/src/configuration-sources.ts | 59 +++++++++---------- 94 files changed, 278 insertions(+), 324 deletions(-) diff --git a/apps/desktop/src/main/ipc.ts b/apps/desktop/src/main/ipc.ts index 78038098bbc..50c3d64efd2 100644 --- a/apps/desktop/src/main/ipc.ts +++ b/apps/desktop/src/main/ipc.ts @@ -27,7 +27,7 @@ import { type TerminalToolArgs, } from '@sim/terminal-protocol' import { getErrorMessage } from '@sim/utils/errors' -import { isRecordLike } from '@sim/utils/object' +import { isRecordLike, toRecord } from '@sim/utils/object' import { PASTE_LIMITS, utf8ByteLength } from '@sim/utils/paste' import type { BrowserWindow, IpcMainEvent, IpcMainInvokeEvent, WebContents } from 'electron' import { clipboard, ipcMain, shell } from 'electron' @@ -856,7 +856,7 @@ export function registerIpcHandlers(deps: IpcDeps): void { ) { return { ok: false, error: `Unknown browser tool: ${String(tool)}` } } - const toolParams = isRecordLike(params) ? params : {} + const toolParams = toRecord(params) return executeTool( scope, tool, @@ -1532,7 +1532,7 @@ export function registerIpcHandlers(deps: IpcDeps): void { ) { return { ok: false, error: `Unknown terminal tool: ${String(tool)}` } } - const call = isRecordLike(params) ? params : {} + const call = toRecord(params) if (!isTerminalOperation(call.operation)) { return { ok: false, error: `Unknown terminal operation: ${String(call.operation)}` } } diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/browser-agent-icon.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/browser-agent-icon.tsx index a3811b418cd..5bf1c53ac4d 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/browser-agent-icon.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/browser-agent-icon.tsx @@ -4,7 +4,7 @@ import { useState } from 'react' import { isBrowserToolName } from '@sim/browser-protocol' import { cn } from '@sim/emcn' import { Globe } from '@sim/emcn/icons' -import { isRecordLike } from '@sim/utils/object' +import { isRecordLike, toRecordOrNull } from '@sim/utils/object' import { isBrowserAgentAvailable } from '@/lib/browser-agent/transport' import { useChatSurface } from '@/app/workspace/[workspaceId]/home/components/chat-surface-context' import type { AgentGroupItem } from '@/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group-view' @@ -40,7 +40,7 @@ export function getBrowserAgentFaviconUrl(items: AgentGroupItem[]): string | nul return typeof params?.url === 'string' ? pageFaviconUrl(params.url) : null } - const output = result?.success && isRecordLike(result.output) ? result.output : null + const output = result?.success ? toRecordOrNull(result.output) : null if (output) { if (isRecordLike(output.activeTab) && typeof output.activeTab.url === 'string') { return pageFaviconUrl(output.activeTab.url) diff --git a/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/turn-model.ts b/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/turn-model.ts index 0b3bf5992d5..14fee904b18 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/turn-model.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/turn-model.ts @@ -1,4 +1,4 @@ -import { isRecordLike } from '@sim/utils/object' +import { isRecordLike, toRecord } from '@sim/utils/object' import { resolveStreamToolOutcome } from '@/lib/copilot/chat/stream-tool-outcome' import { MothershipStreamV1CompletionStatus, @@ -230,7 +230,7 @@ function rebindResolvedIntegrationCall(node: ToolNode, toolName: string): void { * through the `unknown`-typed {@link isRecordLike} guard rather than a double cast. */ function payloadRecord(payload: unknown): Record { - return isRecordLike(payload) ? payload : {} + return toRecord(payload) } /** Parses a wire `ts` to epoch ms, or undefined when absent/unparseable. */ diff --git a/apps/sim/background/webhook-execution.ts b/apps/sim/background/webhook-execution.ts index 78a2f5aa7b0..1779f07a842 100644 --- a/apps/sim/background/webhook-execution.ts +++ b/apps/sim/background/webhook-execution.ts @@ -10,7 +10,7 @@ import { createLogger, type RequestContext, runWithRequestContext } from '@sim/l import { toError } from '@sim/utils/errors' import { interruptibleSleep } from '@sim/utils/helpers' import { generateId } from '@sim/utils/id' -import { isRecordLike } from '@sim/utils/object' +import { isRecordLike, toRecord } from '@sim/utils/object' import { backoffWithJitter } from '@sim/utils/retry' import { task, timeout } from '@trigger.dev/sdk' import { eq } from 'drizzle-orm' @@ -1091,9 +1091,7 @@ async function executeWebhookJobInternal( }) } - const persistedProviderConfig = isRecordLike(resolvedWebhookRecord.providerConfig) - ? resolvedWebhookRecord.providerConfig - : {} + const persistedProviderConfig = toRecord(resolvedWebhookRecord.providerConfig) const slackStreamConfig = payload.provider === 'slack' || payload.provider === 'slack_app' ? readSlackStreamResponseConfig(persistedProviderConfig) diff --git a/apps/sim/ee/scim/lib/protocol/user-patch.ts b/apps/sim/ee/scim/lib/protocol/user-patch.ts index d71f9684cfc..8740f453946 100644 --- a/apps/sim/ee/scim/lib/protocol/user-patch.ts +++ b/apps/sim/ee/scim/lib/protocol/user-patch.ts @@ -1,5 +1,5 @@ import type { ScimUserAttributes, ScimUserEmail } from '@sim/db/schema' -import { isRecordLike } from '@sim/utils/object' +import { isRecordLike, toRecord } from '@sim/utils/object' import type { ScimPatchOperation } from '@/lib/api/contracts/scim' import { invalidPath, invalidValue, mutability, noTarget } from '@/ee/scim/lib/protocol/errors' import { @@ -331,7 +331,7 @@ function applyExtraOperation( else { if (!isRecordLike(value)) throw invalidValue(`${path} requires an object value`) const current = user.extra[extension.schema] - user.extra[extension.schema] = { ...(isRecordLike(current) ? current : {}), ...value } + user.extra[extension.schema] = { ...toRecord(current), ...value } } return } diff --git a/apps/sim/executor/execution/block-executor.ts b/apps/sim/executor/execution/block-executor.ts index d19349ec2d1..34ddc144e50 100644 --- a/apps/sim/executor/execution/block-executor.ts +++ b/apps/sim/executor/execution/block-executor.ts @@ -1,7 +1,7 @@ import { createLogger, type Logger } from '@sim/logger' import { describeError } from '@sim/utils/errors' import { sleep } from '@sim/utils/helpers' -import { isRecordLike } from '@sim/utils/object' +import { isRecordLike, toRecord } from '@sim/utils/object' import { DrizzleQueryError } from 'drizzle-orm/errors' import { isTimeoutAbortReason } from '@/lib/core/execution-limits/types' import { redactApiKeys } from '@/lib/core/security/redaction' @@ -970,7 +970,7 @@ export class BlockExecutor { } })() : mapping - inputs = isRecordLike(parsed) ? parsed : {} + inputs = toRecord(parsed) } const result: Record = {} diff --git a/apps/sim/hooks/queries/custom-tools.ts b/apps/sim/hooks/queries/custom-tools.ts index dfcc734731c..0edd2e4737d 100644 --- a/apps/sim/hooks/queries/custom-tools.ts +++ b/apps/sim/hooks/queries/custom-tools.ts @@ -1,5 +1,5 @@ import { createLogger } from '@sim/logger' -import { isRecordLike } from '@sim/utils/object' +import { isRecordLike, toRecord } from '@sim/utils/object' import { keepPreviousData, useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { requestJson } from '@/lib/api/client/request' import { @@ -125,8 +125,8 @@ async function fetchCustomTools( } const functionSchema = tool.schema.function - const parameters = isRecordLike(functionSchema.parameters) ? functionSchema.parameters : {} - const properties = isRecordLike(parameters.properties) ? parameters.properties : {} + const parameters = toRecord(functionSchema.parameters) + const properties = toRecord(parameters.properties) const required = Array.isArray(parameters.required) ? parameters.required.filter((value): value is string => typeof value === 'string') : undefined diff --git a/apps/sim/hooks/queries/organization.ts b/apps/sim/hooks/queries/organization.ts index b2e40daaa43..d81dd1366b5 100644 --- a/apps/sim/hooks/queries/organization.ts +++ b/apps/sim/hooks/queries/organization.ts @@ -1,5 +1,5 @@ import { createLogger } from '@sim/logger' -import { isRecordLike } from '@sim/utils/object' +import { isRecordLike, toRecord } from '@sim/utils/object' import { queryOptions, type UseQueryResult, @@ -269,7 +269,7 @@ export function useUpdateOrganizationUsageLimit() { organizationKeys.billing(organizationId), (old: unknown) => { if (!isRecordLike(old) || !isRecordLike(old.data)) return old - const usage = isRecordLike(old.data.usage) ? old.data.usage : {} + const usage = toRecord(old.data.usage) const currentUsage = readNumber(old.data.currentUsage) ?? readNumber(usage.current) ?? diff --git a/apps/sim/lib/auth/connectors/managed-oauth.ts b/apps/sim/lib/auth/connectors/managed-oauth.ts index bcd6eacdb14..756736e14b1 100644 --- a/apps/sim/lib/auth/connectors/managed-oauth.ts +++ b/apps/sim/lib/auth/connectors/managed-oauth.ts @@ -1,5 +1,5 @@ import { createHash } from 'node:crypto' -import { isRecordLike } from '@sim/utils/object' +import { isRecordLike, toRecord } from '@sim/utils/object' import type { OAuth2Tokens } from 'better-auth/oauth2' import type { GenericOAuthConfig } from 'better-auth/plugins' import { OAuth2Client, type TokenPayload } from 'google-auth-library' @@ -830,7 +830,7 @@ const USER_INFO_MANAGED_OAUTH_CONNECTORS = new Map ManagedOAuthCon }, parse: (profile) => { const account = asProfileRecord(profile, 'Dropbox') - const name = isRecordLike(account.name) ? account.name : {} + const name = toRecord(account.name) return withOptionalIdentityFields( { providerSubjectId: requireIdentityField(account.account_id, 'Dropbox account id'), @@ -889,15 +889,15 @@ const USER_INFO_MANAGED_OAUTH_CONNECTORS = new Map ManagedOAuthCon * `bot.owner.user`. A workspace-owned internal integration reports * `{ type: 'workspace' }` and identifies nobody, which cannot be bound to an invitation. */ - const bot = isRecordLike(self.bot) ? self.bot : {} - const owner = isRecordLike(bot.owner) ? bot.owner : {} + const bot = toRecord(self.bot) + const owner = toRecord(bot.owner) if (owner.type !== 'user') { throw new Error( 'Notion returned a workspace-owned integration, which identifies no person to bind this invitation to' ) } const user = asProfileRecord(owner.user, 'Notion') - const person = isRecordLike(user.person) ? user.person : {} + const person = toRecord(user.person) return withOptionalIdentityFields( { providerSubjectId: requireIdentityField(user.id, 'Notion user id'), @@ -1004,7 +1004,7 @@ const USER_INFO_MANAGED_OAUTH_CONNECTORS = new Map ManagedOAuthCon scopes: { from: 'profile', read: (profile) => { - const metadata = isRecordLike(profile) ? profile : {} + const metadata = toRecord(profile) if (Array.isArray(metadata.scopes)) { return metadata.scopes.filter((scope): scope is string => typeof scope === 'string') } @@ -1135,7 +1135,7 @@ const USER_INFO_MANAGED_OAUTH_CONNECTORS = new Map ManagedOAuthCon parse: (profile) => { const envelope = asProfileRecord(profile, 'Asana') const user = asProfileRecord(envelope.data, 'Asana') - const photo = isRecordLike(user.photo) ? user.photo : {} + const photo = toRecord(user.photo) return withOptionalIdentityFields( { providerSubjectId: requireIdentityField(user.gid, 'Asana user id'), diff --git a/apps/sim/lib/billing/core/reporting-period.ts b/apps/sim/lib/billing/core/reporting-period.ts index a9d37cb1866..1ec1f03a46f 100644 --- a/apps/sim/lib/billing/core/reporting-period.ts +++ b/apps/sim/lib/billing/core/reporting-period.ts @@ -1,4 +1,4 @@ -import { isRecordLike } from '@sim/utils/object' +import { toRecord } from '@sim/utils/object' import { defaultBillingPeriod } from '@/lib/billing/core/billing-period' import { isEnterprise } from '@/lib/billing/plan-helpers' @@ -91,7 +91,7 @@ export function resolveSubscriptionUsagePeriod( return subscription.usagePeriod } if (subscription && isEnterprise(subscription.plan)) { - const metadata = isRecordLike(subscription.metadata) ? subscription.metadata : {} + const metadata = toRecord(subscription.metadata) const anchor = metadata[ENTERPRISE_REPORTING_PERIOD_ANCHOR_METADATA_KEY] const interval = parseBillingInterval(metadata[ENTERPRISE_REPORTING_PERIOD_INTERVAL_METADATA_KEY]) ?? diff --git a/apps/sim/lib/billing/webhooks/enterprise.ts b/apps/sim/lib/billing/webhooks/enterprise.ts index df120c9bd8c..3f5d6baaa68 100644 --- a/apps/sim/lib/billing/webhooks/enterprise.ts +++ b/apps/sim/lib/billing/webhooks/enterprise.ts @@ -3,7 +3,7 @@ import { db } from '@sim/db' import { foldedEmail, organization, outboxEvent, session, subscription, user } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { generateId } from '@sim/utils/id' -import { isRecordLike } from '@sim/utils/object' +import { isRecordLike, toRecord } from '@sim/utils/object' import { normalizeEmail } from '@sim/utils/string' import { and, eq, inArray, sql } from 'drizzle-orm' import type Stripe from 'stripe' @@ -57,9 +57,7 @@ export async function handleManualEnterpriseSubscription(event: Stripe.Event) { async function processManualEnterpriseSubscription(event: Stripe.Event) { const eventSubscription = event.data.object as Stripe.Subscription const rawPreviousAttributes: unknown = event.data.previous_attributes - const previousAttributes: Record = isRecordLike(rawPreviousAttributes) - ? rawPreviousAttributes - : {} + const previousAttributes: Record = toRecord(rawPreviousAttributes) return withEnterpriseReconciliationLease(eventSubscription.id, (lease) => reconcileManualEnterpriseSubscription(eventSubscription, lease, { created: event.type === 'customer.subscription.created', diff --git a/apps/sim/lib/copilot/chat/citation-evidence.ts b/apps/sim/lib/copilot/chat/citation-evidence.ts index 4f85b09898d..9d4cc32c13c 100644 --- a/apps/sim/lib/copilot/chat/citation-evidence.ts +++ b/apps/sim/lib/copilot/chat/citation-evidence.ts @@ -1,4 +1,4 @@ -import { isRecordLike } from '@sim/utils/object' +import { toRecordOrNull } from '@sim/utils/object' export interface RetrievalCitationBlock { toolCall?: { name: string; status: string; result?: { success: boolean; output?: unknown } } @@ -12,7 +12,7 @@ export function parseCitationRecord(value: unknown): Record | n return null } } - return isRecordLike(value) ? value : null + return toRecordOrNull(value) } /** Only successful retrieval tool results may supply source destinations. */ diff --git a/apps/sim/lib/copilot/tools/client/browser-tool-execution.ts b/apps/sim/lib/copilot/tools/client/browser-tool-execution.ts index d09705eec7d..1b3bc43d247 100644 --- a/apps/sim/lib/copilot/tools/client/browser-tool-execution.ts +++ b/apps/sim/lib/copilot/tools/client/browser-tool-execution.ts @@ -10,7 +10,7 @@ import { type BrowserToolName, browserToolRendererTimeoutMs } from '@sim/browser-protocol' import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' -import { isRecordLike } from '@sim/utils/object' +import { isRecordLike, toRecord } from '@sim/utils/object' import { truncate } from '@sim/utils/string' import { cancelBrowserTool, @@ -143,7 +143,7 @@ function compactCompletionForPageExit( const serialized = JSON.stringify({ toolCallId, ...completion }) if (new Blob([serialized]).size <= PAGE_EXIT_COMPLETION_MAX_BYTES) return completion - const data = isRecordLike(completion.data) ? completion.data : {} + const data = toRecord(completion.data) return { status: completion.status, message: truncate(completion.message, 1024), @@ -164,7 +164,7 @@ function compactCompletionForRetry( const serialized = JSON.stringify({ toolCallId, ...completion }) if (new Blob([serialized]).size <= RETAINED_COMPLETION_MAX_BYTES) return completion - const data = isRecordLike(completion.data) ? completion.data : {} + const data = toRecord(completion.data) return { status: completion.status, message: truncate(completion.message, 1024), diff --git a/apps/sim/lib/copilot/tools/client/browser-tool-result.ts b/apps/sim/lib/copilot/tools/client/browser-tool-result.ts index 7ffafbcd616..2a63053313a 100644 --- a/apps/sim/lib/copilot/tools/client/browser-tool-result.ts +++ b/apps/sim/lib/copilot/tools/client/browser-tool-result.ts @@ -1,5 +1,5 @@ import type { BrowserToolName } from '@sim/browser-protocol' -import { isRecordLike } from '@sim/utils/object' +import { isRecordLike, toRecordOrNull } from '@sim/utils/object' function finiteNumber(value: unknown): value is number { return typeof value === 'number' && Number.isFinite(value) @@ -36,7 +36,7 @@ export function sanitizeBrowserToolResultForModel( note: 'The screenshot could not be encoded. Use browser_snapshot or browser_read_text instead.', } } - const viewport = isRecordLike(rest.viewport) ? rest.viewport : null + const viewport = toRecordOrNull(rest.viewport) const screenshotUrl = typeof rest.url === 'string' && rest.url ? rest.url @@ -44,7 +44,7 @@ export function sanitizeBrowserToolResultForModel( ? viewport.url : '' const location = screenshotUrl ? ` of ${screenshotUrl}` : '' - const clip = isRecordLike(rest.clip) ? rest.clip : null + const clip = toRecordOrNull(rest.clip) const cropSize = imageDimensions(clip) const viewportSize = imageDimensions(viewport) const imageSize = imageDimensions(rest.imageSize) diff --git a/apps/sim/lib/internal/buffer/operations.ts b/apps/sim/lib/internal/buffer/operations.ts index 251cb316906..c947cb9971c 100644 --- a/apps/sim/lib/internal/buffer/operations.ts +++ b/apps/sim/lib/internal/buffer/operations.ts @@ -1,6 +1,6 @@ import { createLogger } from '@sim/logger' import { getErrorMessage } from '@sim/utils/errors' -import { isRecordLike } from '@sim/utils/object' +import { isRecordLike, toRecord } from '@sim/utils/object' import type { EgressProfile } from '@/lib/core/security/egress/profiles' import { secureFetchWithPinnedIP, @@ -166,7 +166,7 @@ async function executePostMutation(args: { }) const data = await parseBufferGraphQLResponse(response) const candidate = data.createPost ?? data.editPost - result = isRecordLike(candidate) ? candidate : {} + result = toRecord(candidate) } catch (error) { context.signal?.throwIfAborted() const message = getErrorMessage(error, 'Buffer API request failed') diff --git a/apps/sim/lib/internal/cbinsights/operations/get-org-funding-window.ts b/apps/sim/lib/internal/cbinsights/operations/get-org-funding-window.ts index 138d7f863e6..2b23c569336 100644 --- a/apps/sim/lib/internal/cbinsights/operations/get-org-funding-window.ts +++ b/apps/sim/lib/internal/cbinsights/operations/get-org-funding-window.ts @@ -1,12 +1,7 @@ +import { toRecordOrNull } from '@sim/utils/object' import type { InternalToolOperationImplementation } from '@/lib/internal/tool-operations/types' import type { CbInsightsOrgParams } from '@/tools/cbinsights/types' -import { - asNumber, - asRecord, - asString, - cbInsightsRequest, - requireOrgId, -} from '@/tools/cbinsights/utils' +import { asNumber, asString, cbInsightsRequest, requireOrgId } from '@/tools/cbinsights/utils' export const executeCbinsightsGetOrgFundingWindowOperation: InternalToolOperationImplementation< CbInsightsOrgParams @@ -25,8 +20,8 @@ export const executeCbinsightsGetOrgFundingWindowOperation: InternalToolOperatio windowStart: asString(data.windowStart), windowEnd: asString(data.windowEnd), cohortNextRoundRate: asNumber(data.cohortNextRoundRate), - cohortCriteria: asRecord(data.cohortCriteria), - latestFunding: asRecord(data.latestFunding), + cohortCriteria: toRecordOrNull(data.cohortCriteria), + latestFunding: toRecordOrNull(data.latestFunding), }), signal ) diff --git a/apps/sim/lib/internal/cbinsights/operations/get-org-outlook.ts b/apps/sim/lib/internal/cbinsights/operations/get-org-outlook.ts index 2d3d8a54e49..e152282ff95 100644 --- a/apps/sim/lib/internal/cbinsights/operations/get-org-outlook.ts +++ b/apps/sim/lib/internal/cbinsights/operations/get-org-outlook.ts @@ -1,6 +1,7 @@ +import { toRecordOrNull } from '@sim/utils/object' import type { InternalToolOperationImplementation } from '@/lib/internal/tool-operations/types' import type { CbInsightsOrgParams } from '@/tools/cbinsights/types' -import { asRecord, cbInsightsRequest, requireOrgId } from '@/tools/cbinsights/utils' +import { cbInsightsRequest, requireOrgId } from '@/tools/cbinsights/utils' export const executeCbinsightsGetOrgOutlookOperation: InternalToolOperationImplementation< CbInsightsOrgParams @@ -14,9 +15,9 @@ export const executeCbinsightsGetOrgOutlookOperation: InternalToolOperationImple params, { path: `/v2/organizations/${orgId}/outlook` }, (data) => ({ - mosaicScore: asRecord(data.mosaicScore), - commercialMaturity: asRecord(data.commercialMaturity), - exitProbability: asRecord(data.exitProbability), + mosaicScore: toRecordOrNull(data.mosaicScore), + commercialMaturity: toRecordOrNull(data.commercialMaturity), + exitProbability: toRecordOrNull(data.exitProbability), }), signal ) diff --git a/apps/sim/lib/internal/cbinsights/operations/get-scouting-report.ts b/apps/sim/lib/internal/cbinsights/operations/get-scouting-report.ts index 6c2b40dcf6b..8559da31071 100644 --- a/apps/sim/lib/internal/cbinsights/operations/get-scouting-report.ts +++ b/apps/sim/lib/internal/cbinsights/operations/get-scouting-report.ts @@ -1,7 +1,7 @@ +import { toRecordOrNull } from '@sim/utils/object' import type { InternalToolOperationImplementation } from '@/lib/internal/tool-operations/types' import type { CbInsightsOrgParams } from '@/tools/cbinsights/types' import { - asRecord, asString, cbInsightsRequest, requireOrgId, @@ -23,7 +23,7 @@ export const executeCbinsightsGetScoutingReportOperation: InternalToolOperationI timeoutMs: SCOUTING_REPORT_TIMEOUT_MS, }, (data) => ({ - orgInfo: asRecord(data.orgInfo), + orgInfo: toRecordOrNull(data.orgInfo), reportMarkdown: asString(data.reportMarkdown), reportJson: asString(data.reportJson), }), diff --git a/apps/sim/lib/internal/crowdstrike/client.ts b/apps/sim/lib/internal/crowdstrike/client.ts index df44323b648..f5198ac245b 100644 --- a/apps/sim/lib/internal/crowdstrike/client.ts +++ b/apps/sim/lib/internal/crowdstrike/client.ts @@ -1,4 +1,4 @@ -import { isRecordLike } from '@sim/utils/object' +import { isRecordLike, toRecordOrNull } from '@sim/utils/object' import { MAX_JSON_API_RESPONSE_BYTES } from '@/lib/core/security/input-validation.server' import { readResponseTextWithLimit } from '@/lib/core/utils/stream-limits' import type { CrowdStrikeBaseParams, CrowdStrikeCloud } from '@/tools/crowdstrike/types' @@ -47,7 +47,7 @@ export function getRecordArray(value: unknown): JsonRecord[] { } export function getRecord(value: unknown): JsonRecord | null { - return isRecordLike(value) ? value : null + return toRecordOrNull(value) } /** diff --git a/apps/sim/lib/internal/crowdstrike/operations.ts b/apps/sim/lib/internal/crowdstrike/operations.ts index d29450b425f..a753bea5d67 100644 --- a/apps/sim/lib/internal/crowdstrike/operations.ts +++ b/apps/sim/lib/internal/crowdstrike/operations.ts @@ -1,5 +1,5 @@ import { createLogger } from '@sim/logger' -import { isRecordLike } from '@sim/utils/object' +import { isRecordLike, toRecord } from '@sim/utils/object' import { truncate } from '@sim/utils/string' import type { CrowdstrikeQueryBody } from '@/lib/api/contracts/tools/crowdstrike' import { @@ -327,7 +327,7 @@ function withCommittedIds( ): CrowdStrikeCallResult { if (committed.length === 0) return result - const envelope = isRecordLike(result.data) ? result.data : {} + const envelope = toRecord(result.data) const existing = getRecordArray(envelope.errors) const reason = getFalconErrorMessage(result.data, 'CrowdStrike rejected a later batch.') const message = diff --git a/apps/sim/lib/internal/discord/client.ts b/apps/sim/lib/internal/discord/client.ts index df86a4a1e15..612fac7ef21 100644 --- a/apps/sim/lib/internal/discord/client.ts +++ b/apps/sim/lib/internal/discord/client.ts @@ -1,4 +1,4 @@ -import { isRecordLike } from '@sim/utils/object' +import { isRecordLike, toRecord } from '@sim/utils/object' import { MAX_JSON_API_RESPONSE_BYTES } from '@/lib/core/security/input-validation.server' import { readResponseJsonWithLimit } from '@/lib/core/utils/stream-limits' import { DiscordOperationError } from '@/lib/internal/discord/errors' @@ -41,5 +41,5 @@ export async function sendDiscordMessage( response.status ) } - return isRecordLike(data) ? data : {} + return toRecord(data) } diff --git a/apps/sim/lib/internal/microsoft-dataverse/client.ts b/apps/sim/lib/internal/microsoft-dataverse/client.ts index 316c6ce2788..b97ea8fcbb4 100644 --- a/apps/sim/lib/internal/microsoft-dataverse/client.ts +++ b/apps/sim/lib/internal/microsoft-dataverse/client.ts @@ -1,4 +1,4 @@ -import { isRecordLike } from '@sim/utils/object' +import { isRecordLike, toRecordOrNull } from '@sim/utils/object' import { MAX_JSON_API_RESPONSE_BYTES, secureFetchWithValidation, @@ -41,7 +41,7 @@ export async function uploadDataverseFile( return } const data = await response.json().catch(() => null) - const error = isRecordLike(data) && isRecordLike(data.error) ? data.error : null + const error = isRecordLike(data) && toRecordOrNull(data.error) const message = error && typeof error.message === 'string' ? error.message diff --git a/apps/sim/lib/internal/microsoft-teams/client.ts b/apps/sim/lib/internal/microsoft-teams/client.ts index 1ac3399dcb4..82ec249010b 100644 --- a/apps/sim/lib/internal/microsoft-teams/client.ts +++ b/apps/sim/lib/internal/microsoft-teams/client.ts @@ -1,5 +1,5 @@ import { getErrorMessage } from '@sim/utils/errors' -import { isRecordLike } from '@sim/utils/object' +import { toRecord } from '@sim/utils/object' import { readResponseTextWithLimit } from '@/lib/core/utils/stream-limits' import { MicrosoftTeamsOperationError } from '@/lib/internal/microsoft-teams/errors' @@ -9,7 +9,7 @@ const MICROSOFT_GRAPH_RESPONSE_MAX_BYTES = 2 * 1024 * 1024 export type MicrosoftTeamsGraphObject = Record function asObject(value: unknown): MicrosoftTeamsGraphObject { - return isRecordLike(value) ? value : {} + return toRecord(value) } function errorMessage(data: MicrosoftTeamsGraphObject, fallback: string): string { diff --git a/apps/sim/lib/internal/microsoft-teams/operations.ts b/apps/sim/lib/internal/microsoft-teams/operations.ts index 7cca5977186..f6829cd2098 100644 --- a/apps/sim/lib/internal/microsoft-teams/operations.ts +++ b/apps/sim/lib/internal/microsoft-teams/operations.ts @@ -1,6 +1,6 @@ import { createLogger } from '@sim/logger' import { getErrorMessage } from '@sim/utils/errors' -import { isRecordLike } from '@sim/utils/object' +import { isRecordLike, toRecord } from '@sim/utils/object' import { isPayloadSizeLimitError, readResponseJsonWithLimit } from '@/lib/core/utils/stream-limits' import { MicrosoftTeamsClient, @@ -71,7 +71,7 @@ function optionalString(data: MicrosoftTeamsGraphObject, key: string): string | } function nestedObject(data: MicrosoftTeamsGraphObject, key: string): MicrosoftTeamsGraphObject { - return isRecordLike(data[key]) ? data[key] : {} + return toRecord(data[key]) } function requiredId(value: string, label: string): string { diff --git a/apps/sim/lib/internal/onedrive/operations.ts b/apps/sim/lib/internal/onedrive/operations.ts index f1d5db4bf19..10075e3b9a9 100644 --- a/apps/sim/lib/internal/onedrive/operations.ts +++ b/apps/sim/lib/internal/onedrive/operations.ts @@ -1,6 +1,6 @@ import { createLogger } from '@sim/logger' import { getErrorMessage } from '@sim/utils/errors' -import { isRecordLike } from '@sim/utils/object' +import { isRecordLike, toRecord } from '@sim/utils/object' import * as XLSX from 'xlsx' import { validateMicrosoftGraphId } from '@/lib/core/security/input-validation' import { @@ -261,7 +261,7 @@ async function writeExcelValues( } } const written = await readGraphJson(writeResponse, context.signal) - const data = isRecordLike(written) ? written : {} + const data = toRecord(written) const returnedValues = Array.isArray(data.values) ? data.values : [] const firstRow = Array.isArray(returnedValues[0]) ? returnedValues[0] : [] return { diff --git a/apps/sim/lib/internal/onepassword/operations.ts b/apps/sim/lib/internal/onepassword/operations.ts index d33b3ba1cb0..1bab6bdcf29 100644 --- a/apps/sim/lib/internal/onepassword/operations.ts +++ b/apps/sim/lib/internal/onepassword/operations.ts @@ -1,5 +1,6 @@ import type { ItemCreateParams } from '@1password/sdk' import { generateId } from '@sim/utils/id' +import { toRecord } from '@sim/utils/object' import type { ContractBody } from '@/lib/api/contracts' import type { onePasswordCreateItemContract, @@ -53,14 +54,8 @@ type DeleteItemInput = ContractBody type ResolveSecretInput = ContractBody type GetItemFileInput = ContractBody -function asRecord(value: unknown): Record { - return value !== null && typeof value === 'object' && !Array.isArray(value) - ? (value as Record) - : {} -} - function providerMessage(data: unknown, fallback: string): string { - const message = asRecord(data).message + const message = toRecord(data).message return typeof message === 'string' && message ? message : fallback } @@ -210,7 +205,7 @@ export async function executeOnePasswordCreateItem( : undefined const fields = input.fields ? (JSON.parse(input.fields) as Array>).map((field) => { - const section = asRecord(field.section) + const section = toRecord(field.section) return { id: (field.id as string) || generateId().slice(0, 8), title: (field.label as string) || (field.title as string) || '', @@ -412,7 +407,7 @@ export async function executeOnePasswordGetItemFile( error: providerMessage(data, 'Failed to get file metadata'), }) } - const metadata = asRecord(await metadataResponse.json()) + const metadata = toRecord(await metadataResponse.json()) context.signal?.throwIfAborted() const contentResponse = await connectRequest({ diff --git a/apps/sim/lib/internal/pipedrive/client.ts b/apps/sim/lib/internal/pipedrive/client.ts index 0fb90aaf4f4..349418100c3 100644 --- a/apps/sim/lib/internal/pipedrive/client.ts +++ b/apps/sim/lib/internal/pipedrive/client.ts @@ -1,4 +1,4 @@ -import { isRecordLike } from '@sim/utils/object' +import { isRecordLike, toRecordOrNull } from '@sim/utils/object' import { MAX_JSON_API_RESPONSE_BYTES, secureFetchWithPinnedIP, @@ -76,9 +76,8 @@ export async function listPipedriveFiles( const files = Array.isArray(data.data) ? data.data.filter((file): file is PipedriveFile => isRecordLike(file)) : [] - const additionalData = isRecordLike(data.additional_data) ? data.additional_data : null - const pagination = - additionalData && isRecordLike(additionalData.pagination) ? additionalData.pagination : null + const additionalData = toRecordOrNull(data.additional_data) + const pagination = additionalData ? toRecordOrNull(additionalData.pagination) : null return { files, hasMore: pagination?.more_items_in_collection === true, diff --git a/apps/sim/lib/internal/quiver/operations.ts b/apps/sim/lib/internal/quiver/operations.ts index 33828b10e3f..82f2ee79d60 100644 --- a/apps/sim/lib/internal/quiver/operations.ts +++ b/apps/sim/lib/internal/quiver/operations.ts @@ -1,5 +1,5 @@ import { createLogger } from '@sim/logger' -import { isRecordLike } from '@sim/utils/object' +import { isRecordLike, toRecord } from '@sim/utils/object' import { validateOpaqueModelInputProvenance } from '@/lib/execution/model-input-provenance' import { requestQuiverSvg } from '@/lib/internal/quiver/client' import { QuiverOperationError } from '@/lib/internal/quiver/errors' @@ -37,7 +37,7 @@ function fail(message: string, status: number, body?: Record): } function record(value: unknown): Record { - return isRecordLike(value) ? value : {} + return toRecord(value) } function optionalNumber(value: unknown): number { diff --git a/apps/sim/lib/internal/resend/client.ts b/apps/sim/lib/internal/resend/client.ts index 3298db84a10..2527689b487 100644 --- a/apps/sim/lib/internal/resend/client.ts +++ b/apps/sim/lib/internal/resend/client.ts @@ -1,4 +1,4 @@ -import { isRecordLike } from '@sim/utils/object' +import { toRecord } from '@sim/utils/object' import { MAX_JSON_API_RESPONSE_BYTES } from '@/lib/core/security/input-validation.server' import { readResponseJsonWithLimit } from '@/lib/core/utils/stream-limits' import { ResendOperationError } from '@/lib/internal/resend/errors' @@ -6,7 +6,7 @@ import { ResendOperationError } from '@/lib/internal/resend/errors' const MAX_RESEND_ERROR_BYTES = 64 * 1024 function record(value: unknown): Record { - return isRecordLike(value) ? value : {} + return toRecord(value) } function message(value: unknown): string { diff --git a/apps/sim/lib/internal/sailpoint/operations.ts b/apps/sim/lib/internal/sailpoint/operations.ts index 1d3b5fa24de..37b79d0dd53 100644 --- a/apps/sim/lib/internal/sailpoint/operations.ts +++ b/apps/sim/lib/internal/sailpoint/operations.ts @@ -1,6 +1,6 @@ import { createLogger } from '@sim/logger' import { getErrorMessage } from '@sim/utils/errors' -import { filterUndefined, isRecordLike } from '@sim/utils/object' +import { filterUndefined, isRecordLike, toRecordOrNull } from '@sim/utils/object' import { isPayloadSizeLimitError } from '@/lib/core/utils/stream-limits' import { getSailPointErrorMessage, @@ -320,7 +320,7 @@ async function executeLoad( ) if (!result.ok) return providerFailure(result) if (isAccountLoad) { - const body = isRecordLike(result.data) ? result.data : null + const body = toRecordOrNull(result.data) if (!body || !isRecordLike(body.task)) { return failureResponse('SailPoint returned an invalid account-load task response', 502) } diff --git a/apps/sim/lib/internal/search/operations.ts b/apps/sim/lib/internal/search/operations.ts index a3612526f5c..6549fa5dfed 100644 --- a/apps/sim/lib/internal/search/operations.ts +++ b/apps/sim/lib/internal/search/operations.ts @@ -1,4 +1,4 @@ -import { isRecordLike } from '@sim/utils/object' +import { isRecordLike, toRecord } from '@sim/utils/object' import { SEARCH_TOOL_COST } from '@/lib/billing/constants' import { env } from '@/lib/core/config/env' import type { SearchResponse } from '@/tools/search/types' @@ -37,7 +37,7 @@ export async function executeSearchOperation( const rawResults = isRecordLike(result.output) && Array.isArray(result.output.results) ? result.output.results : [] const results = rawResults.map((rawResult, index) => { - const resultRecord = isRecordLike(rawResult) ? rawResult : {} + const resultRecord = toRecord(rawResult) const highlights = Array.isArray(resultRecord.highlights) ? resultRecord.highlights.filter((value): value is string => typeof value === 'string') : [] diff --git a/apps/sim/lib/internal/sendgrid/client.ts b/apps/sim/lib/internal/sendgrid/client.ts index 75610b27c0b..d81bffe06a6 100644 --- a/apps/sim/lib/internal/sendgrid/client.ts +++ b/apps/sim/lib/internal/sendgrid/client.ts @@ -1,11 +1,11 @@ -import { isRecordLike } from '@sim/utils/object' +import { toRecord } from '@sim/utils/object' import { consumeOrCancelBody, readResponseJsonWithLimit } from '@/lib/core/utils/stream-limits' import { SendGridOperationError } from '@/lib/internal/sendgrid/errors' const MAX_SENDGRID_ERROR_BYTES = 64 * 1024 function record(value: unknown): Record { - return isRecordLike(value) ? value : {} + return toRecord(value) } function errorMessage(value: unknown): string { diff --git a/apps/sim/lib/internal/servicenow/client.ts b/apps/sim/lib/internal/servicenow/client.ts index 415afc0db90..865b24b83e5 100644 --- a/apps/sim/lib/internal/servicenow/client.ts +++ b/apps/sim/lib/internal/servicenow/client.ts @@ -1,4 +1,4 @@ -import { isRecordLike } from '@sim/utils/object' +import { isRecordLike, toRecordOrNull } from '@sim/utils/object' import { MAX_JSON_API_RESPONSE_BYTES, secureFetchWithValidation, @@ -45,7 +45,7 @@ export async function uploadServiceNowAttachment( ) const data = await response.json().catch(() => null) if (!response.ok) { - const error = isRecordLike(data) && isRecordLike(data.error) ? data.error : null + const error = isRecordLike(data) && toRecordOrNull(data.error) const message = error && typeof error.message === 'string' ? error.message diff --git a/apps/sim/lib/internal/stagehand/schema-conversion.ts b/apps/sim/lib/internal/stagehand/schema-conversion.ts index 9b90cf9485d..048727cb6ef 100644 --- a/apps/sim/lib/internal/stagehand/schema-conversion.ts +++ b/apps/sim/lib/internal/stagehand/schema-conversion.ts @@ -1,5 +1,5 @@ import type { Logger } from '@sim/logger' -import { isRecordLike } from '@sim/utils/object' +import { isRecordLike, toRecord } from '@sim/utils/object' import { z } from 'zod' function jsonSchemaToZod(logger: Logger, jsonSchema: Record): z.ZodType { @@ -12,7 +12,7 @@ function jsonSchemaToZod(logger: Logger, jsonSchema: Record): z : [] ) for (const [key, property] of Object.entries(jsonSchema.properties)) { - const propertySchema = isRecordLike(property) ? property : {} + const propertySchema = toRecord(property) let fieldSchema = jsonSchemaToZod(logger, propertySchema) if (typeof propertySchema.description === 'string') { fieldSchema = fieldSchema.describe(propertySchema.description) diff --git a/apps/sim/lib/internal/vanta/normalizers.ts b/apps/sim/lib/internal/vanta/normalizers.ts index 2976835a35c..149c1305745 100644 --- a/apps/sim/lib/internal/vanta/normalizers.ts +++ b/apps/sim/lib/internal/vanta/normalizers.ts @@ -1,4 +1,4 @@ -import { isRecordLike } from '@sim/utils/object' +import { isRecordLike, toRecord } from '@sim/utils/object' import type { VantaControl, VantaControlDetail, @@ -34,7 +34,7 @@ type JsonRecord = Record * normalizers can run on it; non-object bodies normalize to all-null fields. */ export function asVantaRecord(value: unknown): JsonRecord { - return isRecordLike(value) ? value : {} + return toRecord(value) } function getString(value: unknown): string | null { diff --git a/apps/sim/lib/internal/vision/client.ts b/apps/sim/lib/internal/vision/client.ts index e3cdc7703bb..0a09ae97322 100644 --- a/apps/sim/lib/internal/vision/client.ts +++ b/apps/sim/lib/internal/vision/client.ts @@ -1,6 +1,6 @@ import { GoogleGenAI } from '@google/genai' import { createLogger } from '@sim/logger' -import { isRecordLike } from '@sim/utils/object' +import { toRecord } from '@sim/utils/object' import type { EgressProfile } from '@/lib/core/security/egress/profiles' import { MAX_JSON_API_RESPONSE_BYTES, @@ -40,7 +40,7 @@ export interface VisionAnalysisResult { } function record(value: unknown): Record { - return isRecordLike(value) ? value : {} + return toRecord(value) } function number(value: unknown): number | undefined { diff --git a/apps/sim/lib/internal/whatsapp/client.ts b/apps/sim/lib/internal/whatsapp/client.ts index ff041fe0219..8fe0cd52cca 100644 --- a/apps/sim/lib/internal/whatsapp/client.ts +++ b/apps/sim/lib/internal/whatsapp/client.ts @@ -1,4 +1,4 @@ -import { isRecordLike } from '@sim/utils/object' +import { toRecord } from '@sim/utils/object' import { readResponseTextWithLimit } from '@/lib/core/utils/stream-limits' export const MAX_WHATSAPP_GRAPH_RESPONSE_BYTES = 256 * 1024 @@ -14,5 +14,5 @@ export async function readWhatsAppGraphResponse( signal, }) const parsed = text ? (JSON.parse(text) as unknown) : {} - return isRecordLike(parsed) ? parsed : {} + return toRecord(parsed) } diff --git a/apps/sim/lib/webhooks/polling/types.ts b/apps/sim/lib/webhooks/polling/types.ts index 2647b22c1aa..4fa80be849f 100644 --- a/apps/sim/lib/webhooks/polling/types.ts +++ b/apps/sim/lib/webhooks/polling/types.ts @@ -1,6 +1,6 @@ import type { webhook, workflow } from '@sim/db/schema' import type { Logger } from '@sim/logger' -import { isRecordLike } from '@sim/utils/object' +import { toRecord } from '@sim/utils/object' /** Summary returned after polling all webhooks for a provider. */ export interface PollSummary { @@ -23,7 +23,7 @@ export type WorkflowRecord = typeof workflow.$inferSelect export function getProviderConfigRecord( providerConfig: WebhookRecord['providerConfig'] ): Record { - return isRecordLike(providerConfig) ? providerConfig : {} + return toRecord(providerConfig) } export function getProviderConfig( diff --git a/apps/sim/lib/webhooks/providers/ashby.ts b/apps/sim/lib/webhooks/providers/ashby.ts index ac0ab6e1ac5..c6b6bcff0ba 100644 --- a/apps/sim/lib/webhooks/providers/ashby.ts +++ b/apps/sim/lib/webhooks/providers/ashby.ts @@ -2,7 +2,7 @@ import { createLogger } from '@sim/logger' import { safeCompare } from '@sim/security/compare' import { hmacSha256Hex } from '@sim/security/hmac' import { generateId } from '@sim/utils/id' -import { isRecordLike, omit } from '@sim/utils/object' +import { isRecordLike, omit, toRecord } from '@sim/utils/object' import { NextResponse } from 'next/server' import { isPayloadSizeLimitError, readResponseJsonWithLimit } from '@/lib/core/utils/stream-limits' import { getNotificationUrl, getProviderConfig } from '@/lib/webhooks/provider-subscription-utils' @@ -104,7 +104,7 @@ async function readAshbyManagementResponse( maxBytes: MAX_ASHBY_WEBHOOK_RESPONSE_BYTES, label, }) - return isRecordLike(body) ? body : {} + return toRecord(body) } catch (error) { if (isPayloadSizeLimitError(error)) throw error return {} diff --git a/apps/sim/lib/webhooks/providers/emailbison.ts b/apps/sim/lib/webhooks/providers/emailbison.ts index da1839eeb15..d0248485119 100644 --- a/apps/sim/lib/webhooks/providers/emailbison.ts +++ b/apps/sim/lib/webhooks/providers/emailbison.ts @@ -43,8 +43,8 @@ export const emailBisonHandler: WebhookProviderHandler = { async formatInput({ body, webhook }: FormatInputContext): Promise { const payload = isRecordLike(body) ? unwrapEmailBisonPayload(body) : {} - const event = isRecordLike(payload.event) ? payload.event : null - const data = isRecordLike(payload.data) ? payload.data : null + const event = toRecordOrNull(payload.event) + const data = toRecordOrNull(payload.data) const providerConfig = getProviderConfig(webhook) const triggerId = providerConfig.triggerId as string | undefined const input: Record = { @@ -285,7 +285,7 @@ async function parseJsonResponse( ): Promise | null> { try { const body: unknown = await response.json() - return isRecordLike(body) ? body : null + return toRecordOrNull(body) } catch { return null } diff --git a/apps/sim/lib/webhooks/providers/github.ts b/apps/sim/lib/webhooks/providers/github.ts index 4a94d395bed..0c158b82f0a 100644 --- a/apps/sim/lib/webhooks/providers/github.ts +++ b/apps/sim/lib/webhooks/providers/github.ts @@ -1,7 +1,7 @@ import crypto from 'crypto' import { createLogger } from '@sim/logger' import { safeCompare } from '@sim/security/compare' -import { isRecordLike } from '@sim/utils/object' +import { isRecordLike, toRecord } from '@sim/utils/object' import { NextResponse } from 'next/server' import type { AuthContext, @@ -146,7 +146,7 @@ export const githubHandler: WebhookProviderHandler = { providerConfig, }: EventMatchContext) { const triggerId = providerConfig.triggerId as string | undefined - const obj = isRecordLike(body) ? body : {} + const obj = toRecord(body) if (triggerId && triggerId !== 'github_webhook') { const eventType = request.headers.get('x-github-event') diff --git a/apps/sim/lib/webhooks/providers/instantly.ts b/apps/sim/lib/webhooks/providers/instantly.ts index 30acd025ce1..e35d5aab179 100644 --- a/apps/sim/lib/webhooks/providers/instantly.ts +++ b/apps/sim/lib/webhooks/providers/instantly.ts @@ -1,7 +1,7 @@ import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' import { generateShortId } from '@sim/utils/id' -import { isRecordLike } from '@sim/utils/object' +import { isRecordLike, toRecord, toRecordOrNull } from '@sim/utils/object' import { NextResponse } from 'next/server' import { getNotificationUrl, getProviderConfig } from '@/lib/webhooks/provider-subscription-utils' import type { @@ -90,7 +90,7 @@ export const instantlyHandler: WebhookProviderHandler = { }, async formatInput({ body }: FormatInputContext): Promise { - const payload = isRecordLike(body) ? body : {} + const payload = toRecord(body) return { input: { @@ -265,7 +265,7 @@ export const instantlyHandler: WebhookProviderHandler = { async function parseJsonResponse(response: Response): Promise | null> { try { const body: unknown = await response.json() - return isRecordLike(body) ? body : null + return toRecordOrNull(body) } catch { return null } diff --git a/apps/sim/lib/webhooks/providers/jira.ts b/apps/sim/lib/webhooks/providers/jira.ts index 3e7d850ab76..4bcabf77652 100644 --- a/apps/sim/lib/webhooks/providers/jira.ts +++ b/apps/sim/lib/webhooks/providers/jira.ts @@ -1,7 +1,7 @@ import { createLogger } from '@sim/logger' import { safeCompare } from '@sim/security/compare' import { hmacSha256Hex } from '@sim/security/hmac' -import { isRecordLike } from '@sim/utils/object' +import { isRecordLike, toRecord } from '@sim/utils/object' import type { EventMatchContext, FormatInputContext, @@ -111,7 +111,7 @@ export const jiraHandler: WebhookProviderHandler = { } if (!triggerId || triggerId === 'jira_webhook') { - const obj = isRecordLike(body) ? body : {} + const obj = toRecord(body) return { input: { webhookEvent: obj.webhookEvent, @@ -134,7 +134,7 @@ export const jiraHandler: WebhookProviderHandler = { async matchEvent({ webhook, workflow, body, requestId, providerConfig }: EventMatchContext) { const triggerId = providerConfig.triggerId as string | undefined - const obj = isRecordLike(body) ? body : {} + const obj = toRecord(body) if (triggerId && triggerId !== 'jira_webhook') { const webhookEvent = obj.webhookEvent as string | undefined @@ -172,7 +172,7 @@ export const jiraHandler: WebhookProviderHandler = { }, extractIdempotencyId(body: unknown) { - const obj = isRecordLike(body) ? body : {} + const obj = toRecord(body) const issue = obj.issue as Record | undefined const comment = obj.comment as Record | undefined const worklog = obj.worklog as Record | undefined diff --git a/apps/sim/lib/webhooks/providers/jotform.ts b/apps/sim/lib/webhooks/providers/jotform.ts index bdad7809d73..39de88e8a9b 100644 --- a/apps/sim/lib/webhooks/providers/jotform.ts +++ b/apps/sim/lib/webhooks/providers/jotform.ts @@ -1,7 +1,7 @@ import { db, webhook, workflowDeploymentVersion } from '@sim/db' import { createLogger } from '@sim/logger' import { getErrorMessage } from '@sim/utils/errors' -import { isRecordLike } from '@sim/utils/object' +import { isRecordLike, toRecord } from '@sim/utils/object' import { and, eq, isNull, ne } from 'drizzle-orm' import { getNotificationUrl, getProviderConfig } from '@/lib/webhooks/provider-subscription-utils' import type { @@ -127,7 +127,7 @@ async function activeDeploymentSharesJotformCallback( export const jotformHandler: WebhookProviderHandler = { async formatInput({ body }: FormatInputContext): Promise { - const payload = isRecordLike(body) ? body : {} + const payload = toRecord(body) /* Jotform posts `rawRequest` as a JSON string inside a multipart body. A form whose answers fail to parse is still worth executing on, so a bad string degrades to null diff --git a/apps/sim/lib/webhooks/providers/linear.ts b/apps/sim/lib/webhooks/providers/linear.ts index 0734212bd3b..5b9af23aaff 100644 --- a/apps/sim/lib/webhooks/providers/linear.ts +++ b/apps/sim/lib/webhooks/providers/linear.ts @@ -3,7 +3,7 @@ import { safeCompare } from '@sim/security/compare' import { hmacSha256Hex } from '@sim/security/hmac' import { toError } from '@sim/utils/errors' import { generateId } from '@sim/utils/id' -import { isRecordLike } from '@sim/utils/object' +import { isRecordLike, toRecord } from '@sim/utils/object' import { NextResponse } from 'next/server' import { getNotificationUrl, getProviderConfig } from '@/lib/webhooks/provider-subscription-utils' import type { @@ -107,7 +107,7 @@ export const linearHandler: WebhookProviderHandler = { }, async formatInput({ body }: FormatInputContext): Promise { - const b = isRecordLike(body) ? body : {} + const b = toRecord(body) const rawActor = b.actor let actor: unknown = null if (isRecordLike(rawActor)) { @@ -139,7 +139,7 @@ export const linearHandler: WebhookProviderHandler = { const triggerId = providerConfig.triggerId as string | undefined if (triggerId && !triggerId.endsWith('_webhook') && !triggerId.endsWith('_webhook_v2')) { const { isLinearEventMatch } = await import('@/triggers/linear/utils') - const obj = isRecordLike(body) ? body : {} + const obj = toRecord(body) const action = typeof obj.action === 'string' ? obj.action : undefined const type = typeof obj.type === 'string' ? obj.type : undefined if (!isLinearEventMatch(triggerId, type || '', action)) { diff --git a/apps/sim/lib/webhooks/providers/quickbooks.ts b/apps/sim/lib/webhooks/providers/quickbooks.ts index faacc304dbc..fe48a10ca45 100644 --- a/apps/sim/lib/webhooks/providers/quickbooks.ts +++ b/apps/sim/lib/webhooks/providers/quickbooks.ts @@ -1,6 +1,7 @@ import { createLogger } from '@sim/logger' import { safeCompare } from '@sim/security/compare' import { hmacSha256Base64 } from '@sim/security/hmac' +import { toRecordOrNull } from '@sim/utils/object' import { NextResponse } from 'next/server' import { WebhookDeploymentConfigurationError } from '@/lib/webhooks/providers/errors' import type { @@ -92,11 +93,6 @@ export async function verifyQuickBooksSignatureAgainstVerifierTokenStream( return unauthorized(requestId, 'QuickBooks webhook signature verification failed') } -function asRecord(value: unknown): Record | null { - if (!value || typeof value !== 'object' || Array.isArray(value)) return null - return value as Record -} - export const quickBooksHandler: WebhookProviderHandler = { ingressMode: 'provider', executionMode: 'queue', @@ -145,7 +141,7 @@ export const quickBooksHandler: WebhookProviderHandler = { }, async matchEvent({ body, providerConfig }: EventMatchContext) { - const event = asRecord(body) + const event = toRecordOrNull(body) const triggerId = typeof providerConfig.triggerId === 'string' ? providerConfig.triggerId : '' const eventType = typeof event?.type === 'string' ? event.type : '' const { isQuickBooksEventMatch, quickBooksEventTypesSubBlockId } = await import( @@ -159,7 +155,7 @@ export const quickBooksHandler: WebhookProviderHandler = { }, async formatInput({ body }: FormatInputContext): Promise { - const event = asRecord(body) ?? {} + const event = toRecordOrNull(body) ?? {} const eventType = typeof event.type === 'string' ? event.type : '' const { getQuickBooksTriggerDefinitionByEntity, parseQuickBooksWebhookType } = await import( '@/triggers/quickbooks/quickbooks' @@ -185,7 +181,7 @@ export const quickBooksHandler: WebhookProviderHandler = { }, extractIdempotencyId(body: unknown) { - const event = asRecord(body) + const event = toRecordOrNull(body) return typeof event?.id === 'string' ? event.id : null }, } diff --git a/apps/sim/lib/webhooks/providers/sendblue.ts b/apps/sim/lib/webhooks/providers/sendblue.ts index ec9d700078c..63ca68e52e6 100644 --- a/apps/sim/lib/webhooks/providers/sendblue.ts +++ b/apps/sim/lib/webhooks/providers/sendblue.ts @@ -1,5 +1,5 @@ import { createLogger } from '@sim/logger' -import { isRecordLike } from '@sim/utils/object' +import { isRecordLike, toRecord } from '@sim/utils/object' import { getProviderConfig } from '@/lib/webhooks/provider-subscription-utils' import type { EventMatchContext, @@ -67,7 +67,7 @@ export const sendblueHandler: WebhookProviderHandler = { }, async formatInput({ body }: FormatInputContext): Promise { - const b = isRecordLike(body) ? body : {} + const b = toRecord(body) return { input: { account_email: b.accountEmail ?? b.account_email ?? null, diff --git a/apps/sim/lib/webhooks/providers/sentry.ts b/apps/sim/lib/webhooks/providers/sentry.ts index b16feb0ad40..ab08586967e 100644 --- a/apps/sim/lib/webhooks/providers/sentry.ts +++ b/apps/sim/lib/webhooks/providers/sentry.ts @@ -1,7 +1,7 @@ import { createLogger } from '@sim/logger' import { safeCompare } from '@sim/security/compare' import { hmacSha256Hex } from '@sim/security/hmac' -import { isRecordLike } from '@sim/utils/object' +import { isRecordLike, toRecord } from '@sim/utils/object' import type { EventMatchContext, FormatInputContext, @@ -58,7 +58,7 @@ export const sentryHandler: WebhookProviderHandler = { const triggerId = providerConfig.triggerId as string | undefined if (triggerId) { const resource = request.headers.get(SENTRY_RESOURCE_HEADER) - const obj = isRecordLike(body) ? body : {} + const obj = toRecord(body) const action = typeof obj.action === 'string' ? obj.action : undefined const { isSentryEventMatch } = await import('@/triggers/sentry/utils') @@ -73,8 +73,8 @@ export const sentryHandler: WebhookProviderHandler = { }, async formatInput({ body, headers }: FormatInputContext): Promise { - const b = isRecordLike(body) ? body : {} - const data = isRecordLike(b.data) ? b.data : {} + const b = toRecord(body) + const data = toRecord(b.data) const resource = headers[SENTRY_RESOURCE_HEADER] || '' const envelope = { @@ -115,7 +115,7 @@ export const sentryHandler: WebhookProviderHandler = { extractIdempotencyId(body: unknown): string | null { if (!isRecordLike(body)) return null - const data = isRecordLike(body.data) ? body.data : {} + const data = toRecord(body.data) const action = typeof body.action === 'string' ? body.action : '' const issue = isRecordLike(data.issue) ? data.issue : undefined diff --git a/apps/sim/lib/webhooks/providers/slack.ts b/apps/sim/lib/webhooks/providers/slack.ts index 196638260f4..af61151108a 100644 --- a/apps/sim/lib/webhooks/providers/slack.ts +++ b/apps/sim/lib/webhooks/providers/slack.ts @@ -4,7 +4,7 @@ import { createLogger } from '@sim/logger' import { safeCompare } from '@sim/security/compare' import { hmacSha256Hex } from '@sim/security/hmac' import { toError } from '@sim/utils/errors' -import { isRecordLike } from '@sim/utils/object' +import { isRecordLike, toRecord } from '@sim/utils/object' import { eq } from 'drizzle-orm' import { NextResponse } from 'next/server' import { @@ -907,7 +907,7 @@ export const slackHandler: WebhookProviderHandler = { * (app_mention, message, reaction_added, ... nested under `event`). */ async formatInput({ body, webhook, requestId }: FormatInputContext): Promise { - const b = isRecordLike(body) ? body : {} + const b = toRecord(body) const providerConfig = (webhook.providerConfig as Record) || {} let botToken = providerConfig.botToken as string | undefined // Reusable custom Slack bot credential: use its stored bot token directly. diff --git a/apps/sim/lib/webhooks/providers/twilio-voice.ts b/apps/sim/lib/webhooks/providers/twilio-voice.ts index f6dd8308322..5e0e7bb0a95 100644 --- a/apps/sim/lib/webhooks/providers/twilio-voice.ts +++ b/apps/sim/lib/webhooks/providers/twilio-voice.ts @@ -1,4 +1,4 @@ -import { isRecordLike } from '@sim/utils/object' +import { isRecordLike, toRecord } from '@sim/utils/object' import { NextResponse } from 'next/server' import { verifyTwilioAuth } from '@/lib/webhooks/providers/twilio-signature' import type { @@ -80,7 +80,7 @@ export const twilioVoiceHandler: WebhookProviderHandler = { }, async formatInput({ body }: FormatInputContext): Promise { - const b = isRecordLike(body) ? body : {} + const b = toRecord(body) return { input: { callSid: b.CallSid, diff --git a/apps/sim/lib/webhooks/providers/twilio.ts b/apps/sim/lib/webhooks/providers/twilio.ts index 3ac6d1f3136..6c4ac9fe5e3 100644 --- a/apps/sim/lib/webhooks/providers/twilio.ts +++ b/apps/sim/lib/webhooks/providers/twilio.ts @@ -1,4 +1,4 @@ -import { isRecordLike } from '@sim/utils/object' +import { isRecordLike, toRecord } from '@sim/utils/object' import { verifyTwilioAuth } from '@/lib/webhooks/providers/twilio-signature' import type { AuthContext, @@ -37,7 +37,7 @@ export const twilioHandler: WebhookProviderHandler = { matchEvent({ body, providerConfig }: EventMatchContext) { const triggerId = providerConfig.triggerId as string | undefined if (!triggerId) return true - const b = isRecordLike(body) ? body : {} + const b = toRecord(body) const messageStatus = ((b.MessageStatus as string) ?? '').toLowerCase() const smsStatus = ((b.SmsStatus as string) ?? '').toLowerCase() const isInbound = smsStatus === 'received' || messageStatus === 'received' @@ -67,7 +67,7 @@ export const twilioHandler: WebhookProviderHandler = { }, async formatInput({ body }: FormatInputContext): Promise { - const b = isRecordLike(body) ? body : {} + const b = toRecord(body) return { input: { messageSid: b.MessageSid, diff --git a/apps/sim/lib/workflows/executor/human-in-the-loop-manager.ts b/apps/sim/lib/workflows/executor/human-in-the-loop-manager.ts index 818644c1f0c..8125ead3911 100644 --- a/apps/sim/lib/workflows/executor/human-in-the-loop-manager.ts +++ b/apps/sim/lib/workflows/executor/human-in-the-loop-manager.ts @@ -3,7 +3,7 @@ import { pausedExecutions, resumeQueue, workflowExecutionLogs } from '@sim/db/sc import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' import { generateId } from '@sim/utils/id' -import { isRecordLike, omit } from '@sim/utils/object' +import { isRecordLike, omit, toRecord } from '@sim/utils/object' import type { Edge } from '@xyflow/react' import { and, asc, desc, eq, inArray, lt, type SQL, sql } from 'drizzle-orm' import { releaseExecutionSlot } from '@/lib/billing/calculations/usage-reservation' @@ -3548,7 +3548,7 @@ export class PauseResumeManager { const { entry, pausedExecution } = pendingEntry const resumeMetadata = parsePausedExecutionResumeMetadata(pausedExecution.metadata) - const pausedMetadata = isRecordLike(pausedExecution.metadata) ? pausedExecution.metadata : {} + const pausedMetadata = toRecord(pausedExecution.metadata) PauseResumeManager.startResumeExecution({ resumeEntryId: entry.id, diff --git a/apps/sim/lib/workflows/references/binding-targets.ts b/apps/sim/lib/workflows/references/binding-targets.ts index d1f331aec36..3e2561e2e1b 100644 --- a/apps/sim/lib/workflows/references/binding-targets.ts +++ b/apps/sim/lib/workflows/references/binding-targets.ts @@ -16,7 +16,7 @@ import { workspaceFiles, workspaceSandbox, } from '@sim/db/schema' -import { isRecordLike } from '@sim/utils/object' +import { isRecordLike, toRecord } from '@sim/utils/object' import { and, eq, inArray, isNull, type SQL, sql } from 'drizzle-orm' import { authorizeCredentialUseForAuth } from '@/lib/auth/credential-access' import { OrchestrationError } from '@/lib/core/orchestration/types' @@ -129,7 +129,7 @@ export async function validateWorkflowBindingTargets( type: tool.type, operation: typeof tool.operation === 'string' ? tool.operation : undefined, toolId: typeof tool.toolId === 'string' ? tool.toolId : undefined, - params: isRecordLike(tool.params) ? tool.params : {}, + params: toRecord(tool.params), }, toolIndex: typeof path[0] === 'number' ? path[0] : undefined, parentCanonicalModes: block.data?.canonicalModes, diff --git a/apps/sim/lib/workflows/references/dependent-reconfigs.ts b/apps/sim/lib/workflows/references/dependent-reconfigs.ts index 979c3ddaafa..30e98a60492 100644 --- a/apps/sim/lib/workflows/references/dependent-reconfigs.ts +++ b/apps/sim/lib/workflows/references/dependent-reconfigs.ts @@ -1,4 +1,4 @@ -import { isRecordLike } from '@sim/utils/object' +import { isRecordLike, toRecord } from '@sim/utils/object' import type { ForkDependentReconfig, ForkResourceUsage } from '@/lib/api/contracts/workspace-fork' import { coerceObjectArray } from '@/lib/workflows/persistence/remap-internal-ids' import { toScannerBlocks } from '@/lib/workflows/references/reference-scan' @@ -399,7 +399,7 @@ export function collectForkDependentReconfigs( } const toolConfig = getBlock(tool.type) if (!toolConfig) continue - const toolParams = isRecordLike(tool.params) ? tool.params : {} + const toolParams = toRecord(tool.params) // A tool's `operation` is stored at the tool level, not in params, but subblock // conditions reference it (e.g. a Gmail label only under `read_gmail`). Merge it // in so condition-gating matches the editor's `{ operation, ...params }`. diff --git a/apps/sim/lib/workflows/references/import-configuration.ts b/apps/sim/lib/workflows/references/import-configuration.ts index 30977b0a6c2..0b6607877fc 100644 --- a/apps/sim/lib/workflows/references/import-configuration.ts +++ b/apps/sim/lib/workflows/references/import-configuration.ts @@ -1,5 +1,5 @@ import type { Principal } from '@sim/auth/principal' -import { isRecordLike } from '@sim/utils/object' +import { isRecordLike, toRecord } from '@sim/utils/object' import { OrchestrationError } from '@/lib/core/orchestration/types' import { getSelectorManifestEntry, @@ -106,7 +106,7 @@ export async function inspectImportConfiguration( subBlocks = Object.fromEntries( Object.entries({ operation: tool.operation, - ...(isRecordLike(tool.params) ? tool.params : {}), + ...toRecord(tool.params), }).map(([key, value]) => [key, { value }]) ) canonicalModes = scopeCanonicalModesForTool( diff --git a/apps/sim/lib/workflows/references/remap-references.ts b/apps/sim/lib/workflows/references/remap-references.ts index 64446d67c27..3a3e88609cb 100644 --- a/apps/sim/lib/workflows/references/remap-references.ts +++ b/apps/sim/lib/workflows/references/remap-references.ts @@ -1,6 +1,6 @@ import { createLogger } from '@sim/logger' import { getErrorMessage } from '@sim/utils/errors' -import { isRecordLike, omit } from '@sim/utils/object' +import { isRecordLike, omit, toRecord } from '@sim/utils/object' import type { SubBlockType } from '@sim/workflow-types/blocks' import { isWorkflowAnnotationOnlyBlockType } from '@sim/workflow-types/workflow' import { readFolderPaths } from '@/lib/folders/selection' @@ -1667,8 +1667,8 @@ function collectClearedToolParamDependents( if (!isRecordLike(targetTool) || targetTool.type !== tool.type) continue const toolConfig = getBlock(tool.type) if (!toolConfig) continue - const targetParams = isRecordLike(targetTool.params) ? targetTool.params : {} - const mergedParams = isRecordLike(tool.params) ? tool.params : {} + const targetParams = toRecord(targetTool.params) + const mergedParams = toRecord(tool.params) // A tool's `operation` lives at the tool level, not in params, but conditions // reference it - merge it in so condition/required gating matches the editor. const mergedValues = @@ -1844,7 +1844,7 @@ export function readTargetDraftDependentValue( if (!isRecordLike(targetTool) || typeof targetTool.type !== 'string') return '' const sourceTool = coerceObjectArray(sourceSubBlocks?.[toolInputId]?.value).array?.[index] if (!isRecordLike(sourceTool) || sourceTool.type !== targetTool.type) return '' - const params = isRecordLike(targetTool.params) ? targetTool.params : {} + const params = toRecord(targetTool.params) const value = params[paramId] return selectionValue(value) } @@ -1878,7 +1878,7 @@ function applyNestedToolOverrides( if (!toolConfig && !isMcp) return tool const allowed = reconfigurableDependentIds(toolConfig?.subBlocks ?? []) if (isMcp) allowed.add('toolName') - const params = isRecordLike(tool.params) ? tool.params : {} + const params = toRecord(tool.params) let nextParams: Record | null = null for (const item of forTool) { if (!allowed.has(item.paramId)) continue diff --git a/apps/sim/lib/workflows/sanitization/json-sanitizer.ts b/apps/sim/lib/workflows/sanitization/json-sanitizer.ts index 297a4c3af6c..98d05e14bfc 100644 --- a/apps/sim/lib/workflows/sanitization/json-sanitizer.ts +++ b/apps/sim/lib/workflows/sanitization/json-sanitizer.ts @@ -1,4 +1,4 @@ -import { isRecordLike, sortObjectKeysDeep } from '@sim/utils/object' +import { isRecordLike, sortObjectKeysDeep, toRecord } from '@sim/utils/object' import { normalizeWorkflowEdgeSourceHandle } from '@sim/workflow-types/workflow' import type { Edge } from '@xyflow/react' import { getBaseUrl } from '@/lib/core/utils/urls' @@ -90,7 +90,7 @@ interface SanitizedCondition { } function toSanitizedCondition(condition: unknown): SanitizedCondition { - const record = isRecordLike(condition) ? condition : {} + const record = toRecord(condition) return { id: String(record.id ?? ''), title: String(record.title ?? ''), diff --git a/apps/sim/providers/conversation-generation.ts b/apps/sim/providers/conversation-generation.ts index f86b8eebcd0..6aa5f0ac437 100644 --- a/apps/sim/providers/conversation-generation.ts +++ b/apps/sim/providers/conversation-generation.ts @@ -1,5 +1,5 @@ import { createLogger } from '@sim/logger' -import { isRecordLike, omit } from '@sim/utils/object' +import { isRecordLike, omit, toRecord } from '@sim/utils/object' import { AgentContextLimitError, type ConversationContextGroup, @@ -248,8 +248,8 @@ function tokenCount(value: unknown, model: string): number { } function outputTokens(request: ProviderRequest, payload: Record): number { - const config = isRecordLike(payload.config) ? payload.config : {} - const inference = isRecordLike(payload.inferenceConfig) ? payload.inferenceConfig : {} + const config = toRecord(payload.config) + const inference = toRecord(payload.inferenceConfig) for (const value of [ payload.max_output_tokens, payload.max_completion_tokens, diff --git a/apps/sim/providers/history-adapters.ts b/apps/sim/providers/history-adapters.ts index 3473eddda37..7c629841129 100644 --- a/apps/sim/providers/history-adapters.ts +++ b/apps/sim/providers/history-adapters.ts @@ -1,4 +1,4 @@ -import { isRecordLike } from '@sim/utils/object' +import { isRecordLike, toRecord } from '@sim/utils/object' import type { CapturedConversationStep, ConversationProtocol, @@ -23,7 +23,7 @@ function capture( protocol: ConversationProtocol, value: unknown ): ReturnType { - const message = isRecordLike(value) ? value : {} + const message = toRecord(value) const calls: CapturedConversationStep['calls'] = [] const text: string[] = [] const add = (id: unknown, name: unknown, args: unknown) => { diff --git a/apps/sim/stores/resource-list-preferences/store.ts b/apps/sim/stores/resource-list-preferences/store.ts index e1badaf95c3..7e196db2ade 100644 --- a/apps/sim/stores/resource-list-preferences/store.ts +++ b/apps/sim/stores/resource-list-preferences/store.ts @@ -2,7 +2,7 @@ import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' -import { isRecordLike } from '@sim/utils/object' +import { isRecordLike, toRecord } from '@sim/utils/object' import { create } from 'zustand' import { createJSONStorage, devtools, persist } from 'zustand/middleware' import type { @@ -152,7 +152,7 @@ export const useResourceListPreferencesStore = create ({ preferences: state.preferences }), migrate: () => ({ preferences: {} }), merge: (persisted, current) => { - const persistedState = isRecordLike(persisted) ? persisted : {} + const persistedState = toRecord(persisted) return { ...current, preferences: sanitizePreferences(persistedState.preferences), diff --git a/apps/sim/tools/bitbucket/utils.ts b/apps/sim/tools/bitbucket/utils.ts index bb7639d5706..c2f02c4273c 100644 --- a/apps/sim/tools/bitbucket/utils.ts +++ b/apps/sim/tools/bitbucket/utils.ts @@ -1,3 +1,4 @@ +import { toRecordOrNull } from '@sim/utils/object' import type { BitbucketBranch, BitbucketComment, @@ -96,14 +97,8 @@ export const BITBUCKET_PAGINATION_PARAMS = { type JsonRecord = Record -function asRecord(value: unknown): JsonRecord | null { - return typeof value === 'object' && value !== null && !Array.isArray(value) - ? (value as JsonRecord) - : null -} - function readRecord(record: JsonRecord | null, key: string): JsonRecord | null { - return asRecord(record?.[key]) + return toRecordOrNull(record?.[key]) } function readString(record: JsonRecord | null, key: string): string | null { @@ -130,7 +125,7 @@ function readRequiredString(record: JsonRecord, key: string, context: string): s } function requireResourceRecord(value: unknown, context: string): JsonRecord { - const record = asRecord(value) + const record = toRecordOrNull(value) if (!record) throw new Error(`Bitbucket ${context} must be an object`) readRequiredString(record, 'type', context) return record @@ -417,7 +412,7 @@ export async function assertBitbucketResponseOk(response: Response): Promise { const data: unknown = await response.json() - const record = asRecord(data) + const record = toRecordOrNull(data) if (!record) throw new Error('Bitbucket returned a non-object JSON response') return record } @@ -687,7 +682,7 @@ export function normalizeBitbucketFileMetadata(value: unknown): BitbucketFileMet } function normalizePullRequestEndpoint(value: unknown): BitbucketPullRequestEndpoint | null { - const data = asRecord(value) + const data = toRecordOrNull(value) if (!data) return null return { branchName: readString(readRecord(data, 'branch'), 'name'), @@ -867,7 +862,7 @@ function normalizePipelineCommands( const commands = readOptionalArray(data, key, 'pipeline step') return ( commands?.map((command, index) => { - const commandData = asRecord(command) + const commandData = toRecordOrNull(command) if (!commandData) { throw new Error(`Bitbucket pipeline step.${key}[${index}] must be an object`) } diff --git a/apps/sim/tools/cbinsights/utils.ts b/apps/sim/tools/cbinsights/utils.ts index db88c45bb25..d092c2749ab 100644 --- a/apps/sim/tools/cbinsights/utils.ts +++ b/apps/sim/tools/cbinsights/utils.ts @@ -562,13 +562,6 @@ export function asStringArray(value: unknown): string[] { : [] } -/** Narrows an optional object field. */ -export function asRecord(value: unknown): CbInsightsRecord | null { - return typeof value === 'object' && value !== null && !Array.isArray(value) - ? (value as CbInsightsRecord) - : null -} - /** Narrows an optional string field. */ export function asString(value: unknown): string | null { return typeof value === 'string' ? value : null diff --git a/apps/sim/tools/clickup/create_comment.ts b/apps/sim/tools/clickup/create_comment.ts index ffb8742e9c7..8a29c6bccdd 100644 --- a/apps/sim/tools/clickup/create_comment.ts +++ b/apps/sim/tools/clickup/create_comment.ts @@ -1,4 +1,4 @@ -import { isRecordLike } from '@sim/utils/object' +import { toRecord } from '@sim/utils/object' import { CLICKUP_API_BASE_URL, clickupAuthorizationHeader, @@ -85,7 +85,7 @@ export const clickupCreateCommentTool: ToolConfig< return { success: false, output: { error }, error } } - const record = isRecordLike(data) ? data : {} + const record = toRecord(data) const id = record.id const histId = record.hist_id const rawDate = record.date diff --git a/apps/sim/tools/clickup/delete_time_entry.ts b/apps/sim/tools/clickup/delete_time_entry.ts index 47c2f9927fd..045832b30a9 100644 --- a/apps/sim/tools/clickup/delete_time_entry.ts +++ b/apps/sim/tools/clickup/delete_time_entry.ts @@ -1,4 +1,4 @@ -import { isRecordLike } from '@sim/utils/object' +import { isRecordLike, toRecordOrNull } from '@sim/utils/object' import { CLICKUP_API_BASE_URL, CLICKUP_TIME_ENTRY_OUTPUT_PROPERTIES, @@ -65,9 +65,7 @@ export const clickupDeleteTimeEntryTool: ToolConfig< const payload = isRecordLike(data) ? data.data : null const entry = Array.isArray(payload) ? (payload.find((item) => isRecordLike(item)) ?? null) - : isRecordLike(payload) - ? payload - : null + : toRecordOrNull(payload) return { success: true, diff --git a/apps/sim/tools/clickup/get_running_timer.ts b/apps/sim/tools/clickup/get_running_timer.ts index 9c616839068..dee4d7c68ce 100644 --- a/apps/sim/tools/clickup/get_running_timer.ts +++ b/apps/sim/tools/clickup/get_running_timer.ts @@ -1,4 +1,4 @@ -import { isRecordLike } from '@sim/utils/object' +import { isRecordLike, toRecordOrNull } from '@sim/utils/object' import { CLICKUP_API_BASE_URL, CLICKUP_TIME_ENTRY_OUTPUT_PROPERTIES, @@ -68,7 +68,7 @@ export const clickupGetRunningTimerTool: ToolConfig< return { success: false, output: { error }, error } } - const entry = isRecordLike(data) && isRecordLike(data.data) ? data.data : null + const entry = isRecordLike(data) && toRecordOrNull(data.data) return { success: true, diff --git a/apps/sim/tools/clickup/shared.ts b/apps/sim/tools/clickup/shared.ts index 2d3cebec811..07db3398853 100644 --- a/apps/sim/tools/clickup/shared.ts +++ b/apps/sim/tools/clickup/shared.ts @@ -1,4 +1,4 @@ -import { isRecordLike } from '@sim/utils/object' +import { isRecordLike, toRecordOrNull } from '@sim/utils/object' import type { ClickUpAttachment, ClickUpChecklist, @@ -491,7 +491,7 @@ export function mapClickUpCustomField(value: unknown): ClickUpCustomField { id: getRequiredString(value.id, 'id'), name: getOptionalString(value.name), type: getOptionalString(value.type), - typeConfig: isRecordLike(value.type_config) ? value.type_config : null, + typeConfig: toRecordOrNull(value.type_config), dateCreated: getOptionalString(value.date_created), hideFromGuests: getOptionalBoolean(value.hide_from_guests), } @@ -542,7 +542,7 @@ export function mapClickUpTimeEntry(value: unknown): ClickUpTimeEntry { const rawTags = Array.isArray(value.tags) ? value.tags : [] const rawTaskTags = Array.isArray(value.task_tags) ? value.task_tags : [] - const rawLocation = isRecordLike(value.task_location) ? value.task_location : null + const rawLocation = toRecordOrNull(value.task_location) return { id: getRequiredString(value.id, 'id'), diff --git a/apps/sim/tools/clickup/start_timer.ts b/apps/sim/tools/clickup/start_timer.ts index d3ab101c5b4..d83013e580e 100644 --- a/apps/sim/tools/clickup/start_timer.ts +++ b/apps/sim/tools/clickup/start_timer.ts @@ -1,4 +1,4 @@ -import { isRecordLike } from '@sim/utils/object' +import { isRecordLike, toRecordOrNull } from '@sim/utils/object' import { CLICKUP_API_BASE_URL, CLICKUP_TIME_ENTRY_OUTPUT_PROPERTIES, @@ -92,7 +92,7 @@ export const clickupStartTimerTool: ToolConfig { return value } - const record = isRecordLike(value) ? value : {} + const record = toRecord(value) const file = isUserFile(record.file) ? record.file : undefined const metadata = isRecordLike(record.metadata) ? record.metadata : undefined const fallback: FileParseResult = { diff --git a/apps/sim/tools/harmonic/utils.ts b/apps/sim/tools/harmonic/utils.ts index dc3b6fe0b36..7cdc32cce3f 100644 --- a/apps/sim/tools/harmonic/utils.ts +++ b/apps/sim/tools/harmonic/utils.ts @@ -1,3 +1,4 @@ +import { toRecordOrNull } from '@sim/utils/object' import type { HarmonicContact, HarmonicDroppedIdentifier, @@ -145,11 +146,6 @@ export function harmonicHeaders( } } -function asRecord(value: unknown): Record | null { - if (typeof value !== 'object' || value === null || Array.isArray(value)) return null - return value as Record -} - function asString(value: unknown): string | null { if (typeof value !== 'string') return null const trimmed = value.trim() @@ -437,11 +433,11 @@ function normalizeLinkedinProfileUrl(value: unknown): string | null { } function linkedinUrl(socials: HarmonicPersonOutput['socials']): string | null { - const socialRecord = asRecord(socials) + const socialRecord = toRecordOrNull(socials) if (!socialRecord) return null for (const metadata of Object.values(socialRecord)) { - const normalized = normalizeLinkedinProfileUrl(asRecord(metadata)?.url) + const normalized = normalizeLinkedinProfileUrl(toRecordOrNull(metadata)?.url) if (normalized) return normalized } return null @@ -449,8 +445,8 @@ function linkedinUrl(socials: HarmonicPersonOutput['socials']): string | null { export function normalizePerson(raw: HarmonicPersonOutput): HarmonicContact { const normalizedPersonId = requirePersonId(raw.id) - const contact = asRecord(raw.contact) - const location = (asRecord(raw.location) ?? {}) as HarmonicLocationMetadata + const contact = toRecordOrNull(raw.contact) + const location = (toRecordOrNull(raw.location) ?? {}) as HarmonicLocationMetadata const experiences = currentExperience(raw) const primaryEmail = asString(contact?.primary_email) const contactEmails = nullableStringArray(contact?.emails) @@ -531,7 +527,7 @@ export function normalizeScoutPerson(raw: HarmonicScoutPerson): HarmonicContact export function normalizePageInfo(value: unknown): HarmonicPageInfo | null { if (value === undefined || value === null) return null - const pageInfo = asRecord(value) as HarmonicPaginationMetadata | null + const pageInfo = toRecordOrNull(value) as HarmonicPaginationMetadata | null if (!pageInfo) throw new Error('Harmonic returned invalid page_info metadata') if (typeof pageInfo.has_next !== 'boolean') { throw new Error('Harmonic returned page_info without a boolean has_next value') @@ -592,7 +588,7 @@ export function normalizePeopleResults(value: unknown): { continue } - const person = asRecord(result) as HarmonicPersonOutput | null + const person = toRecordOrNull(result) as HarmonicPersonOutput | null const urn = personUrn(person?.entity_urn) if (!person || !urn) { throw new Error('Harmonic saved search returned a non-person result') @@ -607,7 +603,7 @@ export function normalizePeopleResults(value: unknown): { export function normalizePersonArray(value: unknown): HarmonicContact[] { if (!Array.isArray(value)) throw new Error('Harmonic returned an invalid people array') return value.map((item) => { - const person = asRecord(item) as HarmonicPersonOutput | null + const person = toRecordOrNull(item) as HarmonicPersonOutput | null if (!person || !personUrn(person.entity_urn)) { throw new Error('Harmonic returned an invalid person record') } @@ -616,7 +612,7 @@ export function normalizePersonArray(value: unknown): HarmonicContact[] { } export function responseRecord(value: unknown, context: string): Record { - const record = asRecord(value) + const record = toRecordOrNull(value) if (!record) throw new Error(`Harmonic returned an invalid ${context} response`) return record } @@ -937,7 +933,7 @@ export function normalizePersonUrnList(value: unknown, context: string): string[ export function normalizeOptionalPerson(value: unknown): HarmonicContact | null { if (value === undefined || value === null) return null - const person = asRecord(value) + const person = toRecordOrNull(value) if (!person) throw new Error('Harmonic returned an invalid person record') return normalizePerson(person) } diff --git a/apps/sim/tools/instantly/delete_leads.ts b/apps/sim/tools/instantly/delete_leads.ts index 8e313a2df8e..848216912b0 100644 --- a/apps/sim/tools/instantly/delete_leads.ts +++ b/apps/sim/tools/instantly/delete_leads.ts @@ -1,9 +1,9 @@ +import { toRecord } from '@sim/utils/object' import type { InstantlyDeleteLeadsParams, InstantlyDeleteLeadsResponse, } from '@/tools/instantly/types' import { - asRecord, compactBody, instantlyBaseParamFields, instantlyHeaders, @@ -67,7 +67,7 @@ export const deleteLeadsTool: ToolConfig { const data = await parseInstantlyResponse(response) - const result = asRecord(data) + const result = toRecord(data) return { success: true, diff --git a/apps/sim/tools/instantly/update_lead_interest_status.ts b/apps/sim/tools/instantly/update_lead_interest_status.ts index 83e06078144..f29e76696a1 100644 --- a/apps/sim/tools/instantly/update_lead_interest_status.ts +++ b/apps/sim/tools/instantly/update_lead_interest_status.ts @@ -1,9 +1,9 @@ +import { toRecord } from '@sim/utils/object' import type { InstantlyUpdateLeadInterestStatusParams, InstantlyUpdateLeadInterestStatusResponse, } from '@/tools/instantly/types' import { - asRecord, compactBody, instantlyBaseParamFields, instantlyHeaders, @@ -80,7 +80,7 @@ export const updateLeadInterestStatusTool: ToolConfig< }, transformResponse: async (response) => { const data = await parseInstantlyResponse(response) - const result = asRecord(data) + const result = toRecord(data) return { success: true, diff --git a/apps/sim/tools/instantly/utils.ts b/apps/sim/tools/instantly/utils.ts index 43ff7fbee5f..511b64cf823 100644 --- a/apps/sim/tools/instantly/utils.ts +++ b/apps/sim/tools/instantly/utils.ts @@ -1,4 +1,4 @@ -import { filterUndefined, isRecordLike, toRecord } from '@sim/utils/object' +import { filterUndefined, toRecord, toRecordOrNull } from '@sim/utils/object' import type { InstantlyCampaign, InstantlyEmail, @@ -55,23 +55,18 @@ export async function parseInstantlyResponse(response: Response): Promise { } export function getMessage(value: unknown): string | null { - const data = asRecord(value) + const data = toRecord(value) return typeof data.message === 'string' ? data.message : null } function extractInstantlyError(value: unknown, fallback: string): string { - const data = asRecord(value) + const data = toRecord(value) if (typeof data.message === 'string') return data.message if (typeof data.error === 'string') return data.error return fallback diff --git a/apps/sim/tools/jotform/get_form_properties.ts b/apps/sim/tools/jotform/get_form_properties.ts index 9c76c97b454..cf3668641bd 100644 --- a/apps/sim/tools/jotform/get_form_properties.ts +++ b/apps/sim/tools/jotform/get_form_properties.ts @@ -1,4 +1,4 @@ -import { isRecordLike } from '@sim/utils/object' +import { toRecord } from '@sim/utils/object' import type { JotformFormPropertiesResponse, JotformGetFormPropertiesParams, @@ -70,7 +70,7 @@ export const getFormPropertiesTool: ToolConfig< return { success: true, output: { - properties: isRecordLike(envelope.content) ? envelope.content : {}, + properties: toRecord(envelope.content), }, } }, diff --git a/apps/sim/tools/jotform/normalize.ts b/apps/sim/tools/jotform/normalize.ts index bbbab55926a..e1c36b9caf5 100644 --- a/apps/sim/tools/jotform/normalize.ts +++ b/apps/sim/tools/jotform/normalize.ts @@ -1,4 +1,4 @@ -import { isRecordLike } from '@sim/utils/object' +import { isRecordLike, toRecordOrNull } from '@sim/utils/object' import type { JotformFile, JotformForm, @@ -22,8 +22,8 @@ import { toJsonArray, toStringOrNull } from '@/tools/jotform/utils' /** Unwraps the single-element array form some endpoints document for one resource. */ export function unwrapSingle(content: unknown): Record | null { - if (Array.isArray(content)) return isRecordLike(content[0]) ? content[0] : null - return isRecordLike(content) ? content : null + if (Array.isArray(content)) return toRecordOrNull(content[0]) + return toRecordOrNull(content) } export function toList(content: unknown): Record[] { diff --git a/apps/sim/tools/jotform/update_form_properties.ts b/apps/sim/tools/jotform/update_form_properties.ts index 52fece050e9..37ec0e910bf 100644 --- a/apps/sim/tools/jotform/update_form_properties.ts +++ b/apps/sim/tools/jotform/update_form_properties.ts @@ -1,4 +1,4 @@ -import { isRecordLike } from '@sim/utils/object' +import { toRecord } from '@sim/utils/object' import type { JotformFormPropertiesResponse, JotformUpdateFormPropertiesParams, @@ -82,7 +82,7 @@ export const updateFormPropertiesTool: ToolConfig< return { success: true, output: { - properties: isRecordLike(envelope.content) ? envelope.content : {}, + properties: toRecord(envelope.content), }, } }, diff --git a/apps/sim/tools/jotform/utils.ts b/apps/sim/tools/jotform/utils.ts index 4b2ed551e86..d3256196063 100644 --- a/apps/sim/tools/jotform/utils.ts +++ b/apps/sim/tools/jotform/utils.ts @@ -1,5 +1,5 @@ import { getErrorMessage } from '@sim/utils/errors' -import { isRecordLike } from '@sim/utils/object' +import { isRecordLike, toRecord } from '@sim/utils/object' import { truncate } from '@sim/utils/string' /** @@ -234,7 +234,7 @@ export function normalizeSubmissionAnswers( const qid = key.slice(0, separator) const subField = key.slice(separator + 1) const existing = normalized[qid] - const target = isRecordLike(existing) ? existing : {} + const target = toRecord(existing) target[subField] = value normalized[qid] = target } diff --git a/apps/sim/tools/smartlead/add_leads_to_campaign.ts b/apps/sim/tools/smartlead/add_leads_to_campaign.ts index 6a2f38e8395..34f40d90e6e 100644 --- a/apps/sim/tools/smartlead/add_leads_to_campaign.ts +++ b/apps/sim/tools/smartlead/add_leads_to_campaign.ts @@ -1,4 +1,4 @@ -import { isRecordLike } from '@sim/utils/object' +import { toRecord } from '@sim/utils/object' import { ErrorExtractorId } from '@/tools/error-extractors' import type { SmartleadAddLeadsResponse, SmartleadCampaignIdParams } from '@/tools/smartlead/types' import { @@ -37,7 +37,7 @@ interface AddLeadsToCampaignParams extends SmartleadCampaignIdParams { /** Keeps only fields Smartlead accepts so stray keys can't fail the whole import. */ function toLeadPayload(value: unknown): Record { - const record = isRecordLike(value) ? value : {} + const record = toRecord(value) const payload: Record = {} for (const field of LEAD_FIELDS) { diff --git a/apps/sim/tools/smartlead/create_lead_list.ts b/apps/sim/tools/smartlead/create_lead_list.ts index 30fb3f3c1ab..bf8ee1287a6 100644 --- a/apps/sim/tools/smartlead/create_lead_list.ts +++ b/apps/sim/tools/smartlead/create_lead_list.ts @@ -1,4 +1,4 @@ -import { isRecordLike } from '@sim/utils/object' +import { toRecord } from '@sim/utils/object' import { ErrorExtractorId } from '@/tools/error-extractors' import type { SmartleadBaseParams, SmartleadLeadListResponse } from '@/tools/smartlead/types' import { @@ -38,7 +38,7 @@ export const createLeadListTool: ToolConfig { const record = await smartleadRecord(response, 'lead list') - const data = isRecordLike(record.data) ? record.data : {} + const data = toRecord(record.data) return { success: true, diff --git a/apps/sim/tools/smartlead/get_campaign_webhook_summary.ts b/apps/sim/tools/smartlead/get_campaign_webhook_summary.ts index 7862d0b6b00..5ecf315cd52 100644 --- a/apps/sim/tools/smartlead/get_campaign_webhook_summary.ts +++ b/apps/sim/tools/smartlead/get_campaign_webhook_summary.ts @@ -1,4 +1,4 @@ -import { isRecordLike } from '@sim/utils/object' +import { toRecord } from '@sim/utils/object' import { ErrorExtractorId } from '@/tools/error-extractors' import type { SmartleadCampaignIdParams, @@ -58,9 +58,9 @@ export const getCampaignWebhookSummaryTool: ToolConfig< }, transformResponse: async (response) => { const record = await smartleadRecord(response, 'webhook summary') - const data = isRecordLike(record.data) ? record.data : {} + const data = toRecord(record.data) const summary = opaqueRows(data.summary) - const timeRange = isRecordLike(data.timeRange) ? data.timeRange : {} + const timeRange = toRecord(data.timeRange) return { success: true, diff --git a/apps/sim/tools/smartlead/get_lead_list.ts b/apps/sim/tools/smartlead/get_lead_list.ts index d8675403d02..b078da971c3 100644 --- a/apps/sim/tools/smartlead/get_lead_list.ts +++ b/apps/sim/tools/smartlead/get_lead_list.ts @@ -1,4 +1,4 @@ -import { isRecordLike } from '@sim/utils/object' +import { toRecordOrNull } from '@sim/utils/object' import { ErrorExtractorId } from '@/tools/error-extractors' import type { SmartleadBaseParams, SmartleadLeadListResponse } from '@/tools/smartlead/types' import { @@ -38,7 +38,7 @@ export const getLeadListTool: ToolConfig { const record = await smartleadRecord(response, 'lead list') - const data = isRecordLike(record.data) ? record.data : null + const data = toRecordOrNull(record.data) if (!data) throw new Error('Smartlead lead list not found') return { diff --git a/apps/sim/tools/smartlead/list_lead_activities.ts b/apps/sim/tools/smartlead/list_lead_activities.ts index 06f3df6b3ca..2c4f1d177b7 100644 --- a/apps/sim/tools/smartlead/list_lead_activities.ts +++ b/apps/sim/tools/smartlead/list_lead_activities.ts @@ -1,4 +1,4 @@ -import { isRecordLike } from '@sim/utils/object' +import { toRecord } from '@sim/utils/object' import { ErrorExtractorId } from '@/tools/error-extractors' import type { SmartleadBaseParams, SmartleadPaginatedRowsResponse } from '@/tools/smartlead/types' import { @@ -58,7 +58,7 @@ export const listLeadActivitiesTool: ToolConfig< transformResponse: async (response) => { const record = await smartleadRecord(response, 'lead activities') const rows = opaqueRows(record.data) - const pagination = isRecordLike(record.pagination) ? record.pagination : {} + const pagination = toRecord(record.pagination) return { success: true, diff --git a/apps/sim/tools/smartlead/list_lead_lists.ts b/apps/sim/tools/smartlead/list_lead_lists.ts index 954ef390c07..a973997dfc1 100644 --- a/apps/sim/tools/smartlead/list_lead_lists.ts +++ b/apps/sim/tools/smartlead/list_lead_lists.ts @@ -1,4 +1,4 @@ -import { isRecordLike } from '@sim/utils/object' +import { toRecord } from '@sim/utils/object' import { ErrorExtractorId } from '@/tools/error-extractors' import type { SmartleadBaseParams, SmartleadLeadListsResponse } from '@/tools/smartlead/types' import { @@ -48,7 +48,7 @@ export const listLeadListsTool: ToolConfig { const record = await smartleadRecord(response, 'lead lists') - const data = isRecordLike(record.data) ? record.data : {} + const data = toRecord(record.data) const rows = Array.isArray(data.lists) ? data.lists : [] const lists = rows.map(mapLeadList) diff --git a/apps/sim/tools/smartlead/mark_lead_complete.ts b/apps/sim/tools/smartlead/mark_lead_complete.ts index 4375441e1de..b6f0a596daf 100644 --- a/apps/sim/tools/smartlead/mark_lead_complete.ts +++ b/apps/sim/tools/smartlead/mark_lead_complete.ts @@ -1,4 +1,4 @@ -import { isRecordLike } from '@sim/utils/object' +import { toRecord, toRecordOrNull } from '@sim/utils/object' import { ErrorExtractorId } from '@/tools/error-extractors' import type { SmartleadCampaignIdParams, @@ -54,11 +54,11 @@ export const markLeadCompleteTool: ToolConfig< }, transformResponse: async (response) => { const record = await smartleadRecord(response, 'lead completion') - const status = isRecordLike(record.status) ? record.status : {} + const status = toRecord(record.status) // `nextSequence` is an object ({ id, delayInDays }) when a step remains, else null. // Both members go through the shared numeric coercion: Smartlead string-encodes // numbers inconsistently, and `delayInDays` is legitimately "0" for an immediate step. - const next = isRecordLike(status.nextSequence) ? status.nextSequence : null + const next = toRecordOrNull(status.nextSequence) return { success: true, diff --git a/apps/sim/tools/smartlead/save_campaign_sequences.ts b/apps/sim/tools/smartlead/save_campaign_sequences.ts index fef24b5aae3..026bbdd72c1 100644 --- a/apps/sim/tools/smartlead/save_campaign_sequences.ts +++ b/apps/sim/tools/smartlead/save_campaign_sequences.ts @@ -1,4 +1,4 @@ -import { isRecordLike } from '@sim/utils/object' +import { toRecord, toRecordOrNull } from '@sim/utils/object' import { ErrorExtractorId } from '@/tools/error-extractors' import type { SmartleadCampaignIdParams, @@ -26,8 +26,8 @@ interface SaveCampaignSequencesParams extends SmartleadCampaignIdParams { * value is far easier for a model to supply flat. This accepts either form. */ function toSequencePayload(value: unknown, index: number): Record { - const record = isRecordLike(value) ? value : {} - const existingDelay = isRecordLike(record.seq_delay_details) ? record.seq_delay_details : null + const record = toRecord(value) + const existingDelay = toRecordOrNull(record.seq_delay_details) const delayInDays = existingDelay?.delay_in_days ?? existingDelay?.delayInDays ?? @@ -87,7 +87,7 @@ export const saveCampaignSequencesTool: ToolConfig< }, transformResponse: async (response) => { const record = await smartleadRecord(response, 'sequence save') - const data = isRecordLike(record.data) ? record.data : {} + const data = toRecord(record.data) const saved = Array.isArray(data.sequences) ? data.sequences : [] const sequences = saved.map(mapSavedSequence) diff --git a/apps/sim/tools/smartlead/update_lead_list.ts b/apps/sim/tools/smartlead/update_lead_list.ts index 8d213ebf48e..13d540abf65 100644 --- a/apps/sim/tools/smartlead/update_lead_list.ts +++ b/apps/sim/tools/smartlead/update_lead_list.ts @@ -1,4 +1,4 @@ -import { isRecordLike } from '@sim/utils/object' +import { toRecord } from '@sim/utils/object' import { ErrorExtractorId } from '@/tools/error-extractors' import type { SmartleadBaseParams, SmartleadLeadListResponse } from '@/tools/smartlead/types' import { @@ -46,7 +46,7 @@ export const updateLeadListTool: ToolConfig { const record = await smartleadRecord(response, 'lead list') - const data = isRecordLike(record.data) ? record.data : {} + const data = toRecord(record.data) return { success: true, diff --git a/apps/sim/tools/trello/shared.ts b/apps/sim/tools/trello/shared.ts index b1c2346af83..81911ad84a9 100644 --- a/apps/sim/tools/trello/shared.ts +++ b/apps/sim/tools/trello/shared.ts @@ -1,4 +1,4 @@ -import { isRecordLike } from '@sim/utils/object' +import { isRecordLike, toRecordOrNull } from '@sim/utils/object' import type { TrelloAction, TrelloActionBoardTarget, @@ -226,7 +226,7 @@ export function mapTrelloAction(value: unknown): TrelloAction { throw new Error('Trello returned an invalid action object') } - const data = isRecordLike(value.data) ? value.data : null + const data = toRecordOrNull(value.data) return { id: getRequiredString(value.id, 'id'), diff --git a/apps/sim/tools/whatsapp/utils.ts b/apps/sim/tools/whatsapp/utils.ts index 882a5178415..a5165ec275e 100644 --- a/apps/sim/tools/whatsapp/utils.ts +++ b/apps/sim/tools/whatsapp/utils.ts @@ -1,4 +1,4 @@ -import { isRecordLike } from '@sim/utils/object' +import { isRecordLike, toRecord } from '@sim/utils/object' import type { WhatsAppMediaType, WhatsAppSendResponse } from '@/tools/whatsapp/types' /** WhatsApp Cloud API Graph version used by every outbound tool. */ @@ -69,7 +69,7 @@ export function buildAuthHeaders(accessToken: string | undefined): Record> { const responseText = await response.text() const parsed = responseText ? (JSON.parse(responseText) as unknown) : {} - return isRecordLike(parsed) ? parsed : {} + return toRecord(parsed) } /** diff --git a/apps/sim/tools/windchill/utils.ts b/apps/sim/tools/windchill/utils.ts index 01d9d05fbe9..966025b1374 100644 --- a/apps/sim/tools/windchill/utils.ts +++ b/apps/sim/tools/windchill/utils.ts @@ -1,5 +1,5 @@ import { getErrorMessage } from '@sim/utils/errors' -import { isRecordLike, omit } from '@sim/utils/object' +import { isRecordLike, omit, toRecord } from '@sim/utils/object' import { type WindchillOperationResponse, windchillOperationResponseSchema, @@ -213,7 +213,7 @@ export function normalizeWindchillDocuments(value: unknown): WindchillDocument[] } export function windchillPageInfo(value: unknown, pageCount: number) { - const record = isRecordLike(value) ? value : {} + const record = toRecord(value) return { count: pageCount, totalCount: numberValue(record, '@odata.count'), diff --git a/apps/sim/triggers/github/utils.ts b/apps/sim/triggers/github/utils.ts index 4da55d23863..b658e2fc41c 100644 --- a/apps/sim/triggers/github/utils.ts +++ b/apps/sim/triggers/github/utils.ts @@ -1,4 +1,4 @@ -import { isRecordLike } from '@sim/utils/object' +import { isRecordLike, toRecord } from '@sim/utils/object' /** * Shared sender/user output schema @@ -95,7 +95,7 @@ export function isGitHubEventMatch( } if (config.validator) { - return config.validator(isRecordLike(payload) ? payload : {}) + return config.validator(toRecord(payload)) } return true diff --git a/apps/sim/triggers/jira/utils.ts b/apps/sim/triggers/jira/utils.ts index 215533923a0..688edaeb96d 100644 --- a/apps/sim/triggers/jira/utils.ts +++ b/apps/sim/triggers/jira/utils.ts @@ -1,4 +1,4 @@ -import { isRecordLike } from '@sim/utils/object' +import { toRecord } from '@sim/utils/object' import type { SubBlockConfig } from '@/blocks/types' import type { TriggerOutput } from '@/triggers/types' @@ -508,7 +508,7 @@ export function isJiraEventMatch( } export function extractIssueData(body: unknown) { - const obj = isRecordLike(body) ? body : {} + const obj = toRecord(body) return { webhookEvent: obj.webhookEvent, timestamp: obj.timestamp, @@ -520,7 +520,7 @@ export function extractIssueData(body: unknown) { } export function extractCommentData(body: unknown) { - const obj = isRecordLike(body) ? body : {} + const obj = toRecord(body) return { webhookEvent: obj.webhookEvent, timestamp: obj.timestamp, @@ -531,7 +531,7 @@ export function extractCommentData(body: unknown) { } export function extractWorklogData(body: unknown) { - const obj = isRecordLike(body) ? body : {} + const obj = toRecord(body) return { webhookEvent: obj.webhookEvent, timestamp: obj.timestamp, @@ -743,7 +743,7 @@ export function buildVersionReleasedOutputs(): Record { * Extracts sprint data from a Jira webhook payload */ export function extractSprintData(body: unknown) { - const obj = isRecordLike(body) ? body : {} + const obj = toRecord(body) return { webhookEvent: obj.webhookEvent, timestamp: obj.timestamp, @@ -756,7 +756,7 @@ export function extractSprintData(body: unknown) { * Extracts project data from a Jira webhook payload */ export function extractProjectData(body: unknown) { - const obj = isRecordLike(body) ? body : {} + const obj = toRecord(body) return { webhookEvent: obj.webhookEvent, timestamp: obj.timestamp, @@ -769,7 +769,7 @@ export function extractProjectData(body: unknown) { * Extracts version data from a Jira webhook payload */ export function extractVersionData(body: unknown) { - const obj = isRecordLike(body) ? body : {} + const obj = toRecord(body) return { webhookEvent: obj.webhookEvent, timestamp: obj.timestamp, diff --git a/packages/db/scripts/migrate-slack-custom-bots.ts b/packages/db/scripts/migrate-slack-custom-bots.ts index 33f3c84012e..f5740466ca1 100644 --- a/packages/db/scripts/migrate-slack-custom-bots.ts +++ b/packages/db/scripts/migrate-slack-custom-bots.ts @@ -48,7 +48,7 @@ import { createLogger } from '@sim/logger' import { describeError, getErrorMessage, getPostgresErrorCode } from '@sim/utils/errors' import { chunkArray, sleep } from '@sim/utils/helpers' import { generateId } from '@sim/utils/id' -import { isRecordLike } from '@sim/utils/object' +import { isRecordLike, toRecord } from '@sim/utils/object' import { type BackoffOptions, backoffWithJitter } from '@sim/utils/retry' import { truncate } from '@sim/utils/string' import { and, asc, eq, gt, inArray, isNotNull, isNull, sql } from 'drizzle-orm' @@ -400,7 +400,7 @@ export function extractSlackBotSources(block: SlackMigrationBlock): SlackBotSour ) { throw new Error('Slack triggerConfig must be an object when present') } - const triggerConfig = isRecordLike(rawTriggerConfig) ? rawTriggerConfig : {} + const triggerConfig = toRecord(rawTriggerConfig) const rawSigningSecret = preferredTriggerValue(values, triggerConfig, 'signingSecret') const rawBotToken = preferredTriggerValue(values, triggerConfig, 'botToken') const existingBotCredentialId = nonEmptyString(values.botCredential) diff --git a/packages/sim-cli/src/commands/protocol/workflow-run-wait.ts b/packages/sim-cli/src/commands/protocol/workflow-run-wait.ts index 92a60728c06..9bfb512f338 100644 --- a/packages/sim-cli/src/commands/protocol/workflow-run-wait.ts +++ b/packages/sim-cli/src/commands/protocol/workflow-run-wait.ts @@ -1,4 +1,4 @@ -import { isRecordLike } from '@sim/utils/object' +import { isRecordLike, toRecordOrNull } from '@sim/utils/object' import chalk from 'chalk' import { type Command, Option } from 'commander' import { clientFrom } from '../../context' @@ -102,7 +102,7 @@ function readRun(raw: unknown): RunSnapshot { if (!isRecordLike(run) || typeof run.status !== 'string') { throw new SimApiError('Run status response carried no status.', 0) } - const paused = isRecordLike(run.paused) ? run.paused : null + const paused = toRecordOrNull(run.paused) return { status: run.status, pauseKind: paused ? optionalString(paused.pauseKind) : null, diff --git a/packages/sim-setup/src/configuration-sources.ts b/packages/sim-setup/src/configuration-sources.ts index 34fd8047074..b0c6aa483be 100644 --- a/packages/sim-setup/src/configuration-sources.ts +++ b/packages/sim-setup/src/configuration-sources.ts @@ -6,6 +6,7 @@ import { CORE_CONFIGURATION_KEYS, DEPLOYMENT_CONFIGURATION_KEYS, } from '@sim/deployment-config/env-capabilities' +import { toRecordOrNull } from '@sim/utils/object' import { isPlaceholder, parseEnv, ROOT, SHARED_KEYS } from './env-files' export type ConfigurationSourceKind = 'dev' | 'compose' | 'helm' @@ -129,12 +130,6 @@ function runSafely( } } -function asRecord(value: unknown): Record | null { - return typeof value === 'object' && value !== null && !Array.isArray(value) - ? (value as Record) - : null -} - function parseJson(value: string): unknown | null { try { return JSON.parse(value) as unknown @@ -299,12 +294,12 @@ export function parseDockerInspectEnvironment(output: string): Map }> = [] for (const item of parsed) { - const record = asRecord(item) - const config = asRecord(record?.Config) + const record = toRecordOrNull(item) + const config = toRecordOrNull(record?.Config) if (!config || !Array.isArray(config.Env)) continue const values = parseEnvironmentEntries(config.Env) if (!values) continue - const state = asRecord(record?.State) + const state = toRecordOrNull(record?.State) candidates.push({ running: state?.Running === true, created: typeof record?.Created === 'string' ? record.Created : '', @@ -324,7 +319,7 @@ function parseComposeProjects(output: string): ComposeProject[] | null { if (!Array.isArray(parsed)) return null const projects: ComposeProject[] = [] for (const item of parsed) { - const record = asRecord(item) + const record = toRecordOrNull(item) const name = record?.Name const configFiles = record?.ConfigFiles if (typeof name !== 'string' || typeof configFiles !== 'string') return null @@ -340,13 +335,13 @@ function parseComposeProjects(output: string): ComposeProject[] | null { /** Reads `docker compose config --format json` and returns the resolved app environment. */ export function parseComposeConfigEnvironment(output: string): Map | null { - const parsed = asRecord(parseJson(output)) - const services = asRecord(parsed?.services) - const app = asRecord(services?.simstudio) + const parsed = toRecordOrNull(parseJson(output)) + const services = toRecordOrNull(parsed?.services) + const app = toRecordOrNull(services?.simstudio) if (!app) return null if (Array.isArray(app.environment)) return parseEnvironmentEntries(app.environment) - const environment = asRecord(app.environment) + const environment = toRecordOrNull(app.environment) if (!environment) return null const values = new Map() for (const [key, value] of Object.entries(environment)) { @@ -704,10 +699,10 @@ function resourceEnvironment( kind: KubernetesResourceKind, resource: unknown ): EnvironmentResolution { - const record = asRecord(resource) + const record = toRecordOrNull(resource) if (!record) return { values: null, warning: 'Kubernetes returned an invalid resource.' } const values = new Map() - const data = record.data === undefined ? {} : asRecord(record.data) + const data = record.data === undefined ? {} : toRecordOrNull(record.data) if (!data) return { values: null, warning: 'Kubernetes returned invalid resource data.' } for (const [key, value] of Object.entries(data)) { if (typeof value !== 'string') { @@ -725,7 +720,7 @@ function resourceEnvironment( } const secondaryKey = kind === 'secret' ? 'stringData' : 'binaryData' - const secondary = record[secondaryKey] === undefined ? {} : asRecord(record[secondaryKey]) + const secondary = record[secondaryKey] === undefined ? {} : toRecordOrNull(record[secondaryKey]) if (!secondary) return { values: null, warning: 'Kubernetes returned invalid resource data.' } for (const [key, value] of Object.entries(secondary)) { if (typeof value !== 'string') { @@ -783,7 +778,7 @@ export function resolveKubernetesContainerEnvironment( container: unknown, lookup: KubernetesResourceLookup ): EnvironmentResolution { - const record = asRecord(container) + const record = toRecordOrNull(container) if (!record) return { values: null, warning: 'The app container definition is invalid.' } const values = new Map() const envFrom = record.envFrom === undefined ? [] : record.envFrom @@ -792,11 +787,11 @@ export function resolveKubernetesContainerEnvironment( } for (const source of envFrom) { - const sourceRecord = asRecord(source) + const sourceRecord = toRecordOrNull(source) if (!sourceRecord) return { values: null, warning: 'The app container envFrom entry is invalid.' } - const secretRef = asRecord(sourceRecord.secretRef) - const configMapRef = asRecord(sourceRecord.configMapRef) + const secretRef = toRecordOrNull(sourceRecord.secretRef) + const configMapRef = toRecordOrNull(sourceRecord.configMapRef) const kind: KubernetesResourceKind | null = secretRef ? 'secret' : configMapRef @@ -820,7 +815,7 @@ export function resolveKubernetesContainerEnvironment( return { values: null, warning: 'The app container env definition is invalid.' } } for (const entry of explicitEnv) { - const env = asRecord(entry) + const env = toRecordOrNull(entry) if (!env || typeof env.name !== 'string' || env.name === '') { return { values: null, warning: 'The app container has an invalid explicit env entry.' } } @@ -836,9 +831,9 @@ export function resolveKubernetesContainerEnvironment( continue } - const valueFrom = asRecord(env.valueFrom) - const secretKeyRef = asRecord(valueFrom?.secretKeyRef) - const configMapKeyRef = asRecord(valueFrom?.configMapKeyRef) + const valueFrom = toRecordOrNull(env.valueFrom) + const secretKeyRef = toRecordOrNull(valueFrom?.secretKeyRef) + const configMapKeyRef = toRecordOrNull(valueFrom?.configMapKeyRef) const kind: KubernetesResourceKind | null = secretKeyRef ? 'secret' : configMapKeyRef @@ -877,7 +872,7 @@ function parseHelmReleases(output: string): HelmRelease[] | null { if (!Array.isArray(parsed)) return null const releases: HelmRelease[] = [] for (const item of parsed) { - const record = asRecord(item) + const record = toRecordOrNull(item) if ( typeof record?.name !== 'string' || typeof record.namespace !== 'string' || @@ -959,7 +954,7 @@ function discoverHelmReleases( ) continue } - const deploymentList = asRecord(parseJson(deploymentResult.stdout)) + const deploymentList = toRecordOrNull(parseJson(deploymentResult.stdout)) const items = deploymentList?.items const itemCount = Array.isArray(items) ? items.length : null if (!Array.isArray(items) || items.length !== 1) { @@ -974,10 +969,10 @@ function discoverHelmReleases( ) continue } - const deployment = asRecord(items[0]) - const spec = asRecord(deployment?.spec) - const template = asRecord(spec?.template) - const podSpec = asRecord(template?.spec) + const deployment = toRecordOrNull(items[0]) + const spec = toRecordOrNull(deployment?.spec) + const template = toRecordOrNull(spec?.template) + const podSpec = toRecordOrNull(template?.spec) const containers = podSpec?.containers if (!Array.isArray(containers)) { sources.push( @@ -985,7 +980,7 @@ function discoverHelmReleases( ) continue } - const appContainer = containers.find((container) => asRecord(container)?.name === 'app') + const appContainer = containers.find((container) => toRecordOrNull(container)?.name === 'app') if (!appContainer) { sources.push( helmUnknown(release, context, 'The app Deployment has no container named "app".') From 4c0ee74c4bb130c5a68e78bbb561d08a716e3c30 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Sat, 19 Sep 2026 18:54:20 -0700 Subject: [PATCH 2/2] improvement(utils): route the remaining record-coercion helpers through toRecord MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The first pass matched one operand order, so six exact equivalents written `x !== null && typeof x === 'object'` survived — two of them beside a sibling the pass had already deleted. Domain-named wrappers keep their names and delegate, matching the microsoft-teams client that already did. --- apps/sim/lib/internal/confluence/client.ts | 3 ++- apps/sim/lib/internal/google-drive/client.ts | 5 ++--- .../lib/internal/google-drive/operations.test.ts | 4 ++-- apps/sim/lib/internal/jira/operations.ts | 5 ++--- .../lib/internal/microsoft-dataverse/client.ts | 2 +- apps/sim/lib/internal/onepassword/client.ts | 15 +++++---------- apps/sim/lib/internal/servicenow/client.ts | 2 +- apps/sim/lib/internal/slack/operations.ts | 5 ++--- apps/sim/lib/webhooks/slack-custom-ingress.ts | 8 ++------ apps/sim/tools/bitbucket/get_merge_task_status.ts | 15 +++++---------- apps/sim/tools/clickup/get_running_timer.ts | 2 +- apps/sim/tools/clickup/start_timer.ts | 2 +- apps/sim/tools/clickup/stop_timer.ts | 2 +- 13 files changed, 27 insertions(+), 43 deletions(-) diff --git a/apps/sim/lib/internal/confluence/client.ts b/apps/sim/lib/internal/confluence/client.ts index 603110ba36c..54670de71aa 100644 --- a/apps/sim/lib/internal/confluence/client.ts +++ b/apps/sim/lib/internal/confluence/client.ts @@ -1,3 +1,4 @@ +import { toRecord } from '@sim/utils/object' import { validateJiraCloudId } from '@/lib/core/security/input-validation' import { MAX_JSON_API_RESPONSE_BYTES } from '@/lib/core/security/input-validation.server' import { @@ -17,7 +18,7 @@ export interface ConfluenceConnectionConfig { export type JsonObject = Record export function asObject(value: unknown): JsonObject { - return value && typeof value === 'object' && !Array.isArray(value) ? (value as JsonObject) : {} + return toRecord(value) } export function asArray(value: unknown): unknown[] { diff --git a/apps/sim/lib/internal/google-drive/client.ts b/apps/sim/lib/internal/google-drive/client.ts index b7c51619d97..34b2c379b85 100644 --- a/apps/sim/lib/internal/google-drive/client.ts +++ b/apps/sim/lib/internal/google-drive/client.ts @@ -1,3 +1,4 @@ +import { toRecord } from '@sim/utils/object' import { MAX_JSON_API_RESPONSE_BYTES, type SecureFetchResponse, @@ -54,9 +55,7 @@ export async function requestGoogleDrive( export type JsonObject = Record export function asObject(value: unknown): JsonObject { - return value !== null && typeof value === 'object' && !Array.isArray(value) - ? (value as JsonObject) - : {} + return toRecord(value) } export async function responseObject(response: SecureFetchResponse): Promise { diff --git a/apps/sim/lib/internal/google-drive/operations.test.ts b/apps/sim/lib/internal/google-drive/operations.test.ts index aaab39e66fb..f93cf86bdce 100644 --- a/apps/sim/lib/internal/google-drive/operations.test.ts +++ b/apps/sim/lib/internal/google-drive/operations.test.ts @@ -9,8 +9,7 @@ const mocks = vi.hoisted(() => ({ })) vi.mock('@/lib/internal/google-drive/client', () => ({ - asObject: (value: unknown) => - value !== null && typeof value === 'object' && !Array.isArray(value) ? value : {}, + asObject: (value: unknown) => toRecord(value), googleApiErrorMessage: (data: { error?: { message?: string } }, fallback: string) => data.error?.message || fallback, requestGoogleDrive: mocks.request, @@ -21,6 +20,7 @@ vi.mock('@/lib/internal/google-drive/file-input', () => ({ resolveGoogleDriveUploadFile: mocks.resolveFile, })) +import { toRecord } from '@sim/utils/object' import { executeGoogleDriveDownload, executeGoogleDriveExport, diff --git a/apps/sim/lib/internal/jira/operations.ts b/apps/sim/lib/internal/jira/operations.ts index f776d998f15..d88039ecfa2 100644 --- a/apps/sim/lib/internal/jira/operations.ts +++ b/apps/sim/lib/internal/jira/operations.ts @@ -1,5 +1,6 @@ import type { Logger } from '@sim/logger' import { createLogger } from '@sim/logger' +import { toRecord } from '@sim/utils/object' import { validateAlphanumericId, validateJiraIssueKey } from '@/lib/core/security/input-validation' import { createJiraClient, type JiraClient } from '@/lib/internal/jira/client' import { JiraOperationError } from '@/lib/internal/jira/errors' @@ -26,9 +27,7 @@ export interface JiraOperationContext { type JsonObject = Record function asObject(value: unknown): JsonObject { - return value !== null && typeof value === 'object' && !Array.isArray(value) - ? (value as JsonObject) - : {} + return toRecord(value) } function parseObject(text: string): JsonObject { diff --git a/apps/sim/lib/internal/microsoft-dataverse/client.ts b/apps/sim/lib/internal/microsoft-dataverse/client.ts index b97ea8fcbb4..06e65d400b3 100644 --- a/apps/sim/lib/internal/microsoft-dataverse/client.ts +++ b/apps/sim/lib/internal/microsoft-dataverse/client.ts @@ -41,7 +41,7 @@ export async function uploadDataverseFile( return } const data = await response.json().catch(() => null) - const error = isRecordLike(data) && toRecordOrNull(data.error) + const error = isRecordLike(data) ? toRecordOrNull(data.error) : null const message = error && typeof error.message === 'string' ? error.message diff --git a/apps/sim/lib/internal/onepassword/client.ts b/apps/sim/lib/internal/onepassword/client.ts index 1903bc34eec..382d7e8a75a 100644 --- a/apps/sim/lib/internal/onepassword/client.ts +++ b/apps/sim/lib/internal/onepassword/client.ts @@ -11,6 +11,7 @@ import type { Website, } from '@1password/sdk' import { generateId } from '@sim/utils/id' +import { toRecord } from '@sim/utils/object' import { MAX_JSON_API_RESPONSE_BYTES, type SecureFetchResponse, @@ -449,12 +450,6 @@ export function findItemFileAttributes(item: Item, fileId: string): FileAttribut * category enum strings vs Connect's SCREAMING_SNAKE_CASE) and silently no-ops or * corrupts the write otherwise. */ -function objectValue(value: unknown): Record { - return value !== null && typeof value === 'object' && !Array.isArray(value) - ? (value as Record) - : {} -} - function optionalString(value: unknown): string | undefined { return typeof value === 'string' ? value : undefined } @@ -465,8 +460,8 @@ export function connectItemToSdkItem(connectItem: Record, exist const fields = Array.isArray(connectItem.fields) ? connectItem.fields.map((value) => { - const field = objectValue(value) - const section = objectValue(field.section) + const field = toRecord(value) + const section = toRecord(field.section) const id = optionalString(field.id) return { /** Preserve SDK-only metadata on fields that already existed. */ @@ -482,7 +477,7 @@ export function connectItemToSdkItem(connectItem: Record, exist const sections = Array.isArray(connectItem.sections) ? connectItem.sections.map((value) => { - const section = objectValue(value) + const section = toRecord(value) const id = optionalString(section.id) return { ...(id ? existingSectionsById.get(id) : undefined), @@ -495,7 +490,7 @@ export function connectItemToSdkItem(connectItem: Record, exist const websitesValue = connectItem.urls ?? connectItem.websites const websites = Array.isArray(websitesValue) ? websitesValue.map((value) => { - const website = objectValue(value) + const website = toRecord(value) return { url: optionalString(website.href) || optionalString(website.url) || '', label: optionalString(website.label) || '', diff --git a/apps/sim/lib/internal/servicenow/client.ts b/apps/sim/lib/internal/servicenow/client.ts index 865b24b83e5..7e5c45bb44e 100644 --- a/apps/sim/lib/internal/servicenow/client.ts +++ b/apps/sim/lib/internal/servicenow/client.ts @@ -45,7 +45,7 @@ export async function uploadServiceNowAttachment( ) const data = await response.json().catch(() => null) if (!response.ok) { - const error = isRecordLike(data) && toRecordOrNull(data.error) + const error = isRecordLike(data) ? toRecordOrNull(data.error) : null const message = error && typeof error.message === 'string' ? error.message diff --git a/apps/sim/lib/internal/slack/operations.ts b/apps/sim/lib/internal/slack/operations.ts index c2d2512330b..8328b54301d 100644 --- a/apps/sim/lib/internal/slack/operations.ts +++ b/apps/sim/lib/internal/slack/operations.ts @@ -1,4 +1,5 @@ import { createLogger } from '@sim/logger' +import { toRecord } from '@sim/utils/object' import type { SlackDeleteMessageBody, SlackDownloadBody, @@ -49,9 +50,7 @@ function providerError(data: SlackJsonObject, status: number, fallback: string): } function record(value: unknown): SlackJsonObject { - return typeof value === 'object' && value !== null && !Array.isArray(value) - ? (value as SlackJsonObject) - : {} + return toRecord(value) } function mapReaction(value: unknown) { diff --git a/apps/sim/lib/webhooks/slack-custom-ingress.ts b/apps/sim/lib/webhooks/slack-custom-ingress.ts index 48878f0206a..31105ca45a7 100644 --- a/apps/sim/lib/webhooks/slack-custom-ingress.ts +++ b/apps/sim/lib/webhooks/slack-custom-ingress.ts @@ -1,4 +1,5 @@ import { createLogger } from '@sim/logger' +import { toRecord } from '@sim/utils/object' import type { NextRequest } from 'next/server' import { NextResponse } from 'next/server' import { cancelWorkflowExecution } from '@/lib/execution/cancel-workflow-execution' @@ -46,12 +47,7 @@ interface DispatchSlackCustomBotOptions { export function getLegacySlackCustomBotCredentialId( foundWebhook: LegacySlackPathWebhook ): string | null { - const providerConfig = - foundWebhook.providerConfig !== null && - typeof foundWebhook.providerConfig === 'object' && - !Array.isArray(foundWebhook.providerConfig) - ? (foundWebhook.providerConfig as Record) - : {} + const providerConfig = toRecord(foundWebhook.providerConfig) if (providerConfig.ingressMode !== LEGACY_SLACK_CUSTOM_BOT_INGRESS_MODE) { return null diff --git a/apps/sim/tools/bitbucket/get_merge_task_status.ts b/apps/sim/tools/bitbucket/get_merge_task_status.ts index 46e060376b7..04a08609fda 100644 --- a/apps/sim/tools/bitbucket/get_merge_task_status.ts +++ b/apps/sim/tools/bitbucket/get_merge_task_status.ts @@ -1,3 +1,4 @@ +import { toRecordOrNull } from '@sim/utils/object' import { BITBUCKET_PULL_REQUEST_OUTPUT_PROPERTIES, type BitbucketGetMergeTaskStatusParams, @@ -23,12 +24,6 @@ interface BitbucketMergeTaskOutput { mergeResult: BitbucketPullRequest | null } -function record(value: unknown): Record | null { - return typeof value === 'object' && value !== null && !Array.isArray(value) - ? (value as Record) - : null -} - function stringField(value: unknown): string | null { return typeof value === 'string' ? value : null } @@ -61,7 +56,7 @@ export const bitbucketGetMergeTaskStatusTool: ToolConfig< transformResponse: async (response) => { const data = await bitbucketJson(response) if (data.type === 'error') { - const error = record(data.error) + const error = toRecordOrNull(data.error) const message = stringField(error?.message)?.trim() if (!message) throw new Error('Bitbucket returned a malformed merge task error') const detail = stringField(error?.detail)?.trim() @@ -73,11 +68,11 @@ export const bitbucketGetMergeTaskStatusTool: ToolConfig< throw new Error('Bitbucket merge task status must be PENDING or SUCCESS') } - const links = record(data.links) - const self = record(links?.self) + const links = toRecordOrNull(data.links) + const self = toRecordOrNull(links?.self) let mergeResult: BitbucketPullRequest | null = null if (taskStatus === 'SUCCESS') { - const result = record(data.merge_result) + const result = toRecordOrNull(data.merge_result) if (!result) throw new Error('Bitbucket successful merge task omitted merge_result') mergeResult = normalizeBitbucketPullRequest(result) } diff --git a/apps/sim/tools/clickup/get_running_timer.ts b/apps/sim/tools/clickup/get_running_timer.ts index dee4d7c68ce..936782cf592 100644 --- a/apps/sim/tools/clickup/get_running_timer.ts +++ b/apps/sim/tools/clickup/get_running_timer.ts @@ -68,7 +68,7 @@ export const clickupGetRunningTimerTool: ToolConfig< return { success: false, output: { error }, error } } - const entry = isRecordLike(data) && toRecordOrNull(data.data) + const entry = isRecordLike(data) ? toRecordOrNull(data.data) : null return { success: true, diff --git a/apps/sim/tools/clickup/start_timer.ts b/apps/sim/tools/clickup/start_timer.ts index d83013e580e..d75c8befbe1 100644 --- a/apps/sim/tools/clickup/start_timer.ts +++ b/apps/sim/tools/clickup/start_timer.ts @@ -92,7 +92,7 @@ export const clickupStartTimerTool: ToolConfig