@sanity/sdk 3.0.0 → 3.2.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.
Files changed (132) hide show
  1. package/dist/_exports/_internal.d.ts +72 -19
  2. package/dist/_exports/_internal.d.ts.map +1 -1
  3. package/dist/_exports/_internal.js +130 -11
  4. package/dist/_exports/_internal.js.map +1 -1
  5. package/dist/_exports/agent.d.ts +7 -7
  6. package/dist/_exports/agent.d.ts.map +1 -1
  7. package/dist/_exports/agent.js +1 -1
  8. package/dist/_exports/comlink.d.ts +9 -9
  9. package/dist/_exports/comlink.d.ts.map +1 -1
  10. package/dist/_exports/comlink.js +2 -2
  11. package/dist/_exports/dashboard.d.ts +2 -0
  12. package/dist/_exports/dashboard.js +2 -0
  13. package/dist/{authStore-C0o1NOxW.d.ts → authStore-BO2vch3I.d.ts} +516 -63
  14. package/dist/authStore-BO2vch3I.d.ts.map +1 -0
  15. package/dist/{authStore-CtE9bLHW.js → clientStore-BI4D8HhJ.js} +390 -15
  16. package/dist/clientStore-BI4D8HhJ.js.map +1 -0
  17. package/dist/{createSanityInstance-DUI1bGIg.d.ts → createSanityInstance-BQIqyGLX.d.ts} +17 -2
  18. package/dist/createSanityInstance-BQIqyGLX.d.ts.map +1 -0
  19. package/dist/{createStateSourceAction-BOKtqFNm.js → createStateSourceAction-DdAhEfnZ.js} +7 -3
  20. package/dist/createStateSourceAction-DdAhEfnZ.js.map +1 -0
  21. package/dist/defineProperty-Bho9Ngsi.js +31 -0
  22. package/dist/{getNodeState-CR3__B2D.js → getNodeState-B4nG0sjm.js} +2 -2
  23. package/dist/{getNodeState-CR3__B2D.js.map → getNodeState-B4nG0sjm.js.map} +1 -1
  24. package/dist/index.d.ts +3 -3
  25. package/dist/index.js +370 -90
  26. package/dist/index.js.map +1 -1
  27. package/dist/topicStore-Ct7IFNtY.js +565 -0
  28. package/dist/topicStore-Ct7IFNtY.js.map +1 -0
  29. package/dist/topicStore-D0WqhQGF.d.ts +435 -0
  30. package/dist/topicStore-D0WqhQGF.d.ts.map +1 -0
  31. package/dist/{queryStore-C7yMHPUI.js → version-BcRmCiYi.js} +48 -16
  32. package/dist/version-BcRmCiYi.js.map +1 -0
  33. package/package.json +19 -18
  34. package/src/_exports/_internal.ts +13 -0
  35. package/src/_exports/dashboard.ts +36 -0
  36. package/src/_exports/index.ts +17 -1
  37. package/src/access/checkPermissions.test.ts +1 -1
  38. package/src/access/checkPermissions.ts +1 -1
  39. package/src/applications/applications.test-d.ts +28 -4
  40. package/src/applications/applications.test.ts +4 -4
  41. package/src/applications/applications.ts +43 -18
  42. package/src/auth/authMode.test.ts +24 -0
  43. package/src/auth/authMode.ts +8 -3
  44. package/src/auth/authStore.ts +12 -0
  45. package/src/auth/authStrategy.ts +2 -0
  46. package/src/auth/handleAuthCallback.test.ts +2 -2
  47. package/src/auth/handleAuthCallback.ts +1 -1
  48. package/src/auth/logout.test.ts +46 -1
  49. package/src/auth/logout.ts +2 -1
  50. package/src/auth/oauth/oauthActions.test.ts +457 -0
  51. package/src/auth/oauth/oauthActions.ts +389 -0
  52. package/src/auth/oauth/oauthAuth.test.ts +206 -0
  53. package/src/auth/oauth/oauthAuth.ts +168 -0
  54. package/src/auth/oauth/pkce.test.ts +44 -0
  55. package/src/auth/oauth/pkce.ts +60 -0
  56. package/src/auth/oauth/types.ts +12 -0
  57. package/src/auth/refreshStampedToken.ts +1 -1
  58. package/src/auth/studioModeAuth.test.ts +1 -1
  59. package/src/auth/studioModeAuth.ts +1 -1
  60. package/src/auth/subscribeToStateAndFetchCurrentUser.test.ts +2 -2
  61. package/src/auth/subscribeToStateAndFetchCurrentUser.ts +1 -1
  62. package/src/comments/addonDatasetStore.test.ts +2 -2
  63. package/src/comments/addonDatasetStore.ts +2 -2
  64. package/src/comments/commentActions.ts +6 -5
  65. package/src/comments/commentsStore.ts +4 -4
  66. package/src/config/authConfig.ts +18 -0
  67. package/src/dashboard/messageBus/__fixtures__/test-topics.ts +18 -0
  68. package/src/dashboard/messageBus/bus.test.ts +1033 -0
  69. package/src/dashboard/messageBus/bus.ts +1246 -0
  70. package/src/dashboard/messageBus/store.test.ts +142 -0
  71. package/src/dashboard/messageBus/store.ts +74 -0
  72. package/src/dashboard/messageBus/topicStore.test.ts +172 -0
  73. package/src/dashboard/messageBus/topicStore.ts +184 -0
  74. package/src/dashboard/messageBus/topics.test.ts +71 -0
  75. package/src/dashboard/messageBus/topics.ts +260 -0
  76. package/src/document/actions.ts +3 -3
  77. package/src/document/applyDocumentActions.ts +5 -9
  78. package/src/document/documentStore.concurrency.test.ts +2 -1
  79. package/src/document/documentStore.test.ts +19 -18
  80. package/src/document/documentStore.ts +17 -17
  81. package/src/document/permissions.ts +2 -1
  82. package/src/document/processActions.test.ts +2 -1
  83. package/src/document/processMutations.ts +61 -24
  84. package/src/document/reducers.ts +2 -2
  85. package/src/installations/installations.test-d.ts +3 -3
  86. package/src/installations/installations.test.ts +2 -2
  87. package/src/installations/installations.ts +4 -21
  88. package/src/organization/organization.test.ts +2 -2
  89. package/src/organization/organization.ts +1 -1
  90. package/src/organizations/organizations.test.ts +2 -2
  91. package/src/organizations/organizations.ts +1 -1
  92. package/src/presence/presenceStore.test.ts +15 -22
  93. package/src/presence/presenceStore.ts +3 -2
  94. package/src/project/project.test.ts +3 -3
  95. package/src/project/project.ts +1 -1
  96. package/src/projection/getProjectionState.test.ts +6 -4
  97. package/src/projection/getProjectionState.ts +4 -4
  98. package/src/projection/resolveProjection.ts +2 -2
  99. package/src/projects/projects.test.ts +2 -2
  100. package/src/projects/projects.ts +1 -1
  101. package/src/query/queryStore.ts +6 -6
  102. package/src/store/createSanityInstance.test.ts +1 -1
  103. package/src/store/createSanityInstance.ts +4 -4
  104. package/src/store/createStoreInstance.test.ts +0 -5
  105. package/src/telemetry/telemetryManager.test.ts +1 -1
  106. package/src/telemetry/telemetryManager.ts +2 -2
  107. package/src/typegen/defineProjection.test.ts +17 -0
  108. package/src/typegen/defineProjection.ts +19 -0
  109. package/src/typegen/groqCompat.ts +122 -0
  110. package/src/typegen/resolve.ts +37 -0
  111. package/src/users/usersStore.test.ts +1 -1
  112. package/src/users/usersStore.ts +5 -5
  113. package/src/utils/createGroqSearchFilter.test.ts +8 -0
  114. package/src/utils/createGroqSearchFilter.ts +6 -3
  115. package/src/utils/ids.test.ts +40 -15
  116. package/src/utils/ids.ts +47 -2
  117. package/dist/authStore-C0o1NOxW.d.ts.map +0 -1
  118. package/dist/authStore-CtE9bLHW.js.map +0 -1
  119. package/dist/clientStore-BtE2-q2i.js +0 -90
  120. package/dist/clientStore-BtE2-q2i.js.map +0 -1
  121. package/dist/createSanityInstance-DUI1bGIg.d.ts.map +0 -1
  122. package/dist/createStateSourceAction-BOKtqFNm.js.map +0 -1
  123. package/dist/getEnv-D5RGkQLu.js +0 -8
  124. package/dist/getEnv-D5RGkQLu.js.map +0 -1
  125. package/dist/logger-DAtJb5RQ.js +0 -173
  126. package/dist/logger-DAtJb5RQ.js.map +0 -1
  127. package/dist/queryStore-C7yMHPUI.js.map +0 -1
  128. package/dist/rolldown-runtime-vyAXikos.js +0 -9
  129. package/dist/telemetryManager-UIEiMtC0.js +0 -127
  130. package/dist/telemetryManager-UIEiMtC0.js.map +0 -1
  131. package/dist/version-D8RP_Q59.js +0 -9
  132. package/dist/version-D8RP_Q59.js.map +0 -1
@@ -5,6 +5,7 @@ import {
5
5
  type SanityDocument,
6
6
  } from '@sanity/types'
7
7
 
8
+ import {randomUuid} from '../utils/ids'
8
9
  import {
9
10
  dec,
10
11
  diffMatchPatch,
@@ -17,10 +18,9 @@ import {
17
18
  } from './patchOperations'
18
19
 
19
20
  /**
20
- * Represents a set of document that will go into `applyMutations`. Before
21
- * applying a mutation, it's expected that all relevant documents that the
22
- * mutations affect are included, including those that do not exist yet.
23
- * Documents that don't exist have a `null` value.
21
+ * Maps document IDs to documents, using `null` for documents that do not exist.
22
+ *
23
+ * @beta
24
24
  */
25
25
  export type DocumentSet<TDocument extends SanityDocument = SanityDocument> = {
26
26
  [TDocumentId in string]?: TDocument | null
@@ -28,10 +28,29 @@ export type DocumentSet<TDocument extends SanityDocument = SanityDocument> = {
28
28
 
29
29
  type SupportedPatchOperation = Exclude<keyof PatchOperations, 'merge'>
30
30
 
31
- // > If multiple patches are included, then the order of execution is as follows:
32
- // > - set, setIfMissing, unset, inc, dec, insert.
33
- // > https://www.sanity.io/docs/http-mutations#5b4db1396e56
34
- const patchOperations = {
31
+ /**
32
+ * Exposes the patch operations used by {@link processMutations}.
33
+ *
34
+ * @remarks
35
+ * Property order determines execution order in {@link processMutations}:
36
+ * `ifRevisionID`, `set`, `setIfMissing`, `unset`, `inc`, `dec`, `insert`,
37
+ * then `diffMatchPatch`. Reordering the properties changes mutation results.
38
+ * Freezes the collection to prevent callers from replacing the functions
39
+ * used by the SDK's mutation evaluator.
40
+ *
41
+ * @see https://www.sanity.io/docs/http-mutations#5b4db1396e56
42
+ *
43
+ * @example
44
+ * ```ts
45
+ * const document = {items: [{_key: 'a', title: 'Before'}]}
46
+ * const updated = patchOperations.set<typeof document>(document, {
47
+ * 'items[_key=="a"].title': 'After',
48
+ * })
49
+ * ```
50
+ *
51
+ * @internal
52
+ */
53
+ export const patchOperations = Object.freeze({
35
54
  ifRevisionID,
36
55
  set,
37
56
  setIfMissing,
@@ -45,7 +64,7 @@ const patchOperations = {
45
64
  input: unknown,
46
65
  pathExpressions: NonNullable<PatchOperations[K]>,
47
66
  ) => unknown
48
- }
67
+ })
49
68
 
50
69
  /**
51
70
  * Implements ID generation:
@@ -63,12 +82,17 @@ const patchOperations = {
63
82
  * [- source](https://www.sanity.io/docs/http-mutations#c732f27330a4)
64
83
  */
65
84
  export function getId(id?: string): string {
66
- if (!id || typeof id !== 'string') return crypto.randomUUID()
67
- if (id.endsWith('.')) return `${id}${crypto.randomUUID()}`
85
+ if (!id || typeof id !== 'string') return randomUuid()
86
+ if (id.endsWith('.')) return `${id}${randomUuid()}`
68
87
  return id
69
88
  }
70
89
 
71
- interface ProcessMutationsOptions {
90
+ /**
91
+ * Configures {@link processMutations}.
92
+ *
93
+ * @internal
94
+ */
95
+ export interface ProcessMutationsOptions {
72
96
  /**
73
97
  * The transaction ID that will become the next `_rev` for documents mutated
74
98
  * by the given mutations.
@@ -83,8 +107,8 @@ interface ProcessMutationsOptions {
83
107
  */
84
108
  mutations: Mutation[]
85
109
  /**
86
- * An optional timestamp that will be used for `_createdAt` and `_updatedAt`
87
- * timestamp when applicable.
110
+ * Supplies the default `_createdAt` and `_updatedAt` timestamps.
111
+ * Uses the current time when omitted or empty.
88
112
  */
89
113
  timestamp?: string
90
114
  }
@@ -107,18 +131,31 @@ export function getDocumentIds(selection: MutationSelection): string[] {
107
131
  }
108
132
 
109
133
  /**
110
- * Applies the given mutation to the given document set. Note, it is expected
111
- * that all relevant documents that the mutations affect should be within the
112
- * given `document` set. If a document does not exist, it should have the value
113
- * `null`. If a document is deleted as a result of the mutations, it will still
114
- * have its document ID present in the returns documents, but it will have a
115
- * value of `null`.
134
+ * Applies mutations to an in-memory set of Sanity documents.
135
+ * Deleted documents remain in the returned set with a value of `null`.
136
+ *
137
+ * Include every document affected by `mutations` in `documents`, using `null`
138
+ * for documents that do not exist.
139
+ *
140
+ * Returns `documents` itself when `mutations` is empty. Otherwise, returns a
141
+ * shallow copy of that object. The result can reuse document objects and
142
+ * nested objects from `documents`. Mutating a reused object through the
143
+ * result also changes the input.
144
+ *
145
+ * Sets `_rev` to `transactionId` on created, replaced, and patched documents.
146
+ * Uses `timestamp` for timestamps unless the mutation preserves them.
147
+ * Ignores `merge` patches; `inc` and `dec` skip values that are not numbers.
148
+ * Generates document IDs when omitted or ending in `.`.
149
+ * Patch operations can generate missing array keys.
150
+ *
151
+ * Does not persist documents or run Content Lake's server validation.
116
152
  *
117
- * The given `transactionId` will be used as the resulting `_rev` for documents
118
- * affected by the given set of mutations.
153
+ * @throws If a selection uses a query, a `create` targets an existing document,
154
+ * or a `patch` targets a document that does not exist.
155
+ * @throws If a patch operation fails, including a revision check failure or
156
+ * `diffMatchPatch` applied to a value other than a string or `undefined`.
119
157
  *
120
- * If a `timestamp` is given, that will be used as for the relevant `_updatedAt`
121
- * and `_createdAt` timestamps.
158
+ * @internal
122
159
  */
123
160
  export function processMutations({
124
161
  documents,
@@ -4,7 +4,7 @@ import {type Mutation, type PatchOperations, type SanityDocumentLike} from '@san
4
4
  import {type DocumentHandle} from '../config/sanityConfig'
5
5
  import {isReleasePerspective} from '../releases/utils/isReleasePerspective'
6
6
  import {type StoreContext} from '../store/defineStore'
7
- import {insecureRandomId} from '../utils/ids'
7
+ import {randomId} from '../utils/ids'
8
8
  import {isDeepEqual, omitProperty} from '../utils/object'
9
9
  import {setCleanupTimeout} from '../utils/setCleanupTimeout'
10
10
  import {type Action} from './actions'
@@ -790,7 +790,7 @@ export function manageSubscriberIds(
790
790
  ): () => void {
791
791
  const documentIds = getDocumentIdsFromHandleLikes(handles)
792
792
 
793
- const subscriptionId = insecureRandomId()
793
+ const subscriptionId = randomId(16)
794
794
  state.set('addSubscribers', (prev) =>
795
795
  documentIds.reduce(
796
796
  (acc, id) => addSubscriptionIdToDocument(acc, id, subscriptionId),
@@ -1,5 +1,6 @@
1
1
  import {expectTypeOf, test} from 'vitest'
2
2
 
3
+ import {type ApplicationInterface} from '../applications/applications'
3
4
  import {type SanityInstance} from '../store/createSanityInstance'
4
5
  import {type StateSource} from '../store/createStateSourceAction'
5
6
  import {type FetcherSnapshot} from '../store/fetcherStore'
@@ -10,7 +11,6 @@ import {
10
11
  type InstallationActiveConfig,
11
12
  type InstallationBase,
12
13
  type InstallationInclude,
13
- type InstallationInterface,
14
14
  installations,
15
15
  type InstallationsResponse,
16
16
  } from './installations'
@@ -40,7 +40,7 @@ test('Installation — each token adds its top-level field, required, others abs
40
40
  Extract<keyof Installation<'access'>, 'activeConfig' | 'interfaces'>
41
41
  >().toEqualTypeOf<never>()
42
42
 
43
- expectTypeOf<Installation<'interfaces'>['interfaces']>().toEqualTypeOf<InstallationInterface[]>()
43
+ expectTypeOf<Installation<'interfaces'>['interfaces']>().toEqualTypeOf<ApplicationInterface[]>()
44
44
  expectTypeOf<
45
45
  Installation<'activeConfig'>['activeConfig']
46
46
  >().toEqualTypeOf<InstallationActiveConfig | null>()
@@ -49,7 +49,7 @@ test('Installation — each token adds its top-level field, required, others abs
49
49
  test('Installation — multiple tokens add each field', () => {
50
50
  type Both = Installation<'access' | 'interfaces'>
51
51
  expectTypeOf<Both['access']>().toEqualTypeOf<InstallationAccess[]>()
52
- expectTypeOf<Both['interfaces']>().toEqualTypeOf<InstallationInterface[]>()
52
+ expectTypeOf<Both['interfaces']>().toEqualTypeOf<ApplicationInterface[]>()
53
53
  expectTypeOf<Extract<keyof Both, 'activeConfig'>>().toEqualTypeOf<never>()
54
54
  })
55
55
 
@@ -40,7 +40,7 @@ describe('installations', () => {
40
40
  scope: 'global',
41
41
  })
42
42
  expect(request).toHaveBeenCalledWith({
43
- uri: '/installations',
43
+ url: '/installations',
44
44
  query: {organizationId: 'org1', include: 'interfaces'},
45
45
  tag: 'installations.list',
46
46
  })
@@ -54,7 +54,7 @@ describe('installations', () => {
54
54
 
55
55
  expect(result).toEqual(inst)
56
56
  expect(request).toHaveBeenCalledWith({
57
- uri: '/installations/inst1',
57
+ url: '/installations/inst1',
58
58
  query: {},
59
59
  tag: 'installations.get',
60
60
  })
@@ -1,5 +1,6 @@
1
1
  import {switchMap} from 'rxjs'
2
2
 
3
+ import {type ApplicationInterface} from '../applications/applications'
3
4
  import {getClientState} from '../client/clientStore'
4
5
  import {defineFetcher} from '../store/fetcherStore'
5
6
  import {buildQuery} from '../utils/buildQuery'
@@ -29,24 +30,6 @@ export interface InstallationAccess {
29
30
  resourceId: string
30
31
  }
31
32
 
32
- /**
33
- * An interface exposed by an installation, embedded when `interfaces` is
34
- * included.
35
- *
36
- * @see https://www.sanity.io/docs/http-reference/applications-api
37
- * @public
38
- */
39
- export interface InstallationInterface {
40
- id: string
41
- type: 'app' | 'worker' | 'asset_source' | 'panel'
42
- name: string
43
- title: string
44
- version: string
45
- /** Module federation module ID; resolved from the host mf-manifest at runtime */
46
- moduleId: string
47
- metadata: {group?: string; priority?: number} | null
48
- }
49
-
50
33
  /**
51
34
  * The active config of an installation, embedded when `activeConfig` is
52
35
  * included. `null` when nothing is deployed.
@@ -104,7 +87,7 @@ export type Installation<Include extends InstallationInclude = never> = Installa
104
87
  {activeConfig: InstallationActiveConfig | null}
105
88
  > &
106
89
  Included<InstallationInclude, 'access', Include, {access: InstallationAccess[]}> &
107
- Included<InstallationInclude, 'interfaces', Include, {interfaces: InstallationInterface[]}>
90
+ Included<InstallationInclude, 'interfaces', Include, {interfaces: ApplicationInterface[]}>
108
91
 
109
92
  /**
110
93
  * Options for listing an organization's installations.
@@ -160,7 +143,7 @@ export const installations = defineFetcher<
160
143
  getClientState(instance, {apiVersion: API_VERSION, scope: 'global'}).observable.pipe(
161
144
  switchMap((client) =>
162
145
  client.observable.request<InstallationsResponse<InstallationInclude>>({
163
- uri: '/installations',
146
+ url: '/installations',
164
147
  query: buildQuery({
165
148
  organizationId: options.organizationId,
166
149
  include: serializeInclude(options.include),
@@ -196,7 +179,7 @@ export const installation = defineFetcher<
196
179
  getClientState(instance, {apiVersion: API_VERSION, scope: 'global'}).observable.pipe(
197
180
  switchMap((client) =>
198
181
  client.observable.request<Installation<InstallationInclude>>({
199
- uri: `/installations/${installationId}`,
182
+ url: `/installations/${installationId}`,
200
183
  query: buildQuery({include: serializeInclude(options?.include)}),
201
184
  tag: 'installations.get',
202
185
  }),
@@ -35,7 +35,7 @@ describe('organization', () => {
35
35
  const result = await organization.resolveState(instance, {organizationId: 'org_1'})
36
36
  expect(result).toEqual(detail)
37
37
  expect(request).toHaveBeenCalledWith({
38
- uri: '/organizations/org_1',
38
+ url: '/organizations/org_1',
39
39
  query: {includeMembers: 'false', includeFeatures: 'false'},
40
40
  tag: 'organizations.get',
41
41
  })
@@ -58,7 +58,7 @@ describe('organization', () => {
58
58
  })
59
59
 
60
60
  expect(request).toHaveBeenCalledWith({
61
- uri: '/organizations/org_1',
61
+ url: '/organizations/org_1',
62
62
  query: {
63
63
  includeMembers: 'true',
64
64
  includeFeatures: 'true',
@@ -128,7 +128,7 @@ export const organization = defineFetcher<
128
128
  getClientState(instance, {apiVersion: API_VERSION, scope: 'global'}).observable.pipe(
129
129
  switchMap((client) =>
130
130
  client.observable.request<Organization<boolean, boolean>>({
131
- uri: `/organizations/${resolveOrganizationId(options)}`,
131
+ url: `/organizations/${resolveOrganizationId(options)}`,
132
132
  query: buildQuery(normalizeOrganizationOptions(options)),
133
133
  tag: 'organizations.get',
134
134
  }),
@@ -35,7 +35,7 @@ describe('organizations', () => {
35
35
  const result = await organizations.resolveState(instance)
36
36
  expect(result).toEqual(list)
37
37
  expect(request).toHaveBeenCalledWith({
38
- uri: '/organizations',
38
+ url: '/organizations',
39
39
  query: {
40
40
  includeImplicitMemberships: 'false',
41
41
  includeMembers: 'false',
@@ -62,7 +62,7 @@ describe('organizations', () => {
62
62
  })
63
63
 
64
64
  expect(request).toHaveBeenCalledWith({
65
- uri: '/organizations',
65
+ url: '/organizations',
66
66
  query: {
67
67
  includeImplicitMemberships: 'true',
68
68
  includeMembers: 'true',
@@ -94,7 +94,7 @@ export const organizations = defineFetcher<
94
94
  getClientState(instance, {apiVersion: API_VERSION, scope: 'global'}).observable.pipe(
95
95
  switchMap((client) =>
96
96
  client.observable.request<Organizations<boolean, boolean>>({
97
- uri: '/organizations',
97
+ url: '/organizations',
98
98
  query: buildQuery(normalizeOrganizationsOptions(options)),
99
99
  tag: 'organizations.list',
100
100
  }),
@@ -24,6 +24,12 @@ vi.mock('../client/clientStore')
24
24
  vi.mock('../users/usersStore')
25
25
  vi.mock('./bifurTransport')
26
26
 
27
+ function lastTransportSessionId(): string {
28
+ const call = vi.mocked(createBifurTransport).mock.calls.at(-1)
29
+ if (!call) throw new Error('createBifurTransport was not called')
30
+ return call[0].sessionId
31
+ }
32
+
27
33
  describe('presenceStore', () => {
28
34
  let instance: SanityInstance
29
35
  let mockClient: SanityClient
@@ -49,13 +55,6 @@ describe('presenceStore', () => {
49
55
  beforeEach(() => {
50
56
  vi.clearAllMocks()
51
57
 
52
- // Mock crypto.randomUUID
53
- Object.defineProperty(global, 'crypto', {
54
- value: {
55
- randomUUID: vi.fn(() => 'test-session-id'),
56
- },
57
- })
58
-
59
58
  mockClient = {
60
59
  withConfig: vi.fn().mockReturnThis(),
61
60
  observable: {
@@ -103,7 +102,7 @@ describe('presenceStore', () => {
103
102
  expect(createBifurTransport).toHaveBeenCalledWith({
104
103
  client: mockClient,
105
104
  token$: expect.any(Object),
106
- sessionId: 'test-session-id',
105
+ sessionId: expect.stringMatching(/^[a-zA-Z0-9]{16}$/),
107
106
  })
108
107
  })
109
108
 
@@ -164,7 +163,7 @@ describe('presenceStore', () => {
164
163
  mockIncomingEvents.next({
165
164
  type: 'state',
166
165
  userId: 'user-1',
167
- sessionId: 'test-session-id', // Same as our session
166
+ sessionId: lastTransportSessionId(), // Same as our session
168
167
  timestamp: '2023-01-01T12:00:00Z',
169
168
  locations: [],
170
169
  })
@@ -314,7 +313,7 @@ describe('presenceStore', () => {
314
313
  getPresence(instance, {resource: canvasResource})
315
314
 
316
315
  expect(mockClient.observable.request).toHaveBeenCalledWith({
317
- uri: '/canvases/canvas123',
316
+ url: '/canvases/canvas123',
318
317
  tag: 'canvases.get',
319
318
  })
320
319
  })
@@ -1050,7 +1049,7 @@ describe('presenceStore', () => {
1050
1049
  mockIncomingEvents.next({
1051
1050
  type: 'rollCall',
1052
1051
  userId: 'user-1',
1053
- sessionId: 'test-session-id',
1052
+ sessionId: lastTransportSessionId(),
1054
1053
  })
1055
1054
  await flush()
1056
1055
 
@@ -1139,25 +1138,19 @@ describe('presenceStore', () => {
1139
1138
  })
1140
1139
 
1141
1140
  describe('session id', () => {
1142
- afterEach(() => {
1143
- vi.unstubAllGlobals()
1144
- })
1145
-
1146
1141
  it('gives each store its own id, so nothing can collide across tabs', () => {
1147
1142
  // Ids are deliberately not persisted: a tab that inherits another's
1148
1143
  // session storage would otherwise reuse the id and the two live clients
1149
1144
  // would filter each other out as their own session.
1150
- let counter = 0
1151
- vi.stubGlobal('crypto', {randomUUID: vi.fn(() => `session-${++counter}`)})
1152
-
1153
1145
  getPresence(instance, {resource: {projectId: 'p1', dataset: 'd1'}}).subscribe(() => {})
1154
- const first = vi.mocked(createBifurTransport).mock.calls.at(-1)?.[0].sessionId
1146
+ const first = lastTransportSessionId()
1155
1147
 
1156
1148
  getPresence(instance, {resource: {projectId: 'p2', dataset: 'd2'}}).subscribe(() => {})
1157
- const second = vi.mocked(createBifurTransport).mock.calls.at(-1)?.[0].sessionId
1149
+ const second = lastTransportSessionId()
1158
1150
 
1159
- expect(first).toBe('session-1')
1160
- expect(second).toBe('session-2')
1151
+ expect(first).toMatch(/^[a-zA-Z0-9]{16}$/)
1152
+ expect(second).toMatch(/^[a-zA-Z0-9]{16}$/)
1153
+ expect(first).not.toBe(second)
1161
1154
  })
1162
1155
  })
1163
1156
  })
@@ -38,6 +38,7 @@ import {
38
38
  import {defineStore, type StoreContext} from '../store/defineStore'
39
39
  import {type SanityUser} from '../users/types'
40
40
  import {getUserState} from '../users/usersStore'
41
+ import {randomId} from '../utils/ids'
41
42
  import {createLogger} from '../utils/logger'
42
43
  import {createBifurTransport} from './bifurTransport'
43
44
  import {startsWithPath} from './paths'
@@ -198,7 +199,7 @@ export const presenceStore = defineStore<PresenceStoreState, BoundResourceKey>({
198
199
  // storage (`window.open`, or a same-origin iframe), making two live clients
199
200
  // filter each other out as self. Stale sessions are handled by the
200
201
  // disconnect on unload, with the expiry sweep above as the backstop.
201
- const sessionId = crypto.randomUUID()
202
+ const sessionId = randomId(16)
202
203
 
203
204
  // Dataset resources must use the project hostname so the socket URL is project-specific.
204
205
  // Canvas resources use the global API endpoint via the resource config.
@@ -411,7 +412,7 @@ export const presenceStore = defineStore<PresenceStoreState, BoundResourceKey>({
411
412
  subscription.add(
412
413
  globalClient.observable
413
414
  .request<{organizationId: string}>({
414
- uri: `/canvases/${resource.canvasId}`,
415
+ url: `/canvases/${resource.canvasId}`,
415
416
  tag: 'canvases.get',
416
417
  })
417
418
  .subscribe({
@@ -36,7 +36,7 @@ describe('project', () => {
36
36
  const result = await project.resolveState(instance, {projectId: 'a'})
37
37
  expect(result).toEqual(detail)
38
38
  expect(request).toHaveBeenCalledWith({
39
- uri: '/projects/a',
39
+ url: '/projects/a',
40
40
  query: {includeMembers: 'true', includeFeatures: 'true'},
41
41
  tag: 'projects.get',
42
42
  })
@@ -59,7 +59,7 @@ describe('project', () => {
59
59
  })
60
60
 
61
61
  expect(request).toHaveBeenCalledWith({
62
- uri: '/projects/a',
62
+ url: '/projects/a',
63
63
  query: {
64
64
  includeMembers: 'false',
65
65
  includeFeatures: 'false',
@@ -80,7 +80,7 @@ describe('project', () => {
80
80
 
81
81
  await project.resolveState(instance)
82
82
  expect(request).toHaveBeenCalledWith({
83
- uri: '/projects/p',
83
+ url: '/projects/p',
84
84
  query: {includeMembers: 'true', includeFeatures: 'true'},
85
85
  tag: 'projects.get',
86
86
  })
@@ -130,7 +130,7 @@ export const project = defineFetcher<
130
130
  getClientState(instance, {apiVersion: API_VERSION, scope: 'global'}).observable.pipe(
131
131
  switchMap((client) =>
132
132
  client.observable.request<Project<boolean, boolean>>({
133
- uri: `/projects/${resolveProjectId(instance, options)}`,
133
+ url: `/projects/${resolveProjectId(instance, options)}`,
134
134
  query: buildQuery(normalizeProjectOptions(options)),
135
135
  tag: 'projects.get',
136
136
  }),
@@ -4,7 +4,7 @@ import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'
4
4
  import {createSanityInstance, type SanityInstance} from '../store/createSanityInstance'
5
5
  import {type StoreState} from '../store/createStoreState'
6
6
  import {hashString} from '../utils/hashString'
7
- import {insecureRandomId} from '../utils/ids'
7
+ import {randomId} from '../utils/ids'
8
8
  import {getProjectionState} from './getProjectionState'
9
9
  import {subscribeToStateAndFetchBatches} from './subscribeToStateAndFetchBatches'
10
10
  import {type ProjectionStoreState} from './types'
@@ -19,7 +19,7 @@ vi.mock('../utils/ids', async (importOriginal) => {
19
19
  // Mock implementation uses the external counter
20
20
  return {
21
21
  ...util,
22
- insecureRandomId: vi.fn(() => {
22
+ randomId: vi.fn(() => {
23
23
  const id = `testSubId_${++mockIdCounter}`
24
24
  return id
25
25
  }),
@@ -39,8 +39,7 @@ describe('getProjectionState', () => {
39
39
  let state: StoreState<ProjectionStoreState & {extra?: unknown}>
40
40
 
41
41
  beforeEach(() => {
42
- mockIdCounter = 0
43
- vi.mocked(insecureRandomId).mockClear()
42
+ vi.mocked(randomId).mockClear()
44
43
 
45
44
  // Capture state
46
45
  vi.mocked(subscribeToStateAndFetchBatches).mockImplementation((context) => {
@@ -49,6 +48,9 @@ describe('getProjectionState', () => {
49
48
  })
50
49
 
51
50
  instance = createSanityInstance({projectId: 'exampleProject', dataset: 'exampleDataset'})
51
+ // Reset after instance creation: the instanceId also comes from randomId,
52
+ // but the test ids should only count subscription-related calls
53
+ mockIdCounter = 0
52
54
  vi.useFakeTimers() // Enable fake timers for each test
53
55
  })
54
56
 
@@ -1,5 +1,4 @@
1
1
  import {DocumentId, getPublishedId} from '@sanity/id-utils'
2
- import {type SanityProjectionResult} from 'groq'
3
2
 
4
3
  import {type DocumentHandle} from '../config/sanityConfig'
5
4
  import {bindActionByResourceAndPerspective} from '../store/createActionBinder'
@@ -9,8 +8,9 @@ import {
9
8
  type SelectorContext,
10
9
  type StateSource,
11
10
  } from '../store/createStateSourceAction'
11
+ import {type ResolveProjectionResult} from '../typegen/resolve'
12
12
  import {hashString} from '../utils/hashString'
13
- import {insecureRandomId} from '../utils/ids'
13
+ import {randomId} from '../utils/ids'
14
14
  import {omitProperty} from '../utils/object'
15
15
  import {setCleanupTimeout} from '../utils/setCleanupTimeout'
16
16
  import {projectionStore} from './projectionStore'
@@ -39,7 +39,7 @@ export function getProjectionState<
39
39
  options: ProjectionOptions<TProjection, TDocumentType, TDataset, TProjectId>,
40
40
  ): StateSource<
41
41
  | ProjectionValuePending<
42
- SanityProjectionResult<TProjection, TDocumentType, `${TProjectId}.${TDataset}`>
42
+ ResolveProjectionResult<TProjection, TDocumentType, `${TProjectId}.${TDataset}`>
43
43
  >
44
44
  | undefined
45
45
  >
@@ -85,7 +85,7 @@ export const _getProjectionState = bindActionByResourceAndPerspective(
85
85
  },
86
86
  onSubscribe: ({state}, options: ProjectionOptions<string, string, string, string>) => {
87
87
  const {projection, ...docHandle} = options
88
- const subscriptionId = insecureRandomId()
88
+ const subscriptionId = randomId(16)
89
89
  const documentId = getPublishedId(DocumentId(docHandle.documentId))
90
90
  const validProjection = validateProjection(projection)
91
91
  const projectionHash = hashString(validProjection)
@@ -1,8 +1,8 @@
1
- import {type SanityProjectionResult} from 'groq'
2
1
  import {filter, firstValueFrom} from 'rxjs'
3
2
 
4
3
  import {bindActionByResourceAndPerspective} from '../store/createActionBinder'
5
4
  import {type SanityInstance} from '../store/createSanityInstance'
5
+ import {type ResolveProjectionResult} from '../typegen/resolve'
6
6
  import {getProjectionState, type ProjectionOptions} from './getProjectionState'
7
7
  import {projectionStore} from './projectionStore'
8
8
  import {type ProjectionValuePending} from './types'
@@ -18,7 +18,7 @@ export function resolveProjection<
18
18
  options: ProjectionOptions<TProjection, TDocumentType, TDataset, TProjectId>,
19
19
  ): Promise<
20
20
  ProjectionValuePending<
21
- SanityProjectionResult<TProjection, TDocumentType, `${TProjectId}.${TDataset}`>
21
+ ResolveProjectionResult<TProjection, TDocumentType, `${TProjectId}.${TDataset}`>
22
22
  >
23
23
  >
24
24
 
@@ -35,7 +35,7 @@ describe('projects', () => {
35
35
  const result = await projects.resolveState(instance)
36
36
  expect(result).toEqual(list)
37
37
  expect(request).toHaveBeenCalledWith({
38
- uri: '/projects',
38
+ url: '/projects',
39
39
  query: {includeMembers: 'false', includeFeatures: 'true', onlyExplicitMembership: 'false'},
40
40
  tag: 'projects.list',
41
41
  })
@@ -58,7 +58,7 @@ describe('projects', () => {
58
58
  })
59
59
 
60
60
  expect(request).toHaveBeenCalledWith({
61
- uri: '/projects',
61
+ url: '/projects',
62
62
  query: {
63
63
  organizationId: 'org123',
64
64
  includeMembers: 'true',
@@ -58,7 +58,7 @@ export const projects = defineFetcher<
58
58
  getClientState(instance, {apiVersion: API_VERSION, scope: 'global'}).observable.pipe(
59
59
  switchMap((client) =>
60
60
  client.observable.request<Project<boolean, boolean>[]>({
61
- uri: '/projects',
61
+ url: '/projects',
62
62
  query: buildQuery(normalizeProjectsOptions(options)),
63
63
  tag: 'projects.list',
64
64
  }),
@@ -1,5 +1,4 @@
1
1
  import {type ResponseQueryOptions} from '@sanity/client'
2
- import {type SanityQueryResult} from 'groq'
3
2
  import {
4
3
  catchError,
5
4
  combineLatest,
@@ -36,7 +35,8 @@ import {
36
35
  } from '../store/createStateSourceAction'
37
36
  import {type StoreState} from '../store/createStoreState'
38
37
  import {defineStore, type StoreContext} from '../store/defineStore'
39
- import {insecureRandomId} from '../utils/ids'
38
+ import {type ResolveQueryResult} from '../typegen/resolve'
39
+ import {randomId} from '../utils/ids'
40
40
  import {setCleanupTimeout} from '../utils/setCleanupTimeout'
41
41
  import {
42
42
  QUERY_STATE_CLEAR_DELAY,
@@ -278,7 +278,7 @@ export function getQueryState<
278
278
  >(
279
279
  instance: SanityInstance,
280
280
  queryOptions: QueryOptions<TQuery, TDataset, TProjectId>,
281
- ): StateSource<SanityQueryResult<TQuery, `${TProjectId}.${TDataset}`> | undefined>
281
+ ): StateSource<ResolveQueryResult<TQuery, `${TProjectId}.${TDataset}`> | undefined>
282
282
 
283
283
  /** @beta */
284
284
  export function getQueryState<TData>(
@@ -309,7 +309,7 @@ const _getQueryState = bindActionByResource(
309
309
  return queryState?.result
310
310
  },
311
311
  onSubscribe: ({state, instance}, options: QueryOptions) => {
312
- const subscriptionId = insecureRandomId()
312
+ const subscriptionId = randomId(16)
313
313
  const key = getQueryKey(instance, options)
314
314
 
315
315
  state.set('addSubscriber', addSubscriber(key, subscriptionId))
@@ -347,7 +347,7 @@ export function resolveQuery<
347
347
  >(
348
348
  instance: SanityInstance,
349
349
  queryOptions: ResolveQueryOptions<TQuery, TDataset, TProjectId>,
350
- ): Promise<SanityQueryResult<TQuery, `${TProjectId}.${TDataset}`>>
350
+ ): Promise<ResolveQueryResult<TQuery, `${TProjectId}.${TDataset}`>>
351
351
 
352
352
  /** @beta */
353
353
  export function resolveQuery<TData>(
@@ -370,7 +370,7 @@ const _resolveQuery = bindActionByResource(
370
370
  // while a component is suspended leaves a subscriber-less key behind — the
371
371
  // component never commits, so no other subscriber exists — and its stored
372
372
  // error would be rethrown on every future mount without ever refetching.
373
- const subscriptionId = insecureRandomId()
373
+ const subscriptionId = randomId(16)
374
374
  state.set('addSubscriber', addSubscriber(key, subscriptionId))
375
375
 
376
376
  const aborted$ = signal