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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .claude/rules/global.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ Use shared helpers from `@sim/utils` instead of writing inline implementations:
- `omit(obj, keys)` from `@sim/utils/object` — remove keys from object
- `filterUndefined(obj)` from `@sim/utils/object` — strip undefined-valued keys. Never write `Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== undefined))`
- `isRecordLike(value)` from `@sim/utils/object` — indexable-object guard. Never redeclare `typeof value === 'object' && value !== null && !Array.isArray(value)`
- `toRecord(value)` / `toRecordOrNull(value)` / `toArray(value)` from `@sim/utils/object` — coerce an untyped payload value to a record or array. Never inline `isRecordLike(v) ? v : {}` or `Array.isArray(v) ? v : []`. Where the source is already typed, keep the inline `Array.isArray` check: it narrows, while `toArray` asserts
- `toStringOrNull(value)` / `toNumberOrNull(value)` / `toBooleanOrNull(value)` from `@sim/utils/coerce` — read one scalar out of an untyped payload. Never declare a local one-liner that is byte-identical to one of these (`asString`, `getString`, `nullableString`, …). Keep a local helper that differs: one returning `undefined` rather than `null` changes the wire shape, and one adding `Number.isFinite` or a string parse is a stricter check these deliberately omit
- `truncate(str, maxLength, suffix?)` from `@sim/utils/string` — safe string truncation with ellipsis
- `escapeRegExp(value)` from `@sim/utils/string` — escape regex metacharacters. Never inline `replace(/[.*+?^${}()|[\]\\]/g, '\\$&')`
- `compareStrings(left, right)` from `@sim/utils/string` — code-unit string comparator for hashes, fingerprints, and values compared across processes. Never `localeCompare` there
Expand Down
2 changes: 2 additions & 0 deletions .cursor/rules/global.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ Use shared helpers from `@sim/utils` instead of writing inline implementations:
- `omit(obj, keys)` from `@sim/utils/object` — remove keys from object
- `filterUndefined(obj)` from `@sim/utils/object` — strip undefined-valued keys. Never write `Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== undefined))`
- `isRecordLike(value)` from `@sim/utils/object` — indexable-object guard. Never redeclare `typeof value === 'object' && value !== null && !Array.isArray(value)`
- `toRecord(value)` / `toRecordOrNull(value)` / `toArray(value)` from `@sim/utils/object` — coerce an untyped payload value to a record or array. Never inline `isRecordLike(v) ? v : {}` or `Array.isArray(v) ? v : []`. Where the source is already typed, keep the inline `Array.isArray` check: it narrows, while `toArray` asserts
- `toStringOrNull(value)` / `toNumberOrNull(value)` / `toBooleanOrNull(value)` from `@sim/utils/coerce` — read one scalar out of an untyped payload. Never declare a local one-liner that is byte-identical to one of these (`asString`, `getString`, `nullableString`, …). Keep a local helper that differs: one returning `undefined` rather than `null` changes the wire shape, and one adding `Number.isFinite` or a string parse is a stricter check these deliberately omit
- `truncate(str, maxLength, suffix?)` from `@sim/utils/string` — safe string truncation with ellipsis
- `escapeRegExp(value)` from `@sim/utils/string` — escape regex metacharacters. Never inline `replace(/[.*+?^${}()|[\]\\]/g, '\\$&')`
- `compareStrings(left, right)` from `@sim/utils/string` — code-unit string comparator for hashes, fingerprints, and values compared across processes. Never `localeCompare` there
Expand Down
2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ You are a professional software engineer. All code must follow best practices: a
- `structuredClone(value)` — built-in deep clone; never `JSON.parse(JSON.stringify(...))`
- `omit(obj, keys)` / `filterUndefined(obj)` from `@sim/utils/object` — object trimming; never `Object.fromEntries(Object.entries(...).filter(...))`
- `isRecordLike(value)` from `@sim/utils/object` — never redeclare `typeof value === 'object' && value !== null && !Array.isArray(value)`
- `toRecord(value)` / `toRecordOrNull(value)` / `toArray(value)` from `@sim/utils/object` — coerce an untyped payload value to a record or array; never inline `isRecordLike(v) ? v : {}` or `Array.isArray(v) ? v : []`. Where the source is already typed, keep the inline `Array.isArray` check: it narrows, while `toArray` asserts
- `toStringOrNull(value)` / `toNumberOrNull(value)` / `toBooleanOrNull(value)` from `@sim/utils/coerce` — read one scalar out of an untyped payload; never declare a local one-liner byte-identical to one of these. Keep a local helper that differs: `undefined` instead of `null` changes the wire shape, and a `Number.isFinite` or string-parse variant is a stricter check these omit
- `truncate(str, maxLength, suffix?)` from `@sim/utils/string` — never inline slice + ellipsis
- `escapeRegExp(value)` from `@sim/utils/string` — never inline `replace(/[.*+?^${}()|[\]\\]/g, '\\$&')`
- `compareStrings(left, right)` from `@sim/utils/string` — code-unit ordering for hashes, fingerprints, and cross-process comparisons; never `localeCompare` there
Expand Down
18 changes: 7 additions & 11 deletions apps/sim/app/api/v2/tables/utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { toStringOrNull } from '@sim/utils/coerce'
import type {
V2ApiTable,
V2EnrichmentProviderOutcome,
Expand Down Expand Up @@ -238,11 +239,6 @@ function storedNumber(value: unknown): number {
return typeof value === 'number' && Number.isFinite(value) ? value : 0
}

/** Reads a stored field that the published shape declares as a nullable string. */
function storedNullableString(value: unknown): string | null {
return typeof value === 'string' ? value : null
}

/**
* Reads a stored timestamp, keeping only a value the published `date-time`
* format will accept. A Postgres literal or a half-written blob becomes `null`
Expand All @@ -257,13 +253,13 @@ function storedTimestamp(value: unknown): string | null {
function toApiEnrichmentProvider(value: unknown): V2EnrichmentProviderOutcome {
const provider = (value ?? {}) as Record<string, unknown>
return {
id: storedNullableString(provider.id) ?? '',
label: storedNullableString(provider.label) ?? '',
toolId: storedNullableString(provider.toolId) ?? '',
status: storedNullableString(provider.status) ?? 'not_run',
id: toStringOrNull(provider.id) ?? '',
label: toStringOrNull(provider.label) ?? '',
toolId: toStringOrNull(provider.toolId) ?? '',
status: toStringOrNull(provider.status) ?? 'not_run',
cost: storedNumber(provider.cost),
durationMs: storedNumber(provider.durationMs),
error: storedNullableString(provider.error),
error: toStringOrNull(provider.error),
}
}

Expand All @@ -287,7 +283,7 @@ export function toApiEnrichmentDetail(
completedAt: storedTimestamp(stored.completedAt),
durationMs: storedNumber(stored.durationMs),
totalCost: storedNumber(stored.totalCost),
matchedProvider: storedNullableString(stored.matchedProvider),
matchedProvider: toStringOrNull(stored.matchedProvider),
aborted: stored.aborted === true,
providers: Array.isArray(stored.providers) ? stored.providers.map(toApiEnrichmentProvider) : [],
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { toArray } from '@sim/utils/object'
/**
* Extracts the raw value from a preview context entry.
*
Expand Down Expand Up @@ -41,5 +42,5 @@ export function parseJsonArrayValue<T>(value: unknown): T[] {
return []
}
}
return Array.isArray(parsed) ? (parsed as T[]) : []
return toArray<T>(parsed)
}
3 changes: 2 additions & 1 deletion apps/sim/connectors/grain/grain.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { toArray } from '@sim/utils/object'
import { fetchWithRetry } from '@/lib/knowledge/documents/secure-fetch.server'
import { VALIDATE_RETRY_OPTIONS } from '@/lib/knowledge/documents/utils'
import { grainConnectorMeta } from '@/connectors/grain/meta'
Expand Down Expand Up @@ -328,7 +329,7 @@ async function fetchTranscript(
}

const data = await response.json()
return Array.isArray(data) ? (data as GrainTranscriptSegment[]) : []
return toArray<GrainTranscriptSegment>(data)
}

export const grainConnector: ConnectorConfig = {
Expand Down
3 changes: 2 additions & 1 deletion apps/sim/lib/internal/asana/client.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { toArray } from '@sim/utils/object'
import { readResponseTextWithLimit } from '@/lib/core/utils/stream-limits'
import { AsanaOperationError } from '@/lib/internal/asana/errors'

Expand All @@ -13,7 +14,7 @@ export function asObject(value: unknown): AsanaJsonObject {
}

export function asArray(value: unknown): unknown[] {
return Array.isArray(value) ? value : []
return toArray(value)
}

function providerErrorMessage(response: Response, text: string): string {
Expand Down
8 changes: 4 additions & 4 deletions apps/sim/lib/internal/cbinsights/operations/chat.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { toStringOrNull } from '@sim/utils/coerce'
import type { InternalToolOperationImplementation } from '@/lib/internal/tool-operations/types'
import type { CbInsightsChatParams } from '@/tools/cbinsights/chat'
import {
asArray,
asString,
asStringArray,
cbInsightsRequest,
compactBody,
Expand All @@ -29,9 +29,9 @@ export const executeCbinsightsChatOperation: InternalToolOperationImplementation
body: compactBody({ message, chatID: parseOptionalStringParam(params.chatId, 'chatId') }),
},
(data) => ({
chatId: asString(data.chatID),
title: asString(data.title),
message: asString(data.message),
chatId: toStringOrNull(data.chatID),
title: toStringOrNull(data.title),
message: toStringOrNull(data.message),
sources: asArray(data.sources),
relatedContent: asArray(data.relatedContent),
suggestions: asStringArray(data.suggestions),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { toStringOrNull } from '@sim/utils/coerce'
import type { InternalToolOperationImplementation } from '@/lib/internal/tool-operations/types'
import type { CbInsightsExitProbabilityHistoryParams } from '@/tools/cbinsights/get_exit_probability_history'
import {
asArray,
asString,
cbInsightsRequest,
compactBody,
parseOptionalStringParam,
Expand All @@ -25,7 +25,7 @@ export const executeCbinsightsGetExitProbabilityHistoryOperation: InternalToolOp
(data) => ({
ipo: asArray(data.ipo),
mna: asArray(data.mna),
incompleteRoundType: asString(data.incompleteRoundType),
incompleteRoundType: toStringOrNull(data.incompleteRoundType),
}),
signal
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { toNumberOrNull, toStringOrNull } from '@sim/utils/coerce'
import { toRecordOrNull } from '@sim/utils/object'
import type { InternalToolOperationImplementation } from '@/lib/internal/tool-operations/types'
import type { CbInsightsOrgParams } from '@/tools/cbinsights/types'
import { asNumber, asString, cbInsightsRequest, requireOrgId } from '@/tools/cbinsights/utils'
import { cbInsightsRequest, requireOrgId } from '@/tools/cbinsights/utils'

export const executeCbinsightsGetOrgFundingWindowOperation: InternalToolOperationImplementation<
CbInsightsOrgParams
Expand All @@ -17,9 +18,9 @@ export const executeCbinsightsGetOrgFundingWindowOperation: InternalToolOperatio
params,
{ path: `/v2/organizations/${orgId}/fundingwindow` },
(data) => ({
windowStart: asString(data.windowStart),
windowEnd: asString(data.windowEnd),
cohortNextRoundRate: asNumber(data.cohortNextRoundRate),
windowStart: toStringOrNull(data.windowStart),
windowEnd: toStringOrNull(data.windowEnd),
cohortNextRoundRate: toNumberOrNull(data.cohortNextRoundRate),
cohortCriteria: toRecordOrNull(data.cohortCriteria),
latestFunding: toRecordOrNull(data.latestFunding),
}),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { toNumberOrNull } from '@sim/utils/coerce'
import type { InternalToolOperationImplementation } from '@/lib/internal/tool-operations/types'
import type { CbInsightsOrgManagementParams } from '@/tools/cbinsights/get_org_management_and_board'
import {
asArray,
asNumber,
cbInsightsRequest,
compactBody,
parseIdListParam,
Expand All @@ -21,7 +21,7 @@ export const executeCbinsightsGetOrgManagementAndBoardOperation: InternalToolOpe
},
(data) => ({
people: asArray(data.people),
mosaicManagement: asNumber(data.mosaicManagement),
mosaicManagement: toNumberOrNull(data.mosaicManagement),
Comment thread
waleedlatif1 marked this conversation as resolved.
}),
signal
)
Expand Down
15 changes: 5 additions & 10 deletions apps/sim/lib/internal/cbinsights/operations/get-org-revenue.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
import { toNumberOrNull, toStringOrNull } from '@sim/utils/coerce'
import type { InternalToolOperationImplementation } from '@/lib/internal/tool-operations/types'
import type { CbInsightsOrgParams } from '@/tools/cbinsights/types'
import {
asArray,
asNumber,
asString,
cbInsightsRequest,
requireOrgId,
} from '@/tools/cbinsights/utils'
import { asArray, cbInsightsRequest, requireOrgId } from '@/tools/cbinsights/utils'

export const executeCbinsightsGetOrgRevenueOperation: InternalToolOperationImplementation<
CbInsightsOrgParams
Expand All @@ -21,9 +16,9 @@ export const executeCbinsightsGetOrgRevenueOperation: InternalToolOperationImple
params,
{ path: `/v2/organizations/${orgId}/revenuebyyear` },
(data) => ({
orgId: asNumber(data.orgId),
orgName: asString(data.orgName),
orgUrl: asString(data.orgUrl),
orgId: toNumberOrNull(data.orgId),
orgName: toStringOrNull(data.orgName),
orgUrl: toStringOrNull(data.orgUrl),
revenue: asArray(data.revenue),
}),
signal
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { toStringOrNull } from '@sim/utils/coerce'
import { toRecordOrNull } from '@sim/utils/object'
import type { InternalToolOperationImplementation } from '@/lib/internal/tool-operations/types'
import type { CbInsightsOrgParams } from '@/tools/cbinsights/types'
import {
asString,
cbInsightsRequest,
requireOrgId,
SCOUTING_REPORT_TIMEOUT_MS,
Expand All @@ -24,8 +24,8 @@ export const executeCbinsightsGetScoutingReportOperation: InternalToolOperationI
},
(data) => ({
orgInfo: toRecordOrNull(data.orgInfo),
reportMarkdown: asString(data.reportMarkdown),
reportJson: asString(data.reportJson),
reportMarkdown: toStringOrNull(data.reportMarkdown),
reportJson: toStringOrNull(data.reportJson),
}),
signal
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { toStringOrNull } from '@sim/utils/coerce'
import type { InternalToolOperationImplementation } from '@/lib/internal/tool-operations/types'
import type { CbInsightsOrgParams } from '@/tools/cbinsights/types'
import { asArray, asString, cbInsightsRequest, requireOrgId } from '@/tools/cbinsights/utils'
import { asArray, cbInsightsRequest, requireOrgId } from '@/tools/cbinsights/utils'

export const executeCbinsightsGetStrategyMapOperation: InternalToolOperationImplementation<
CbInsightsOrgParams
Expand All @@ -10,8 +11,8 @@ export const executeCbinsightsGetStrategyMapOperation: InternalToolOperationImpl
params,
{ path: `/v2/organizations/${orgId}/strategymap` },
(data) => ({
orgName: asString(data.orgName),
logoUrl: asString(data.logoUrl),
orgName: toStringOrNull(data.orgName),
logoUrl: toStringOrNull(data.logoUrl),
categories: asArray(data.categories),
}),
signal
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { toStringOrNull } from '@sim/utils/coerce'
import type { InternalToolOperationImplementation } from '@/lib/internal/tool-operations/types'
import type { CbInsightsListBusinessRelationshipsParams } from '@/tools/cbinsights/list_business_relationships'
import {
asArray,
asString,
cbInsightsRequest,
compactBody,
parseOptionalStringParam,
Expand All @@ -21,6 +21,6 @@ export const executeCbinsightsListBusinessRelationshipsOperation: InternalToolOp
nextPageToken: parseOptionalStringParam(params.nextPageToken, 'nextPageToken'),
}),
},
(data) => ({ orgs: asArray(data.orgs), nextPageToken: asString(data.nextPageToken) }),
(data) => ({ orgs: asArray(data.orgs), nextPageToken: toStringOrNull(data.nextPageToken) }),
signal
)
4 changes: 2 additions & 2 deletions apps/sim/lib/internal/cbinsights/operations/rag.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { toStringOrNull } from '@sim/utils/coerce'
import type { InternalToolOperationImplementation } from '@/lib/internal/tool-operations/types'
import type { CbInsightsRagParams } from '@/tools/cbinsights/rag'
import {
asString,
asStringArray,
cbInsightsRequest,
parseOptionalStringParam,
Expand All @@ -19,7 +19,7 @@ export const executeCbinsightsRagOperation: InternalToolOperationImplementation<
return cbInsightsRequest<{ data?: unknown; guidance?: unknown }>(
params,
{ path: '/v2/cbirag', body: { message } },
(data) => ({ data: asString(data.data), guidance: asStringArray(data.guidance) }),
(data) => ({ data: toStringOrNull(data.data), guidance: asStringArray(data.guidance) }),
signal
)
}
4 changes: 2 additions & 2 deletions apps/sim/lib/internal/confluence/client.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { toRecord } from '@sim/utils/object'
import { toArray, 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 {
Expand All @@ -22,7 +22,7 @@ export function asObject(value: unknown): JsonObject {
}

export function asArray(value: unknown): unknown[] {
return Array.isArray(value) ? value : []
return toArray(value)
}

export function nested(object: JsonObject, ...keys: string[]): unknown {
Expand Down
Loading
Loading