@sanity/client 7.23.2 → 7.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,111 @@
1
+ import type {Any, ClientReturn} from '@sanity/client'
2
+
3
+ /**
4
+ * A string that might contain stega-encoded data, and is unsafe to compare against string literals
5
+ * until it's been cleaned with `stegaClean()`.
6
+ *
7
+ * The type intersects a template literal that has a human readable suffix, with a brand property
8
+ * that holds the original string type:
9
+ * - The suffix makes TypeScript report "This comparison appears to be unintentional" (TS2367) when
10
+ * the string is compared to a literal (`===`, `switch` cases), and makes it unassignable to
11
+ * literal unions like `'left' | 'right'`, while keeping it assignable to `string` so rendering,
12
+ * interpolation and string methods keep working. The suffix is a type-level fiction, the runtime
13
+ * value never ends with that text.
14
+ * - The brand property preserves the original type so `stegaClean()` can recover it exactly. It's
15
+ * a string-keyed property rather than a `unique symbol` so that branded types stay structurally
16
+ * compatible across duplicated copies of `@sanity/client` in `node_modules`. It only exists in
17
+ * the type system and is never present at runtime.
18
+ *
19
+ * @beta
20
+ */
21
+ export type StegaString<T extends string = string> =
22
+ `${T} (may contain hidden stega characters)` & {
23
+ readonly ' stegaBrand': T
24
+ }
25
+
26
+ /**
27
+ * Deeply brands the string properties of a query result as `StegaString`, marking them as
28
+ * potentially containing stega-encoded data.
29
+ *
30
+ * String properties that the stega encoder is guaranteed to never encode keep their plain type:
31
+ * - keys starting with `_` (`_id`, `_type`, `_createdAt`, `_updatedAt`, `_key`, `_ref`, `_rev` and
32
+ * so on), which also preserves discriminated union narrowing on `_type`
33
+ * - `slug.current` patterns: a `current` key directly under a `slug` key, as well as string valued
34
+ * `slug` keys (covering `"slug": slug.current` projections)
35
+ * - Portable Text internals: the encoder only visits `children` of `_type: 'block'` objects and
36
+ * `text` of `_type: 'span'` objects, so properties like `style`, `listItem` and `marks` stay
37
+ * plain
38
+ *
39
+ * Every other string is assumed to be "poisoned", even if the runtime `filter` happens to skip it
40
+ * (URLs, dates, keys ending in `Id`, denylisted keys). Being conservative is safe: the worst case
41
+ * is a redundant `stegaClean()` call, whereas under-branding would hide real bugs.
42
+ *
43
+ * The `Key` and `ParentKey` type parameters track the key the current value is found under, and
44
+ * the key of the object containing it, they're only used internally during recursion.
45
+ *
46
+ * @beta
47
+ */
48
+ export type StegaBranded<
49
+ T,
50
+ Key extends PropertyKey = number,
51
+ ParentKey extends PropertyKey = number,
52
+ > = 0 extends 1 & T
53
+ ? T
54
+ : T extends string
55
+ ? T extends {readonly ' stegaBrand': string}
56
+ ? T
57
+ : Key extends `_${string}` | 'slug'
58
+ ? T
59
+ : Key extends 'current'
60
+ ? ParentKey extends 'slug'
61
+ ? T
62
+ : StegaString<T>
63
+ : StegaString<T>
64
+ : T extends number | bigint | boolean | null | undefined
65
+ ? T
66
+ : T extends Date | RegExp | ((...args: never[]) => unknown)
67
+ ? T
68
+ : T extends readonly unknown[]
69
+ ? {[Index in keyof T]: StegaBranded<T[Index], number, number>}
70
+ : T extends {_type: 'block'}
71
+ ? {[K in keyof T]: K extends 'children' ? StegaBranded<T[K], K, Key> : T[K]}
72
+ : T extends {_type: 'span'}
73
+ ? {[K in keyof T]: K extends 'text' ? StegaBranded<T[K], K, Key> : T[K]}
74
+ : T extends object
75
+ ? {[K in keyof T]: StegaBranded<T[K], K, Key>}
76
+ : T
77
+
78
+ /**
79
+ * Drop-in replacement for `ClientReturn` that brands the result strings as `StegaString`, for use
80
+ * as the first generic of `client.fetch` when stega is enabled:
81
+ * ```ts
82
+ * import {createClient} from '@sanity/client'
83
+ * import type {ClientReturnStega} from '@sanity/client/stega'
84
+ *
85
+ * const query = '*[_type == "post"][0]'
86
+ * const post = await client.fetch<ClientReturnStega<typeof query>>(query)
87
+ * ```
88
+ * When the query is registered in the `SanityQueries` interface (for example by `sanity typegen`),
89
+ * the result type is looked up and deeply branded with `StegaBranded`. Otherwise it falls back to
90
+ * `Fallback`, which defaults to `any`, just like `ClientReturn`.
91
+ *
92
+ * @beta
93
+ */
94
+ export type ClientReturnStega<GroqString extends string, Fallback = Any> = StegaBranded<
95
+ ClientReturn<GroqString, Fallback>
96
+ >
97
+
98
+ /**
99
+ * Marks strings in an already fetched query result as potentially containing stega-encoded data,
100
+ * by re-typing them as `StegaString`. Comparing branded strings to string literals is a type error
101
+ * until they're cleaned with `stegaClean()`, which recovers the original type.
102
+ *
103
+ * This is an identity function, it only changes the type of the input, not its value.
104
+ * Prefer passing `ClientReturnStega` as the first generic to `client.fetch` when possible, and use
105
+ * this function for data that has already been fetched.
106
+ *
107
+ * @beta
108
+ */
109
+ export function stegaBrand<Result>(result: Result): StegaBranded<Result> {
110
+ return result as StegaBranded<Result>
111
+ }
@@ -1,12 +1,33 @@
1
1
  import {vercelStegaClean} from '@vercel/stega'
2
2
 
3
+ /**
4
+ * The result of removing stega-encoded data from a value with `stegaClean()`: strings that were
5
+ * branded as `StegaString` are returned to their original type, everything else is left as-is.
6
+ * @public
7
+ */
8
+ export type StegaCleaned<T> = 0 extends 1 & T
9
+ ? T
10
+ : T extends {readonly ' stegaBrand': infer Original extends string}
11
+ ? Original
12
+ : T extends string | number | bigint | boolean | null | undefined
13
+ ? T
14
+ : T extends Date | RegExp | ((...args: never[]) => unknown)
15
+ ? T
16
+ : T extends readonly unknown[]
17
+ ? {[Index in keyof T]: StegaCleaned<T[Index]>}
18
+ : T extends object
19
+ ? {[K in keyof T]: StegaCleaned<T[K]>}
20
+ : T
21
+
3
22
  /**
4
23
  * Can take a `result` JSON from a `const {result} = client.fetch(query, params, {filterResponse: false})`
5
24
  * and remove all stega-encoded data from it.
25
+ * If the result type has strings branded as `StegaString` (by `ClientReturnStega` or `stegaBrand()`),
26
+ * the brand is stripped and the original string type is restored.
6
27
  * @public
7
28
  */
8
- export function stegaClean<Result = unknown>(result: Result): Result {
9
- return vercelStegaClean<Result>(result)
29
+ export function stegaClean<Result = unknown>(result: Result): StegaCleaned<Result> {
30
+ return vercelStegaClean(result) as StegaCleaned<Result>
10
31
  }
11
32
 
12
33
  /**
package/src/types.ts CHANGED
@@ -55,6 +55,18 @@ export type ClientPerspective =
55
55
  | 'raw'
56
56
  | StackablePerspective[]
57
57
 
58
+ /**
59
+ * @public
60
+ * @beta
61
+ */
62
+ export type ClientVariantConditions = Record<string, string>
63
+
64
+ /**
65
+ * @public
66
+ * @beta
67
+ */
68
+ export type ClientVariant = ClientVariantConditions | string
69
+
58
70
  type ClientConfigResource =
59
71
  | {
60
72
  type: 'canvas'
@@ -111,6 +123,10 @@ export interface ClientConfig {
111
123
  * @defaultValue 'published'
112
124
  */
113
125
  perspective?: ClientPerspective
126
+ /**
127
+ * @beta
128
+ */
129
+ variant?: ClientVariant
114
130
  apiHost?: string
115
131
 
116
132
  /**
@@ -484,6 +500,10 @@ export interface RequestObservableOptions extends Omit<RequestOptions, 'url'> {
484
500
  returnQuery?: boolean
485
501
  resultSourceMap?: boolean | 'withKeyArraySelector'
486
502
  perspective?: ClientPerspective
503
+ /**
504
+ * @beta
505
+ */
506
+ variant?: ClientVariant
487
507
  lastLiveEventId?: string
488
508
  cacheMode?: 'noStale'
489
509
  }
@@ -679,6 +699,8 @@ export interface QueryParams {
679
699
  /** @deprecated you're using a fetch option as a GROQ parameter, this is likely a mistake */
680
700
  perspective?: never
681
701
  /** @deprecated you're using a fetch option as a GROQ parameter, this is likely a mistake */
702
+ variant?: never
703
+ /** @deprecated you're using a fetch option as a GROQ parameter, this is likely a mistake */
682
704
  query?: never
683
705
  /** @deprecated you're using a fetch option as a GROQ parameter, this is likely a mistake */
684
706
  resultSourceMap?: never
@@ -1424,6 +1446,10 @@ export interface ResumableListenOptions extends Omit<ListenOptions, 'events' | '
1424
1446
  /** @public */
1425
1447
  export interface ResponseQueryOptions extends RequestOptions {
1426
1448
  perspective?: ClientPerspective
1449
+ /**
1450
+ * @beta
1451
+ */
1452
+ variant?: ClientVariant
1427
1453
  resultSourceMap?: boolean | 'withKeyArraySelector'
1428
1454
  returnQuery?: boolean
1429
1455
  useCdn?: boolean
@@ -3096,6 +3096,7 @@ ${selectionOpts}`);
3096
3096
  tag,
3097
3097
  returnQuery,
3098
3098
  perspective: options.perspective,
3099
+ variant: options.variant,
3099
3100
  resultSourceMap: options.resultSourceMap,
3100
3101
  lastLiveEventId: Array.isArray(lastLiveEventId) ? lastLiveEventId[0] : lastLiveEventId,
3101
3102
  cacheMode,
@@ -3143,7 +3144,25 @@ ${selectionOpts}`);
3143
3144
  perspective: Array.isArray(perspectiveOption) ? perspectiveOption.join(",") : perspectiveOption,
3144
3145
  ...options.query
3145
3146
  }, (Array.isArray(perspectiveOption) && perspectiveOption.length > 0 || // previewDrafts was renamed to drafts, but keep for backwards compat
3146
- perspectiveOption === "previewDrafts" || perspectiveOption === "drafts") && useCdn && (useCdn = false, printCdnPreviewDraftsWarning())), options.lastLiveEventId && (options.query = { ...options.query, lastLiveEventId: options.lastLiveEventId }), options.returnQuery === false && (options.query = { returnQuery: "false", ...options.query }), useCdn && options.cacheMode == "noStale" && (options.query = { cacheMode: "noStale", ...options.query });
3147
+ perspectiveOption === "previewDrafts" || perspectiveOption === "drafts") && useCdn && (useCdn = false, printCdnPreviewDraftsWarning()));
3148
+ const variantOption = options.variant || config.variant;
3149
+ if (typeof variantOption < "u" && (typeof variantOption == "string" && (options.query = {
3150
+ variant: variantOption,
3151
+ ...options.query
3152
+ }), typeof variantOption == "object")) {
3153
+ const variantConditions = Object.entries(variantOption), searchParams = variantConditionPairsToSearchParams(variantConditions).slice(0, 1);
3154
+ if (variantConditions.length > 1) {
3155
+ const formatter = new Intl.ListFormat("en");
3156
+ console.warn(
3157
+ `The Sanity client's beta \`variant\` option currently only supports one condition. Dropped: ${formatter.format(variantConditions.slice(1).map(([subject]) => JSON.stringify(subject)))}.`
3158
+ );
3159
+ }
3160
+ options.query = {
3161
+ ...Object.fromEntries(searchParams),
3162
+ ...options.query
3163
+ };
3164
+ }
3165
+ options.lastLiveEventId && (options.query = { ...options.query, lastLiveEventId: options.lastLiveEventId }), options.returnQuery === false && (options.query = { returnQuery: "false", ...options.query }), useCdn && options.cacheMode == "noStale" && (options.query = { cacheMode: "noStale", ...options.query });
3147
3166
  }
3148
3167
  const reqOptions = requestOptions(
3149
3168
  config,
@@ -3218,6 +3237,12 @@ ${selectionOpts}`);
3218
3237
  throw new Error(`Unsupported resource type: ${type.toString()}`);
3219
3238
  }
3220
3239
  };
3240
+ function variantConditionPairsToSearchParams(variantConditionPairs) {
3241
+ return variantConditionPairs.map(([condition, value]) => [
3242
+ "variantCondition",
3243
+ `${condition}:${value}`
3244
+ ]);
3245
+ }
3221
3246
  function _generate(client, httpRequest, request) {
3222
3247
  const dataset2 = hasDataset(client.config());
3223
3248
  return _request(client, httpRequest, {