sanity
Version:
Sanity is a real-time content infrastructure with a scalable, hosted backend featuring a Graph Oriented Query Language (GROQ), asset pipelines and fast edge caches
171 lines (137 loc) • 4.35 kB
text/typescript
import {
type IndexTuple,
isIndexSegment,
isIndexTuple,
isKeyedObject,
isKeySegment,
type KeyedSegment,
type Path,
type PathSegment,
} from '@sanity/types'
import {isRecord} from '../../util'
const rePropName =
/[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g
const reKeySegment = /_key\s*==\s*['"](.*)['"]/
/** @internal */
export function pathToString(path: Path): string {
if (!Array.isArray(path)) {
throw new Error('Path is not an array')
}
return path.reduce<string>((target, segment, i) => {
if (isIndexSegment(segment)) {
return `${target}[${segment}]`
}
if (isKeySegment(segment) && segment._key) {
return `${target}[_key=="${segment._key}"]`
}
if (isIndexTuple(segment)) {
const [from, to] = segment
return `${target}[${from}:${to}]`
}
if (typeof segment === 'string') {
const separator = i === 0 ? '' : '.'
return `${target}${separator}${segment}`
}
throw new Error(`Unsupported path segment \`${JSON.stringify(segment)}\``)
}, '')
}
/** @internal */
export function getValueAtPath(rootValue: unknown, path: Path): unknown {
const segment = path[0]
if (!segment) {
return rootValue
}
const tail = path.slice(1)
if (isIndexSegment(segment)) {
return getValueAtPath(Array.isArray(rootValue) ? rootValue[segment] : undefined, tail)
}
if (isKeySegment(segment)) {
return getValueAtPath(
Array.isArray(rootValue) ? rootValue.find((item) => item._key === segment._key) : undefined,
tail,
)
}
if (typeof segment === 'string') {
return getValueAtPath(isRecord(rootValue) ? rootValue[segment] : undefined, tail)
}
throw new Error(`Unknown segment type ${JSON.stringify(segment)}`)
}
/** @internal */
export function findIndex(array: unknown[], segment: PathSegment): number {
if (typeof segment !== 'number' && !isKeySegment(segment)) {
return -1
}
return typeof segment === 'number'
? segment
: array.findIndex((item) => isKeyedObject(item) && item._key === segment._key)
}
/** @internal */
export function stringToPath(path: string): Path {
const segments = path.match(rePropName)
if (!segments) {
throw new Error('Invalid path string')
}
return segments.map(normalizePathSegment)
}
/** @internal */
export function normalizePathSegment(segment: string): PathSegment {
if (isIndexSegment(segment)) {
return normalizeIndexSegment(segment)
}
if (isKeySegment(segment)) {
return normalizeKeySegment(segment)
}
if (isIndexTuple(segment)) {
return normalizeIndexTupleSegment(segment)
}
return segment
}
/** @internal */
export function normalizeIndexSegment(segment: string): PathSegment {
return Number(segment.replace(/[^\d]/g, ''))
}
/** @internal */
export function normalizeKeySegment(segment: string): KeyedSegment {
const segments = segment.match(reKeySegment)
if (!segments) {
throw new Error('Invalid key segment')
}
return {_key: segments[1]}
}
/** @internal */
export function normalizeIndexTupleSegment(segment: string): IndexTuple {
const [from, to] = segment.split(':').map((seg) => (seg === '' ? seg : Number(seg)))
return [from, to]
}
/** @internal */
export function pathsAreEqual(pathA: Path, pathB: Path): boolean {
if (pathA.length !== pathB.length) {
return false
}
return pathA.every((segmentA, index) => {
const segmentB = pathB[index]
if (isKeySegment(segmentA) && isKeySegment(segmentB)) {
return segmentA._key === segmentB._key
}
if (isIndexSegment(segmentA)) {
return Number(segmentA) === Number(segmentB)
}
if (isIndexTuple(segmentA) && isIndexTuple(segmentB)) {
return segmentA[0] === segmentB[0] && segmentA[1] === segmentB[1]
}
return segmentA === segmentB
})
}
/** @internal */
export function getItemKey(arrayItem: unknown): string | undefined {
return isKeyedObject(arrayItem) ? arrayItem._key : undefined
}
/** @internal */
export function getItemKeySegment(arrayItem: unknown): KeyedSegment | undefined {
const key = getItemKey(arrayItem)
return key ? {_key: key} : undefined
}
/** @internal */
export function isEmptyObject(item: unknown): boolean {
return typeof item === 'object' && item !== null && Object.keys(item).length <= 0
}