UNPKG

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

242 lines (199 loc) 7.2 kB
import { isIndexSegment, isKeySegment, isReferenceSchemaType, type ObjectField, type ObjectFieldType, type ObjectSchemaType, type SanityDocumentLike, type SchemaType, } from '@sanity/types' import * as PathUtils from '@sanity/util/paths' import {type ExprNode, parse} from 'groq-js' import {collate, getPublishedId} from 'sanity' import {type DocumentListPaneItem, type SortOrder} from './types' export function getDocumentKey(value: DocumentListPaneItem, index: number): string { return value._id ? getPublishedId(value._id) : `item-${index}` } export function removePublishedWithDrafts(documents: SanityDocumentLike[]): DocumentListPaneItem[] { return collate(documents).map((entry) => { const doc = entry.draft || entry.published || entry.versions[0] const hasDraft = Boolean(entry.draft) return { ...doc, hasPublished: !!entry.published, hasDraft, } }) as any } export function applyOrderingFunctions(order: SortOrder, schemaType: ObjectSchemaType): SortOrder { const orderBy = order.by.map((by) => { // Skip those that already have a mapper if (by.mapWith) { return by } const fieldType = tryResolveSchemaTypeForPath(schemaType, by.field) if (!fieldType) { return by } // Note: order matters here, since the jsonType of a date field is `string`, // but we want to apply `datetime()`, not `lower()` if (fieldExtendsType(fieldType, 'datetime')) { return {...by, mapWith: 'dateTime'} } if (fieldType.jsonType === 'string') { return {...by, mapWith: 'lower'} } return by }) return orderBy.every((item, index) => item === order.by[index]) ? order : {...order, by: orderBy} } function tryResolveSchemaTypeForPath(baseType: SchemaType, path: string): SchemaType | undefined { const pathSegments = PathUtils.fromString(path) let current: SchemaType | undefined = baseType for (const segment of pathSegments) { if (!current) { return undefined } if (typeof segment === 'string') { current = getFieldTypeByName(current, segment) continue } const isArrayAccessor = isKeySegment(segment) || isIndexSegment(segment) if (!isArrayAccessor || current.jsonType !== 'array') { return undefined } const [memberType, otherType] = current.of || [] if (otherType || !memberType) { // Can't figure out the type without knowing the value return undefined } if (!isReferenceSchemaType(memberType)) { current = memberType continue } const [refType, otherRefType] = memberType.to || [] if (otherRefType || !refType) { // Can't figure out the type without knowing the value return undefined } current = refType } return current } function getFieldTypeByName(type: SchemaType, fieldName: string): SchemaType | undefined { if (!('fields' in type)) { return undefined } const fieldType = type.fields.find((field) => field.name === fieldName) return fieldType ? fieldType.type : undefined } export function fieldExtendsType(field: ObjectField | ObjectFieldType, ofType: string): boolean { let current: SchemaType | undefined = field.type while (current) { if (current.name === ofType) { return true } if (!current.type && current.jsonType === ofType) { return true } current = current.type } return false } /** * Recursively extract static `_type`s from GROQ filter expressions. If the * types can't be statically determined then it will return `null`. */ // eslint-disable-next-line complexity function findTypes(node: ExprNode): Set<string> | null { switch (node.type) { case 'OpCall': { const {left, right} = node switch (node.op) { // e.g. `a == b` case '==': { // e.g. `_type == 'value'` if (left.type === 'AccessAttribute' && left.name === '_type' && !left.base) { if (right.type !== 'Value' || typeof right.value !== 'string') return null return new Set([right.value]) } // e.g. `'value' == _type` if (right.type === 'AccessAttribute' && right.name === '_type' && !right.base) { if (left.type !== 'Value' || typeof left.value !== 'string') return null return new Set([left.value]) } // otherwise, we can't determine the types statically return null } // e.g. `a in b` case 'in': { // if `_type` is not on the left hand side of `in` then it can't be determined if (left.type !== 'AccessAttribute' || left.name !== '_type' || left.base) return null // if the right hand side is not an array then the types can't be determined if (right.type !== 'Array') return null const types = new Set<string>() // iterate through all the types for (const element of right.elements) { // if we find a splat, then early return, we can't determine the types if (element.isSplat) return null // if the array element is not just a simple value, then early return if (element.value.type !== 'Value') return null // if the array element value is not a string, then early return if (typeof element.value.value !== 'string') return null // otherwise add the element value to the set of types types.add(element.value.value) } // if there were any elements in the types set, return it if (types.size) return types // otherwise, the set of types cannot be determined return null } default: { return null } } } // groups can just be unwrapped, the AST preserves the order case 'Group': { return findTypes(node.base) } // e.g. `_type == 'a' || _type == 'b'` // with Or nodes, if we can't determine the types for either the left or // right hand side then we can't determine the types for any // e.g. `_type == 'a' || isActive` // — can't determine types because `isActive` could be true on another types case 'Or': { const left = findTypes(node.left) if (!left) return null const right = findTypes(node.right) if (!right) return null return new Set([...left, ...right]) } // e.g. `_type == 'a' && isActive` // with And nodes, we can determine the types as long as we can determine // the types for one side. We can't determine the types if both are `null`. case 'And': { const left = findTypes(node.left) const right = findTypes(node.right) if (!left && !right) return null return new Set([...(left || []), ...(right || [])]) } default: { return null } } } export function findStaticTypesInFilter( filter: string, params: Record<string, unknown> = {}, ): string[] | null { try { const types = findTypes(parse(filter, {params})) if (!types) return null return Array.from(types).sort() } catch { // if we couldn't parse the filter, just return `null` return null } }