Files
sim/packages/utils/src/object.ts
T
Waleed 0673e3c0f7 refactor(sim): consolidate record guards + pure utils into @sim/utils (#5061)
* feat(utils): add record guards and pure helpers to @sim/utils

Add isRecordLike (loose, non-prototype-checked record guard) and
sortObjectKeysDeep; relocate isPlainRecord (strict) and normalizeEmail
into @sim/utils so they are reusable across apps and packages. Unit tests
cover the loose-vs-strict distinction, deep key sorting, and email
normalization.

* 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.
2026-06-15 12:32:19 -07:00

73 lines
2.6 KiB
TypeScript

/**
* Returns a new object with the given keys removed.
*
* @example
* omit({ a: 1, b: 2, c: 3 }, ['b', 'c']) // { a: 1 }
*/
export function omit<T extends object, K extends keyof T>(obj: T, keys: K[]): Omit<T, K> {
const result = { ...obj }
for (const key of keys) {
delete result[key]
}
return result
}
/**
* Returns a shallow copy of `obj` with all `undefined`-valued keys removed.
* Useful for building query-param or request-body objects where undefined
* fields should not be serialized.
*
* Replaces the common `Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== undefined))` pattern.
*/
export function filterUndefined<T extends Record<string, unknown>>(obj: T): Partial<T> {
return Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== undefined)) as Partial<T>
}
/**
* Returns true only for object-map values, excluding arrays and null.
*/
export function isPlainRecord(value: unknown): value is Record<string, unknown> {
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
return false
}
const prototype = Object.getPrototypeOf(value)
return prototype === Object.prototype || prototype === null
}
/**
* Returns true for any non-null, non-array object — the LOOSE record check.
* Unlike {@link isPlainRecord}, this does NOT inspect the prototype, so it also
* accepts class instances, Date, Map, etc. Use this when you only need to know a
* value is an indexable object (e.g. before spreading or Object.entries), and
* reach for isPlainRecord when you must exclude exotic objects.
*/
export function isRecordLike(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value)
}
/**
* Recursively sorts the keys of every plain object reachable from {@link value},
* preserving array order while recursing into array elements. Primitives and
* `null` are returned unchanged. Produces a structurally equivalent value with
* deterministic key ordering, suitable for stable comparison or serialization.
*
* @remarks Only string keys are sorted and retained; symbol keys are dropped,
* matching the stable-serialization callers this serves.
*/
export function sortObjectKeysDeep(value: unknown): unknown {
if (Array.isArray(value)) {
return value.map(sortObjectKeysDeep)
}
if (value !== null && typeof value === 'object') {
const obj = value as Record<string, unknown>
return Object.keys(obj)
.sort()
.reduce((result: Record<string, unknown>, key: string) => {
result[key] = sortObjectKeysDeep(obj[key])
return result
}, {})
}
return value
}