UNPKG

hydrogen-sanity

Version:
154 lines (134 loc) 4.79 kB
import { type ClientPerspective, type QueryParams, type QueryWithoutParams, validateApiPerspective, } from '@sanity/client' import {urlSearchParamPreviewPerspective} from '@sanity/preview-url-secret/constants' import type {HydrogenSession} from '@shopify/hydrogen' import type {SanityPreviewSession} from './preview/session' /** * Create an SHA-256 hash as a hex string * @see https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest#converting_a_digest_to_a_hex_string */ export async function sha256(message: string): Promise<string> { // encode as UTF-8 const messageBuffer = await new TextEncoder().encode(message) // hash the message const hashBuffer = await crypto.subtle.digest('SHA-256', messageBuffer) // convert bytes to hex string return Array.from(new Uint8Array(hashBuffer)) .map((b) => b.toString(16).padStart(2, '0')) .join('') } /** * Hash query and its parameters for use as cache key. * NOTE: Oxygen deployment will break if the cache key is long or contains `\n` */ export function hashQuery( query: string, params: QueryParams | QueryWithoutParams, ): Promise<string> { let hash = query if (params) { hash += JSON.stringify(params) } return sha256(hash) } /** * Sanitizes and validates a perspective value. * Handles both string (comma-separated) and array formats. */ export function sanitizePerspective(perspective: unknown): Exclude<ClientPerspective, 'raw'> { let sanitizedPerspective = typeof perspective === 'string' && perspective.includes(',') ? perspective.split(',') : perspective // Filter out empty strings and undefined values from perspective array if (Array.isArray(sanitizedPerspective)) { sanitizedPerspective = sanitizedPerspective.filter( (p): p is string => typeof p === 'string' && p.length > 0, ) } validateApiPerspective(sanitizedPerspective) return sanitizedPerspective === 'raw' ? 'drafts' : sanitizedPerspective } /** * Check if API version supports perspective stack (v2025-02-19 or later) * Special versions: '1' doesn't support perspectives, 'X' does support perspectives */ export function supportsPerspectiveStack(apiVersion: string): boolean { // Special cases if (apiVersion === '1') return false if (apiVersion === 'X') return true // Normalize version by removing 'v' prefix if present const normalizedVersion = `${apiVersion}`.replace(/^v/, '') // Parse date format: 2025-02-19 if (!/^\d{4}-\d{2}-\d{2}$/.test(normalizedVersion)) return false const versionDate = new Date(normalizedVersion) const cutoffDate = new Date('2025-02-19') return versionDate >= cutoffDate } /** * Extracts and validates the perspective from a session. */ export function getPerspective(session: SanityPreviewSession | HydrogenSession): ClientPerspective { const perspective = session .get('perspective') ?.split(',') .filter((p: string) => p.length > 0) validateApiPerspective(perspective) return perspective } /** * Reads the `sanity-preview-perspective` URL search param and validates it. * Returns `undefined` if absent or invalid, so callers can fall back to the session. */ export function getPerspectiveFromUrl(url: URL | string): ClientPerspective | undefined { try { const parsed = typeof url === 'string' ? new URL(url) : url const param = parsed.searchParams.get(urlSearchParamPreviewPerspective) if (!param) return undefined return sanitizePerspective(param) } catch { return undefined } } /** * Type guard that checks if a session object is a SanityPreviewSession. * Validates presence of required methods: has, destroy (in addition to Hydrogen session methods). */ export function isSanityPreviewSession(session: unknown): session is SanityPreviewSession { return ( isHydrogenSession(session) && 'has' in session && typeof session.has === 'function' && 'destroy' in session && typeof session.destroy === 'function' ) } /** * Type guard that checks if a session object is a valid Hydrogen session. * Validates presence of required methods: get, set, unset, commit. */ export function isHydrogenSession(session: unknown): session is HydrogenSession { return ( !!session && typeof session === 'object' && 'get' in session && typeof session.get === 'function' && 'set' in session && typeof session.set === 'function' && 'unset' in session && typeof session.unset === 'function' && 'commit' in session && typeof session.commit === 'function' ) } /** * Utility function that detects if code is running on the server. * Used for SSR safety and preventing client-only code from running on server. */ export function isServer(): boolean { return typeof document === 'undefined' }