Skip to content

Commit e73d2d8

Browse files
committed
improvement(utils): adopt shared compareStrings and isRecordLike over local copies
1 parent 720915b commit e73d2d8

23 files changed

Lines changed: 90 additions & 141 deletions

‎apps/sim/ee/scim/lib/protocol/canonical.ts‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import type { ScimUserAttributes, ScimUserEmail } from '@sim/db/schema'
2+
import { isRecordLike } from '@sim/utils/object'
23
import { isValidEmailSyntax } from '@sim/utils/string'
34
import type { ScimGroupWriteParsed, ScimUserWriteParsed } from '@/lib/api/contracts/scim'
45
import { SCIM_ENTERPRISE_USER_SCHEMA } from '@/ee/scim/lib/protocol/constants'
56
import { invalidValue } from '@/ee/scim/lib/protocol/errors'
6-
import { isRecord, isScimPasswordAttribute } from '@/ee/scim/lib/protocol/normalize'
7+
import { isScimPasswordAttribute } from '@/ee/scim/lib/protocol/normalize'
78

89
/** Attributes Sim models itself; everything else is preserved under `extra`. */
910
const MODELLED_USER_KEYS = new Set([
@@ -111,7 +112,7 @@ export function toCanonicalUser(body: ScimUserWriteParsed): ScimUserAttributes {
111112
...(displayName ? { displayName, displayNameSource: 'provider' as const } : {}),
112113
name,
113114
emails,
114-
...(isRecord(enterprise) ? { enterprise: normalizeEnterprise(enterprise) } : {}),
115+
...(isRecordLike(enterprise) ? { enterprise: normalizeEnterprise(enterprise) } : {}),
115116
...(extra ? { extra } : {}),
116117
}
117118
}
@@ -142,7 +143,7 @@ function normalizeEnterprise(value: Record<string, unknown>): EnterpriseAttribut
142143
const manager = value.manager
143144
if (typeof manager === 'string' && manager.trim()) {
144145
enterprise.manager = { value: manager.trim() }
145-
} else if (isRecord(manager)) {
146+
} else if (isRecordLike(manager)) {
146147
const managerValue = text(manager.value)
147148
const displayName = text(manager.displayName)
148149
if (managerValue || displayName) {
@@ -195,6 +196,6 @@ export function toCanonicalGroup(body: ScimGroupWriteParsed): CanonicalScimGroup
195196
/** Reads the member id out of a PATCH value entry, which may be bare or wrapped. */
196197
export function readMemberValue(entry: unknown): string {
197198
if (typeof entry === 'string') return entry.trim()
198-
if (isRecord(entry) && typeof entry.value === 'string') return entry.value.trim()
199+
if (isRecordLike(entry) && typeof entry.value === 'string') return entry.value.trim()
199200
throw invalidValue('Group member entries require a value')
200201
}

‎apps/sim/ee/scim/lib/protocol/group-patch.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1+
import { isRecordLike } from '@sim/utils/object'
12
import type { ScimPatchOperation } from '@/lib/api/contracts/scim'
23
import { readMemberValue } from '@/ee/scim/lib/protocol/canonical'
34
import { invalidPath, invalidValue, mutability, noTarget } from '@/ee/scim/lib/protocol/errors'
4-
import { isRecord, normalizeAttributePath } from '@/ee/scim/lib/protocol/normalize'
5+
import { normalizeAttributePath } from '@/ee/scim/lib/protocol/normalize'
56

67
/**
78
* A parsed Group PATCH.
@@ -86,7 +87,7 @@ export function parseGroupPatch(operations: readonly ScimPatchOperation[]): Grou
8687

8788
if (!operation.path) {
8889
const value = operation.value
89-
if (!isRecord(value)) {
90+
if (!isRecordLike(value)) {
9091
throw invalidValue('A PATCH operation without a path requires an object value')
9192
}
9293
incremental = false

‎apps/sim/ee/scim/lib/protocol/normalize.ts‎

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { isRecordLike } from '@sim/utils/object'
12
/**
23
* Tolerances for what identity providers actually send, as distinct from what
34
* RFC 7644 describes.
@@ -35,11 +36,6 @@ export function unwrapSingleElement(value: unknown): unknown {
3536
return Array.isArray(value) && value.length === 1 ? value[0] : value
3637
}
3738

38-
/** True when the value is a plain object rather than an array or null. */
39-
export function isRecord(value: unknown): value is Record<string, unknown> {
40-
return typeof value === 'object' && value !== null && !Array.isArray(value)
41-
}
42-
4339
/**
4440
* Strips a schema URN prefix from an attribute path and decodes it.
4541
*
@@ -94,7 +90,7 @@ export function canonicalizeAttributeNames(
9490
body: unknown,
9591
canonicalNames: readonly string[]
9692
): unknown {
97-
if (!isRecord(body)) return body
93+
if (!isRecordLike(body)) return body
9894
const byLower = new Map(canonicalNames.map((name) => [name.toLowerCase(), name]))
9995
const result: Record<string, unknown> = {}
10096
for (const [key, value] of Object.entries(body)) {

‎apps/sim/ee/scim/lib/protocol/resources.ts‎

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { ScimUserAttributes } from '@sim/db/schema'
2+
import { isRecordLike } from '@sim/utils/object'
23
import {
34
SCIM_ENTERPRISE_USER_SCHEMA,
45
SCIM_GROUP_SCHEMA,
@@ -7,11 +8,7 @@ import {
78
SCIM_USER_SCHEMA,
89
} from '@/ee/scim/lib/protocol/constants'
910
import { invalidValue } from '@/ee/scim/lib/protocol/errors'
10-
import {
11-
isRecord,
12-
isScimPasswordAttribute,
13-
normalizeAttributePath,
14-
} from '@/ee/scim/lib/protocol/normalize'
11+
import { isScimPasswordAttribute, normalizeAttributePath } from '@/ee/scim/lib/protocol/normalize'
1512

1613
export interface ScimResourceMeta {
1714
resourceType: 'User' | 'Group'
@@ -291,7 +288,7 @@ function projectAttribute(
291288
.filter((entry) => entry !== undefined)
292289
return included || projected.length > 0 ? projected : undefined
293290
}
294-
if (!isRecord(value)) return included ? value : undefined
291+
if (!isRecordLike(value)) return included ? value : undefined
295292
const projected: Record<string, unknown> = {}
296293
for (const [key, nested] of Object.entries(value)) {
297294
const selected = projectAttribute(

‎apps/sim/ee/scim/lib/protocol/user-patch.ts‎

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import type { ScimUserAttributes, ScimUserEmail } from '@sim/db/schema'
2+
import { isRecordLike } from '@sim/utils/object'
23
import type { ScimPatchOperation } from '@/lib/api/contracts/scim'
34
import { invalidPath, invalidValue, mutability, noTarget } from '@/ee/scim/lib/protocol/errors'
45
import {
5-
isRecord,
66
isScimPasswordAttribute,
77
normalizeAttributePath,
88
normalizeScimBoolean,
@@ -89,7 +89,7 @@ function normalizeEmailList(
8989
const entries = Array.isArray(value) ? value : [value]
9090
const normalized: ScimUserEmail[] = []
9191
for (const entry of entries) {
92-
if (!isRecord(entry)) throw invalidValue(`${attribute} entries must be objects`)
92+
if (!isRecordLike(entry)) throw invalidValue(`${attribute} entries must be objects`)
9393
const address = requireString(entry.value, `${attribute}.value`)
9494
normalized.push({
9595
value: address.toLowerCase(),
@@ -146,7 +146,7 @@ function applyOperation(
146146
else user.enterprise = undefined
147147
return
148148
}
149-
if (!isRecord(value)) throw invalidValue(`${path} requires an object value`)
149+
if (!isRecordLike(value)) throw invalidValue(`${path} requires an object value`)
150150
for (const [sub, nested] of sortFormattedLast(Object.entries(value))) {
151151
applyOperation(user, op, `${path}.${sub}`, nested)
152152
}
@@ -253,7 +253,7 @@ function applyOperation(
253253
user.enterprise.manager = { value: unwrapped }
254254
return
255255
}
256-
if (isRecord(unwrapped)) {
256+
if (isRecordLike(unwrapped)) {
257257
user.enterprise.manager = {
258258
...(typeof unwrapped.value === 'string' ? { value: unwrapped.value } : {}),
259259
...(typeof unwrapped.displayName === 'string'
@@ -301,7 +301,7 @@ function extensionTarget(
301301
...(path.length > existing.length ? { attribute: path.slice(existing.length + 1) } : {}),
302302
}
303303
}
304-
if (resourceAttribute && isRecord(value)) return { schema: path }
304+
if (resourceAttribute && isRecordLike(value)) return { schema: path }
305305
const separator = path.lastIndexOf(':')
306306
if (separator <= 'urn:'.length) throw invalidPath(`User PATCH path ${path} is not supported`)
307307
return { schema: path.slice(0, separator), attribute: path.slice(separator + 1) }
@@ -329,14 +329,14 @@ function applyExtraOperation(
329329
if (!extension.attribute) {
330330
if (op === 'remove') delete user.extra[extension.schema]
331331
else {
332-
if (!isRecord(value)) throw invalidValue(`${path} requires an object value`)
332+
if (!isRecordLike(value)) throw invalidValue(`${path} requires an object value`)
333333
const current = user.extra[extension.schema]
334-
user.extra[extension.schema] = { ...(isRecord(current) ? current : {}), ...value }
334+
user.extra[extension.schema] = { ...(isRecordLike(current) ? current : {}), ...value }
335335
}
336336
return
337337
}
338338
const current = user.extra[extension.schema]
339-
const attributes = isRecord(current) ? { ...current } : {}
339+
const attributes = isRecordLike(current) ? { ...current } : {}
340340
applyExtraAttribute(attributes, op, extension.attribute, value)
341341
user.extra[extension.schema] = attributes
342342
return
@@ -368,18 +368,18 @@ function applyExtraAttribute(
368368
if (type) {
369369
const list = Array.isArray(attributes[attribute]) ? [...attributes[attribute]] : []
370370
const index = list.findIndex(
371-
(entry) => isRecord(entry) && String(entry.type).toLowerCase() === type.toLowerCase()
371+
(entry) => isRecordLike(entry) && String(entry.type).toLowerCase() === type.toLowerCase()
372372
)
373373
if (op === 'remove' && !sub) {
374374
if (index !== -1) list.splice(index, 1)
375375
} else if (sub) {
376376
if (op === 'remove' && index === -1) return
377-
const current = index !== -1 && isRecord(list[index]) ? list[index] : { type }
377+
const current = index !== -1 && isRecordLike(list[index]) ? list[index] : { type }
378378
const key = Object.keys(current).find((key) => key.toLowerCase() === sub.toLowerCase()) ?? sub
379379
const next = { ...current, [key]: op === 'remove' ? undefined : value }
380380
if (index === -1) list.push(next)
381381
else list[index] = next
382-
} else if (isRecord(value)) {
382+
} else if (isRecordLike(value)) {
383383
if (index === -1) list.push({ type, ...value })
384384
else list[index] = { ...(list[index] as Record<string, unknown>), ...value }
385385
} else {
@@ -389,7 +389,7 @@ function applyExtraAttribute(
389389
return
390390
}
391391

392-
const current = isRecord(attributes[attribute]) ? { ...attributes[attribute] } : {}
392+
const current = isRecordLike(attributes[attribute]) ? { ...attributes[attribute] } : {}
393393
const key = Object.keys(current).find((key) => key.toLowerCase() === sub.toLowerCase()) ?? sub
394394
current[key] = op === 'remove' ? undefined : value
395395
attributes[attribute] = current
@@ -404,7 +404,7 @@ function applyExtraAttribute(
404404
*/
405405
function sortDeep(value: unknown): unknown {
406406
if (Array.isArray(value)) return value.map(sortDeep)
407-
if (!isRecord(value)) return value
407+
if (!isRecordLike(value)) return value
408408
const sorted: Record<string, unknown> = {}
409409
for (const key of Object.keys(value).sort()) sorted[key] = sortDeep(value[key])
410410
return sorted
@@ -454,7 +454,7 @@ export function applyUserPatch(
454454

455455
if (!operation.path) {
456456
const value = operation.value
457-
if (!isRecord(value)) {
457+
if (!isRecordLike(value)) {
458458
throw invalidValue('A PATCH operation without a path requires an object value')
459459
}
460460
/**

‎apps/sim/executor/utils/file-tool-processor.ts‎

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { createLogger } from '@sim/logger'
22
import { toError } from '@sim/utils/errors'
3-
import { omit } from '@sim/utils/object'
3+
import { isRecordLike, omit } from '@sim/utils/object'
44
import { isCanonicalBase64 } from '@/lib/api/contracts/primitives'
55
import { isUserFile, type UserFileLike } from '@/lib/core/utils/user-file'
66
import {
@@ -16,10 +16,6 @@ import type { ToolDefinition } from '@/tools/types'
1616

1717
const logger = createLogger('FileToolProcessor')
1818

19-
function isRecord(value: unknown): value is Record<string, unknown> {
20-
return typeof value === 'object' && value !== null && !Array.isArray(value)
21-
}
22-
2319
/** Strip a data URI prefix while preserving legitimate zero-byte payloads. */
2420
function stripBase64DataUri(value: string): string {
2521
return /^data:[^,]*;base64,/i.test(value) ? value.slice(value.indexOf(',') + 1) : value
@@ -113,7 +109,7 @@ export class FileToolProcessor {
113109
const files = outputDef.type === 'file[]' && Array.isArray(value) ? value : [value]
114110
for (const file of files) {
115111
signal?.throwIfAborted()
116-
if (!isRecord(file)) throw new Error('File output must be a file object')
112+
if (!isRecordLike(file)) throw new Error('File output must be a file object')
117113
if (isUserFile(file)) {
118114
if (file.base64 !== undefined) replacements.set(file, omit(file, ['base64']))
119115
continue
@@ -137,15 +133,15 @@ export class FileToolProcessor {
137133
})
138134
if (replacements.size === 0) return toolOutput
139135
const output = replaceFileReferences(toolOutput, replacements)
140-
if (!isRecord(output)) throw new Error('Tool file output must be an object')
136+
if (!isRecordLike(output)) throw new Error('Tool file output must be an object')
141137
return output
142138
}
143139
if (pendingFiles.size === 0) return present([])
144140
return storeInternalToolFileResult(
145141
createInternalToolFilesResult([...pendingFiles.values()], present),
146142
context,
147143
(output) => {
148-
if (!isRecord(output)) throw new Error('Tool file output must be an object')
144+
if (!isRecordLike(output)) throw new Error('Tool file output must be an object')
149145
return output
150146
},
151147
signal
@@ -178,7 +174,7 @@ export class FileToolProcessor {
178174
data instanceof ArrayBuffer
179175
? Buffer.from(data)
180176
: Buffer.from(data.buffer, data.byteOffset, data.byteLength)
181-
} else if (Array.isArray(data) || (isRecord(data) && data.type === 'Buffer')) {
177+
} else if (Array.isArray(data) || (isRecordLike(data) && data.type === 'Buffer')) {
182178
const bytes = Array.isArray(data) ? data : data.data
183179
if (!Array.isArray(bytes)) throw new Error(`Invalid serialized buffer format for ${name}`)
184180
assertFileSize(bytes.length, name, remainingBytes)

‎apps/sim/executor/utils/resolved-secret-matcher-capacity.ts‎

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,10 @@
1+
import { compareStrings } from '@sim/utils/string'
2+
13
const MAX_MATCHER_NODES = 250_000
24
const MAX_SECRET_LITERAL_LENGTH = 64 * 1024
35

46
export type ResolvedSecretMatcherCapacityFailure = 'literal-too-long' | 'node-limit-exceeded'
57

6-
function compareStrings(left: string, right: string): number {
7-
if (left < right) return -1
8-
if (left > right) return 1
9-
return 0
10-
}
11-
128
function commonPrefixLength(left: string, right: string): number {
139
const limit = Math.min(left.length, right.length)
1410
let index = 0

‎apps/sim/executor/utils/resolved-secret-matcher.ts‎

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { compareStrings } from '@sim/utils/string'
12
import { MAX_INLINE_MATERIALIZATION_BYTES } from '@/lib/execution/payloads/limits'
23
import { isNonIdentifyingSecretLiteral } from '@/executor/utils/resolved-secret-match-policy'
34
import { getResolvedSecretMatcherCapacityFailure } from '@/executor/utils/resolved-secret-matcher-capacity'
@@ -49,12 +50,6 @@ class ResolvedSecretMatcherError extends Error {
4950
}
5051
}
5152

52-
function compareStrings(left: string, right: string): number {
53-
if (left < right) return -1
54-
if (left > right) return 1
55-
return 0
56-
}
57-
5853
function createMatcherFromReplacements(
5954
replacements: readonly SecretReplacement[]
6055
): ResolvedSecretMatcher {

‎apps/sim/executor/utils/resolved-secret-trace-registry.ts‎

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { createLogger } from '@sim/logger'
22
import { getErrorMessage } from '@sim/utils/errors'
3+
import { compareStrings } from '@sim/utils/string'
34
import { decryptSecret } from '@/lib/core/security/encryption'
45
import { isLargeArrayManifest } from '@/lib/execution/payloads/large-array-manifest-metadata'
56
import { isLargeValueRef } from '@/lib/execution/payloads/large-value-ref'
@@ -443,12 +444,6 @@ export interface CreateResolvedSecretTraceRegistryOptions {
443444
workspaceUnredactedKeys?: readonly string[]
444445
}
445446

446-
function compareStrings(left: string, right: string): number {
447-
if (left < right) return -1
448-
if (left > right) return 1
449-
return 0
450-
}
451-
452447
function cloneProvenanceScope(scope: ResolvedSecretTraceScopeV1): ResolvedSecretTraceScopeV1 {
453448
return {
454449
userId: scope.userId,

‎apps/sim/lib/execution/durable-secret-provenance.ts‎

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { createHash } from 'node:crypto'
22
import type { DurableSecretProvenanceEntry } from '@sim/db/schema'
3+
import { compareStrings } from '@sim/utils/string'
34
import {
45
isPrivateSecretProvenanceBundleV1,
56
type PrivateSecretProvenanceBundleV1,
@@ -24,10 +25,6 @@ export const EXACT_EMPTY_DURABLE_SECRET_PROVENANCE = Object.freeze({
2425
entries: Object.freeze([]),
2526
})
2627

27-
function compareStrings(left: string, right: string): number {
28-
return left < right ? -1 : left > right ? 1 : 0
29-
}
30-
3128
/** Normalizes one private sidecar payload and enforces the shared resource bounds. */
3229
export function normalizeDurableSecretProvenanceEntries(
3330
value: unknown

0 commit comments

Comments
 (0)