@sanity/client
Version:
Client for retrieving, creating and patching data from Sanity.io
114 lines (109 loc) • 5.28 kB
text/typescript
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>
}