UNPKG

@sanity/client

Version:

Client for retrieving, creating and patching data from Sanity.io

114 lines (109 loc) 5.28 kB
import type {Any, ClientReturn} from '@sanity/client' /** * A string that might contain stega-encoded data, and is unsafe to compare against string literals * until it's been cleaned with `stegaClean()`. * * The type intersects a template literal that has a human readable suffix, with a brand property * that holds the original string type: * - The suffix makes TypeScript report "This comparison appears to be unintentional" (TS2367) when * the string is compared to a literal (`===`, `switch` cases), and makes it unassignable to * literal unions like `'left' | 'right'`, while keeping it assignable to `string` so rendering, * interpolation and string methods keep working. The suffix is a type-level fiction, the runtime * value never ends with that text. * - The brand property preserves the original type so `stegaClean()` can recover it exactly. It's * a string-keyed property rather than a `unique symbol` so that branded types stay structurally * compatible across duplicated copies of `@sanity/client` in `node_modules`. It only exists in * the type system and is never present at runtime. * * @beta */ export type StegaString<T extends string = string> = `${T} (may contain hidden stega characters)` & { readonly ' stegaBrand': T } /** * Deeply brands the string properties of a query result as `StegaString`, marking them as * potentially containing stega-encoded data. * * String properties that the stega encoder is guaranteed to never encode keep their plain type: * - keys starting with `_` (`_id`, `_type`, `_createdAt`, `_updatedAt`, `_key`, `_ref`, `_rev` and * so on), which also preserves discriminated union narrowing on `_type` * - `slug.current` patterns: a `current` key directly under a `slug` key, as well as string valued * `slug` keys (covering `"slug": slug.current` projections) * - Portable Text internals: the encoder only visits `children` of `_type: 'block'` objects and * `text` of `_type: 'span'` objects, so properties like `style`, `listItem` and `marks` stay * plain * - symbol-keyed properties (like TypeGen's `internalGroqTypeReferenceTo` reference marker): JSON * can't contain symbol keys, so they only exist in the type system and are never encoded * * Every other string is assumed to be "poisoned", even if the runtime `filter` happens to skip it * (URLs, dates, keys ending in `Id`, denylisted keys). Being conservative is safe: the worst case * is a redundant `stegaClean()` call, whereas under-branding would hide real bugs. * * The `Key` and `ParentKey` type parameters track the key the current value is found under, and * the key of the object containing it, they're only used internally during recursion. * * @beta */ export type StegaBranded< T, Key extends PropertyKey = number, ParentKey extends PropertyKey = number, > = 0 extends 1 & T ? T : T extends string ? T extends {readonly ' stegaBrand': string} ? T : Key extends `_${string}` | 'slug' | symbol ? T : Key extends 'current' ? ParentKey extends 'slug' ? T : StegaString<T> : StegaString<T> : T extends number | bigint | boolean | null | undefined ? T : T extends Date | RegExp | ((...args: never[]) => unknown) ? T : T extends readonly unknown[] ? {[Index in keyof T]: StegaBranded<T[Index], number, number>} : T extends {_type: 'block'} ? {[K in keyof T]: K extends 'children' ? StegaBranded<T[K], K, Key> : T[K]} : T extends {_type: 'span'} ? {[K in keyof T]: K extends 'text' ? StegaBranded<T[K], K, Key> : T[K]} : T extends object ? {[K in keyof T]: StegaBranded<T[K], K, Key>} : T /** * Drop-in replacement for `ClientReturn` that brands the result strings as `StegaString`, for use * as the first generic of `client.fetch` when stega is enabled: * ```ts * import {createClient} from '@sanity/client' * import type {ClientReturnStega} from '@sanity/client/stega' * * const query = '*[_type == "post"][0]' * const post = await client.fetch<ClientReturnStega<typeof query>>(query) * ``` * When the query is registered in the `SanityQueries` interface (for example by `sanity typegen`), * the result type is looked up and deeply branded with `StegaBranded`. Otherwise it falls back to * `Fallback`, which defaults to `any`, just like `ClientReturn`. * * @beta */ export type ClientReturnStega<GroqString extends string, Fallback = Any> = StegaBranded< ClientReturn<GroqString, Fallback> > /** * Marks strings in an already fetched query result as potentially containing stega-encoded data, * by re-typing them as `StegaString`. Comparing branded strings to string literals is a type error * until they're cleaned with `stegaClean()`, which recovers the original type. * * This is an identity function, it only changes the type of the input, not its value. * Prefer passing `ClientReturnStega` as the first generic to `client.fetch` when possible, and use * this function for data that has already been fetched. * * @beta */ export function stegaBrand<Result>(result: Result): StegaBranded<Result> { return result as StegaBranded<Result> }