Skip to content

Commit 69ad368

Browse files
committed
refactor(sim): consolidate record guards and normalize helpers onto @sim/utils
Replace ~55 re-implemented loose record guards with the canonical @sim/utils isRecordLike (and one strict site with isPlainRecord), and dedupe three normalize clusters: sortObjectKeysDeep (sanitization + copilot builders), normalizeToken (salesforce + servicenow triggers), and normalizeEmail. Array-allowing guards and domain-specific normalizers are intentionally left untouched. Pure refactor — identical predicates and transforms, no behavior change.
1 parent 94a271b commit 69ad368

66 files changed

Lines changed: 308 additions & 474 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/sim/app/(auth)/verify/use-verification.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import { useEffect, useState } from 'react'
44
import { createLogger } from '@sim/logger'
5+
import { normalizeEmail } from '@sim/utils'
56
import { useRouter, useSearchParams } from 'next/navigation'
67
import { client, useSession } from '@/lib/auth/auth-client'
78
import { validateCallbackUrl } from '@/lib/core/security/input-validation'
@@ -100,7 +101,7 @@ export function useVerification({
100101
setErrorMessage('')
101102

102103
try {
103-
const normalizedEmail = email.trim().toLowerCase()
104+
const normalizedEmail = normalizeEmail(email)
104105
const response = await client.emailOtp.verifyEmail({
105106
email: normalizedEmail,
106107
otp,
@@ -173,7 +174,7 @@ export function useVerification({
173174
setIsLoading(true)
174175
setErrorMessage('')
175176

176-
const normalizedEmail = email.trim().toLowerCase()
177+
const normalizedEmail = normalizeEmail(email)
177178
client.emailOtp
178179
.sendVerificationOtp({
179180
email: normalizedEmail,

apps/sim/app/api/credential-sets/invite/[token]/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
organization,
88
} from '@sim/db/schema'
99
import { createLogger } from '@sim/logger'
10+
import { normalizeEmail } from '@sim/utils'
1011
import { generateId } from '@sim/utils/id'
1112
import { and, eq } from 'drizzle-orm'
1213
import { type NextRequest, NextResponse } from 'next/server'
@@ -17,7 +18,6 @@ import {
1718
import { parseRequest } from '@/lib/api/server'
1819
import { getSession } from '@/lib/auth'
1920
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
20-
import { normalizeEmail } from '@/lib/invitations/core'
2121
import { syncAllWebhooksForCredentialSet } from '@/lib/webhooks/utils.server'
2222

2323
const logger = createLogger('CredentialSetInviteToken')

apps/sim/app/api/invitations/[id]/route.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit'
22
import { db } from '@sim/db'
33
import { invitation, invitationWorkspaceGrant } from '@sim/db/schema'
44
import { createLogger } from '@sim/logger'
5+
import { normalizeEmail } from '@sim/utils'
56
import { and, eq } from 'drizzle-orm'
67
import { type NextRequest, NextResponse } from 'next/server'
78
import {
@@ -13,7 +14,7 @@ import { getValidationErrorMessage, parseRequest } from '@/lib/api/server'
1314
import { getSession } from '@/lib/auth'
1415
import { isOrganizationOwnerOrAdmin } from '@/lib/billing/core/organization'
1516
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
16-
import { cancelInvitation, getInvitationById, normalizeEmail } from '@/lib/invitations/core'
17+
import { cancelInvitation, getInvitationById } from '@/lib/invitations/core'
1718
import { hasWorkspaceAdminAccess } from '@/lib/workspaces/permissions/utils'
1819

1920
const logger = createLogger('InvitationsAPI')

apps/sim/app/api/tools/crowdstrike/query/route.ts

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { createLogger } from '@sim/logger'
2+
import { isRecordLike } from '@sim/utils'
23
import { toError } from '@sim/utils/errors'
34
import { generateId } from '@sim/utils/id'
45
import { type NextRequest, NextResponse } from 'next/server'
@@ -31,10 +32,6 @@ function getCloudBaseUrl(cloud: CrowdStrikeCloud): string {
3132
return cloudMap[cloud]
3233
}
3334

34-
function isJsonRecord(value: unknown): value is JsonRecord {
35-
return typeof value === 'object' && value !== null && !Array.isArray(value)
36-
}
37-
3835
function getString(value: unknown): string | null {
3936
return typeof value === 'string' ? value : null
4037
}
@@ -56,32 +53,32 @@ function getRecordArray(value: unknown): JsonRecord[] {
5653
return []
5754
}
5855

59-
return value.filter(isJsonRecord)
56+
return value.filter(isRecordLike)
6057
}
6158

6259
function getResourcesArray(data: unknown): unknown[] {
6360
const root = getResponseRoot(data)
64-
if (!isJsonRecord(root) || !Array.isArray(root.resources)) {
61+
if (!isRecordLike(root) || !Array.isArray(root.resources)) {
6562
return []
6663
}
6764

6865
return root.resources
6966
}
7067

7168
function getRecordResources(data: unknown): JsonRecord[] {
72-
return getResourcesArray(data).filter(isJsonRecord)
69+
return getResourcesArray(data).filter(isRecordLike)
7370
}
7471

7572
function getStringResources(data: unknown): string[] {
7673
return getStringArray(getResourcesArray(data))
7774
}
7875

7976
function getResponseRoot(data: unknown): unknown {
80-
if (!isJsonRecord(data)) {
77+
if (!isRecordLike(data)) {
8178
return null
8279
}
8380

84-
if (isJsonRecord(data.body)) {
81+
if (isRecordLike(data.body)) {
8582
return data.body
8683
}
8784

@@ -90,7 +87,7 @@ function getResponseRoot(data: unknown): unknown {
9087

9188
function getPagination(data: unknown) {
9289
const root = getResponseRoot(data)
93-
if (!isJsonRecord(root) || !isJsonRecord(root.meta) || !isJsonRecord(root.meta.pagination)) {
90+
if (!isRecordLike(root) || !isRecordLike(root.meta) || !isRecordLike(root.meta.pagination)) {
9491
return null
9592
}
9693

@@ -102,13 +99,13 @@ function getPagination(data: unknown) {
10299
}
103100

104101
function getErrorMessage(data: unknown, fallback: string): string {
105-
if (!isJsonRecord(data)) {
102+
if (!isRecordLike(data)) {
106103
return fallback
107104
}
108105

109106
const errors = Array.isArray(data.errors) ? data.errors : []
110107
const firstError = errors[0]
111-
if (isJsonRecord(firstError)) {
108+
if (isRecordLike(firstError)) {
112109
const firstMessage = getString(firstError.message) ?? getString(firstError.code)
113110
if (firstMessage) {
114111
return firstMessage
@@ -179,7 +176,7 @@ async function getAccessToken(params: CrowdStrikeBaseParams): Promise<string> {
179176
throw new Error(getErrorMessage(data, 'Failed to authenticate with CrowdStrike'))
180177
}
181178

182-
if (!isJsonRecord(data) || typeof data.access_token !== 'string') {
179+
if (!isRecordLike(data) || typeof data.access_token !== 'string') {
183180
throw new Error('CrowdStrike authentication did not return an access token')
184181
}
185182

@@ -234,7 +231,7 @@ function normalizeAggregationBucket(resource: JsonRecord): CrowdStrikeSensorAggr
234231
count: getNumber(resource.count),
235232
from: getNumber(resource.from),
236233
keyAsString: getString(resource.key_as_string),
237-
label: isJsonRecord(resource.label) ? resource.label : null,
234+
label: isRecordLike(resource.label) ? resource.label : null,
238235
stringFrom: getString(resource.string_from),
239236
stringTo: getString(resource.string_to),
240237
subAggregates: getRecordArray(resource.sub_aggregates).map(normalizeAggregationResult),

apps/sim/app/api/tools/image/route.ts

Lines changed: 16 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { createLogger } from '@sim/logger'
2+
import { isRecordLike } from '@sim/utils'
23
import { getErrorMessage, toError } from '@sim/utils/errors'
34
import { sleep } from '@sim/utils/helpers'
45
import { type NextRequest, NextResponse } from 'next/server'
@@ -431,10 +432,6 @@ const FALAI_IMAGE_MODEL_CONFIGS: Record<string, FalAIImageModelConfig> = {
431432
},
432433
}
433434

434-
function isRecord(value: unknown): value is Record<string, unknown> {
435-
return typeof value === 'object' && value !== null && !Array.isArray(value)
436-
}
437-
438435
function getStringProperty(
439436
record: Record<string, unknown> | undefined,
440437
key: string
@@ -452,7 +449,7 @@ function getNumberProperty(
452449
}
453450

454451
function firstRecord(value: unknown): Record<string, unknown> | undefined {
455-
return Array.isArray(value) ? value.find(isRecord) : undefined
452+
return Array.isArray(value) ? value.find(isRecordLike) : undefined
456453
}
457454

458455
function pickAllowed(
@@ -575,7 +572,7 @@ async function generateWithOpenAI(
575572
maxBytes: MAX_IMAGE_JSON_BYTES,
576573
label: 'OpenAI image response',
577574
})
578-
if (!isRecord(data)) {
575+
if (!isRecordLike(data)) {
579576
throw new Error('Invalid OpenAI image response')
580577
}
581578

@@ -669,27 +666,27 @@ async function generateWithGemini(
669666
maxBytes: MAX_IMAGE_JSON_BYTES,
670667
label: 'Gemini image response',
671668
})
672-
if (!isRecord(data)) {
669+
if (!isRecordLike(data)) {
673670
throw new Error('Invalid Gemini image response')
674671
}
675672

676673
const candidate = firstRecord(data.candidates)
677-
const content = isRecord(candidate?.content) ? candidate.content : undefined
674+
const content = isRecordLike(candidate?.content) ? candidate.content : undefined
678675
const parts = Array.isArray(content?.parts) ? content.parts : []
679-
const textPart = parts.find((part) => isRecord(part) && typeof part.text === 'string')
676+
const textPart = parts.find((part) => isRecordLike(part) && typeof part.text === 'string')
680677
const imagePart = parts.find((part) => {
681-
if (!isRecord(part)) return false
682-
return isRecord(part.inlineData) || isRecord(part.inline_data)
678+
if (!isRecordLike(part)) return false
679+
return isRecordLike(part.inlineData) || isRecordLike(part.inline_data)
683680
})
684681

685-
if (!isRecord(imagePart)) {
682+
if (!isRecordLike(imagePart)) {
686683
logger.error(`[${requestId}] Gemini response missing image part`)
687684
throw new Error('No image data found in Gemini response')
688685
}
689686

690-
const inlineData = isRecord(imagePart.inlineData)
687+
const inlineData = isRecordLike(imagePart.inlineData)
691688
? imagePart.inlineData
692-
: isRecord(imagePart.inline_data)
689+
: isRecordLike(imagePart.inline_data)
693690
? imagePart.inline_data
694691
: undefined
695692
const base64Image = getStringProperty(inlineData, 'data')
@@ -712,7 +709,7 @@ async function generateWithGemini(
712709
fileName: `gemini-${model}.${extensionFromContentType(contentType)}`,
713710
provider: 'gemini',
714711
model,
715-
description: isRecord(textPart) ? getStringProperty(textPart, 'text') : undefined,
712+
description: isRecordLike(textPart) ? getStringProperty(textPart, 'text') : undefined,
716713
}
717714
}
718715

@@ -722,7 +719,7 @@ function buildFalAIQueueUrl(endpoint: string, requestId: string, path: 'status'
722719

723720
function getFalAIErrorMessage(error: unknown): string {
724721
if (typeof error === 'string') return error
725-
if (isRecord(error)) {
722+
if (isRecordLike(error)) {
726723
return (
727724
getStringProperty(error, 'message') ||
728725
getStringProperty(error, 'detail') ||
@@ -835,7 +832,7 @@ async function generateWithFalAI(
835832
maxBytes: MAX_IMAGE_JSON_BYTES,
836833
label: 'Fal.ai create response',
837834
})
838-
if (!isRecord(createData)) {
835+
if (!isRecordLike(createData)) {
839836
throw new Error('Invalid Fal.ai queue response')
840837
}
841838

@@ -878,7 +875,7 @@ async function generateWithFalAI(
878875
maxBytes: MAX_IMAGE_JSON_BYTES,
879876
label: 'Fal.ai status response',
880877
})
881-
if (!isRecord(statusData)) {
878+
if (!isRecordLike(statusData)) {
882879
throw new Error('Invalid Fal.ai status response')
883880
}
884881

@@ -910,7 +907,7 @@ async function generateWithFalAI(
910907
maxBytes: MAX_IMAGE_JSON_BYTES,
911908
label: 'Fal.ai result response',
912909
})
913-
if (!isRecord(resultData)) {
910+
if (!isRecordLike(resultData)) {
914911
throw new Error('Invalid Fal.ai result response')
915912
}
916913

apps/sim/app/api/tools/video/route.ts

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { createLogger } from '@sim/logger'
2+
import { isRecordLike } from '@sim/utils'
23
import { getErrorMessage } from '@sim/utils/errors'
34
import { sleep } from '@sim/utils/helpers'
45
import { generateId } from '@sim/utils/id'
@@ -1081,10 +1082,6 @@ function formatFalAIDuration(
10811082
return String(duration)
10821083
}
10831084

1084-
function isRecord(value: unknown): value is Record<string, unknown> {
1085-
return typeof value === 'object' && value !== null && !Array.isArray(value)
1086-
}
1087-
10881085
function getStringProperty(
10891086
record: Record<string, unknown> | undefined,
10901087
key: string
@@ -1159,7 +1156,7 @@ function getFalAIValidationError(
11591156

11601157
function getFalAIErrorMessage(error: unknown): string {
11611158
if (typeof error === 'string') return error
1162-
if (isRecord(error)) return getStringProperty(error, 'message') || JSON.stringify(error)
1159+
if (isRecordLike(error)) return getStringProperty(error, 'message') || JSON.stringify(error)
11631160
return 'Unknown error'
11641161
}
11651162

@@ -1236,7 +1233,7 @@ async function generateWithFalAI(
12361233
}
12371234

12381235
const createData = await readVideoJson<unknown>(createResponse, 'Fal.ai queue response')
1239-
if (!isRecord(createData)) {
1236+
if (!isRecordLike(createData)) {
12401237
throw new Error('Invalid Fal.ai queue response')
12411238
}
12421239

@@ -1273,7 +1270,7 @@ async function generateWithFalAI(
12731270
}
12741271

12751272
const statusData = await readVideoJson<unknown>(statusResponse, 'Fal.ai status response')
1276-
if (!isRecord(statusData)) {
1273+
if (!isRecordLike(statusData)) {
12771274
throw new Error('Invalid Fal.ai status response')
12781275
}
12791276

@@ -1300,12 +1297,12 @@ async function generateWithFalAI(
13001297
}
13011298

13021299
const resultData = await readVideoJson<unknown>(resultResponse, 'Fal.ai result response')
1303-
if (!isRecord(resultData)) {
1300+
if (!isRecordLike(resultData)) {
13041301
throw new Error('Invalid Fal.ai result response')
13051302
}
13061303

1307-
const videoOutput = isRecord(resultData.video) ? resultData.video : undefined
1308-
const fallbackOutput = isRecord(resultData.output) ? resultData.output : undefined
1304+
const videoOutput = isRecordLike(resultData.video) ? resultData.video : undefined
1305+
const fallbackOutput = isRecordLike(resultData.output) ? resultData.output : undefined
13091306
const videoUrl =
13101307
getStringProperty(videoOutput, 'url') || getStringProperty(fallbackOutput, 'url')
13111308
if (!videoUrl) {

apps/sim/app/api/workspaces/invitations/batch/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { createLogger } from '@sim/logger'
2+
import { normalizeEmail } from '@sim/utils'
23
import { type NextRequest, NextResponse } from 'next/server'
34
import { batchWorkspaceInvitationsContract } from '@/lib/api/contracts/invitations'
45
import { parseRequest } from '@/lib/api/server'
56
import { getSession } from '@/lib/auth'
67
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
7-
import { normalizeEmail } from '@/lib/invitations/core'
88
import {
99
createWorkspaceInvitation,
1010
prepareWorkspaceInvitationContext,

apps/sim/app/workspace/[workspaceId]/home/hooks/stream/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,4 @@ export {
88
type StreamLoopOptions,
99
type StreamLoopState,
1010
} from './stream-context'
11-
export { finalizeResidualToolCalls, isRecord } from './stream-helpers'
11+
export { finalizeResidualToolCalls } from './stream-helpers'

apps/sim/app/workspace/[workspaceId]/home/hooks/stream/stream-helpers.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { createLogger } from '@sim/logger'
2+
import { isRecordLike } from '@sim/utils'
23
import { resolveStreamToolOutcome } from '@/lib/copilot/chat/stream-tool-outcome'
34
import type { MothershipStreamV1ToolUI } from '@/lib/copilot/generated/mothership-stream-v1'
45
import {
@@ -81,12 +82,8 @@ export type ToolResultPhasePayload = {
8182
success?: boolean
8283
}
8384

84-
export function isRecord(value: unknown): value is Record<string, unknown> {
85-
return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
86-
}
87-
8885
export function asPayloadRecord(value: unknown): StreamPayload | undefined {
89-
return isRecord(value) ? value : undefined
86+
return isRecordLike(value) ? value : undefined
9087
}
9188

9289
export function getToolUI(ui?: MothershipStreamV1ToolUI): StreamToolUI | undefined {

0 commit comments

Comments
 (0)