@sanity/sdk 3.0.0 → 3.1.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.
- package/dist/_exports/_internal.d.ts +25 -3
- package/dist/_exports/_internal.d.ts.map +1 -1
- package/dist/_exports/_internal.js +127 -9
- package/dist/_exports/_internal.js.map +1 -1
- package/dist/_exports/agent.d.ts +1 -1
- package/dist/_exports/agent.js +1 -1
- package/dist/_exports/comlink.d.ts +1 -1
- package/dist/_exports/comlink.js +2 -3
- package/dist/_exports/comlink.js.map +1 -1
- package/dist/{authStore-C0o1NOxW.d.ts → authStore-C-OEkGaM.d.ts} +2 -2
- package/dist/{authStore-C0o1NOxW.d.ts.map → authStore-C-OEkGaM.d.ts.map} +1 -1
- package/dist/{authStore-CtE9bLHW.js → clientStore-gUALQ0qW.js} +269 -11
- package/dist/{authStore-CtE9bLHW.js.map → clientStore-gUALQ0qW.js.map} +1 -1
- package/dist/{createSanityInstance-DUI1bGIg.d.ts → createSanityInstance-DCePpBYW.d.ts} +2 -2
- package/dist/{createSanityInstance-DUI1bGIg.d.ts.map → createSanityInstance-DCePpBYW.d.ts.map} +1 -1
- package/dist/{getNodeState-CR3__B2D.js → getNodeState-E0hjqFOo.js} +2 -3
- package/dist/{getNodeState-CR3__B2D.js.map → getNodeState-E0hjqFOo.js.map} +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +40 -44
- package/dist/index.js.map +1 -1
- package/dist/{createStateSourceAction-BOKtqFNm.js → object-BwynGPRZ.js} +87 -3
- package/dist/object-BwynGPRZ.js.map +1 -0
- package/dist/{queryStore-C7yMHPUI.js → version-C90rNWaX.js} +47 -15
- package/dist/version-C90rNWaX.js.map +1 -0
- package/package.json +5 -5
- package/src/_exports/_internal.ts +1 -0
- package/src/access/checkPermissions.test.ts +1 -1
- package/src/access/checkPermissions.ts +1 -1
- package/src/applications/applications.test.ts +4 -4
- package/src/applications/applications.ts +4 -4
- package/src/auth/handleAuthCallback.test.ts +2 -2
- package/src/auth/handleAuthCallback.ts +1 -1
- package/src/auth/logout.test.ts +1 -1
- package/src/auth/logout.ts +1 -1
- package/src/auth/refreshStampedToken.ts +1 -1
- package/src/auth/studioModeAuth.test.ts +1 -1
- package/src/auth/studioModeAuth.ts +1 -1
- package/src/auth/subscribeToStateAndFetchCurrentUser.test.ts +2 -2
- package/src/auth/subscribeToStateAndFetchCurrentUser.ts +1 -1
- package/src/comments/addonDatasetStore.test.ts +2 -2
- package/src/comments/addonDatasetStore.ts +2 -2
- package/src/comments/commentActions.ts +6 -5
- package/src/comments/commentsStore.ts +4 -4
- package/src/document/applyDocumentActions.ts +2 -6
- package/src/document/documentStore.concurrency.test.ts +2 -1
- package/src/document/documentStore.test.ts +19 -18
- package/src/document/documentStore.ts +5 -5
- package/src/document/permissions.ts +2 -1
- package/src/document/processActions.test.ts +2 -1
- package/src/document/processMutations.ts +3 -2
- package/src/document/reducers.ts +2 -2
- package/src/installations/installations.test.ts +2 -2
- package/src/installations/installations.ts +2 -2
- package/src/organization/organization.test.ts +2 -2
- package/src/organization/organization.ts +1 -1
- package/src/organizations/organizations.test.ts +2 -2
- package/src/organizations/organizations.ts +1 -1
- package/src/presence/presenceStore.test.ts +15 -22
- package/src/presence/presenceStore.ts +3 -2
- package/src/project/project.test.ts +3 -3
- package/src/project/project.ts +1 -1
- package/src/projection/getProjectionState.test.ts +6 -4
- package/src/projection/getProjectionState.ts +2 -2
- package/src/projects/projects.test.ts +2 -2
- package/src/projects/projects.ts +1 -1
- package/src/query/queryStore.ts +3 -3
- package/src/store/createSanityInstance.test.ts +1 -1
- package/src/store/createSanityInstance.ts +4 -4
- package/src/store/createStoreInstance.test.ts +0 -5
- package/src/telemetry/telemetryManager.test.ts +1 -1
- package/src/telemetry/telemetryManager.ts +2 -2
- package/src/users/usersStore.test.ts +1 -1
- package/src/users/usersStore.ts +5 -5
- package/src/utils/ids.test.ts +40 -15
- package/src/utils/ids.ts +47 -2
- package/dist/clientStore-BtE2-q2i.js +0 -90
- package/dist/clientStore-BtE2-q2i.js.map +0 -1
- package/dist/createStateSourceAction-BOKtqFNm.js.map +0 -1
- package/dist/getEnv-D5RGkQLu.js +0 -8
- package/dist/getEnv-D5RGkQLu.js.map +0 -1
- package/dist/logger-DAtJb5RQ.js +0 -173
- package/dist/logger-DAtJb5RQ.js.map +0 -1
- package/dist/object-CxXlSVg7.js +0 -83
- package/dist/object-CxXlSVg7.js.map +0 -1
- package/dist/queryStore-C7yMHPUI.js.map +0 -1
- package/dist/rolldown-runtime-vyAXikos.js +0 -9
- package/dist/telemetryManager-UIEiMtC0.js +0 -127
- package/dist/telemetryManager-UIEiMtC0.js.map +0 -1
- package/dist/version-D8RP_Q59.js +0 -9
- package/dist/version-D8RP_Q59.js.map +0 -1
|
@@ -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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
83
|
+
url: '/projects/p',
|
|
84
84
|
query: {includeMembers: 'true', includeFeatures: 'true'},
|
|
85
85
|
tag: 'projects.get',
|
|
86
86
|
})
|
package/src/project/project.ts
CHANGED
|
@@ -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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
type StateSource,
|
|
11
11
|
} from '../store/createStateSourceAction'
|
|
12
12
|
import {hashString} from '../utils/hashString'
|
|
13
|
-
import {
|
|
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'
|
|
@@ -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 =
|
|
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)
|
|
@@ -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
|
-
|
|
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
|
-
|
|
61
|
+
url: '/projects',
|
|
62
62
|
query: {
|
|
63
63
|
organizationId: 'org123',
|
|
64
64
|
includeMembers: 'true',
|
package/src/projects/projects.ts
CHANGED
|
@@ -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
|
-
|
|
61
|
+
url: '/projects',
|
|
62
62
|
query: buildQuery(normalizeProjectsOptions(options)),
|
|
63
63
|
tag: 'projects.list',
|
|
64
64
|
}),
|
package/src/query/queryStore.ts
CHANGED
|
@@ -36,7 +36,7 @@ import {
|
|
|
36
36
|
} from '../store/createStateSourceAction'
|
|
37
37
|
import {type StoreState} from '../store/createStoreState'
|
|
38
38
|
import {defineStore, type StoreContext} from '../store/defineStore'
|
|
39
|
-
import {
|
|
39
|
+
import {randomId} from '../utils/ids'
|
|
40
40
|
import {setCleanupTimeout} from '../utils/setCleanupTimeout'
|
|
41
41
|
import {
|
|
42
42
|
QUERY_STATE_CLEAR_DELAY,
|
|
@@ -309,7 +309,7 @@ const _getQueryState = bindActionByResource(
|
|
|
309
309
|
return queryState?.result
|
|
310
310
|
},
|
|
311
311
|
onSubscribe: ({state, instance}, options: QueryOptions) => {
|
|
312
|
-
const subscriptionId =
|
|
312
|
+
const subscriptionId = randomId(16)
|
|
313
313
|
const key = getQueryKey(instance, options)
|
|
314
314
|
|
|
315
315
|
state.set('addSubscriber', addSubscriber(key, subscriptionId))
|
|
@@ -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 =
|
|
373
|
+
const subscriptionId = randomId(16)
|
|
374
374
|
state.set('addSubscriber', addSubscriber(key, subscriptionId))
|
|
375
375
|
|
|
376
376
|
const aborted$ = signal
|
|
@@ -6,7 +6,7 @@ import {createSanityInstance} from './createSanityInstance'
|
|
|
6
6
|
describe('createSanityInstance', () => {
|
|
7
7
|
it('should create an instance with a unique instanceId and given config', () => {
|
|
8
8
|
const instance = createSanityInstance({projectId: 'proj1', dataset: 'ds1'})
|
|
9
|
-
expect(
|
|
9
|
+
expect(instance.instanceId).toMatch(/^[a-zA-Z0-9]{16}$/)
|
|
10
10
|
expect(instance.config).toEqual({projectId: 'proj1', dataset: 'ds1'})
|
|
11
11
|
expect(instance.isDisposed()).toBe(false)
|
|
12
12
|
})
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import {type SanityConfig} from '../config/sanityConfig'
|
|
2
|
-
import {
|
|
2
|
+
import {randomId} from '../utils/ids'
|
|
3
3
|
import {createLogger, type InstanceContext} from '../utils/logger'
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -10,7 +10,7 @@ import {createLogger, type InstanceContext} from '../utils/logger'
|
|
|
10
10
|
export interface SanityInstance {
|
|
11
11
|
/**
|
|
12
12
|
* Unique identifier for this instance
|
|
13
|
-
* @remarks Generated
|
|
13
|
+
* @remarks Generated as a random 16-character alphanumeric ID
|
|
14
14
|
*/
|
|
15
15
|
readonly instanceId: string
|
|
16
16
|
|
|
@@ -47,7 +47,7 @@ export interface SanityInstance {
|
|
|
47
47
|
* @public
|
|
48
48
|
*/
|
|
49
49
|
export function createSanityInstance(config: SanityConfig = {}): SanityInstance {
|
|
50
|
-
const instanceId =
|
|
50
|
+
const instanceId = randomId(16)
|
|
51
51
|
const disposeListeners = new Map<string, () => void>()
|
|
52
52
|
const disposed = {current: false}
|
|
53
53
|
|
|
@@ -99,7 +99,7 @@ export function createSanityInstance(config: SanityConfig = {}): SanityInstance
|
|
|
99
99
|
logger.info('Instance disposed')
|
|
100
100
|
},
|
|
101
101
|
onDispose: (cb) => {
|
|
102
|
-
const listenerId =
|
|
102
|
+
const listenerId = randomId(16)
|
|
103
103
|
disposeListeners.set(listenerId, cb)
|
|
104
104
|
return () => {
|
|
105
105
|
disposeListeners.delete(listenerId)
|
|
@@ -8,11 +8,6 @@ describe('createStoreInstance', () => {
|
|
|
8
8
|
let instance: ReturnType<typeof createSanityInstance>
|
|
9
9
|
|
|
10
10
|
beforeEach(() => {
|
|
11
|
-
// Mock crypto for predictable instance IDs
|
|
12
|
-
vi.stubGlobal('crypto', {
|
|
13
|
-
randomUUID: () => 'test-uuid-1234',
|
|
14
|
-
})
|
|
15
|
-
|
|
16
11
|
instance = createSanityInstance({projectId: 'test', dataset: 'test'})
|
|
17
12
|
})
|
|
18
13
|
|
|
@@ -280,7 +280,7 @@ describe('createTelemetryManager', () => {
|
|
|
280
280
|
const result = await manager.checkConsent()
|
|
281
281
|
expect(result).toBe(true)
|
|
282
282
|
expect(mockClient.request).toHaveBeenCalledWith(
|
|
283
|
-
expect.objectContaining({
|
|
283
|
+
expect.objectContaining({url: '/intake/telemetry-status'}),
|
|
284
284
|
)
|
|
285
285
|
})
|
|
286
286
|
|
|
@@ -86,7 +86,7 @@ export function createTelemetryManager(options: TelemetryManagerOptions): Teleme
|
|
|
86
86
|
try {
|
|
87
87
|
const client = getClient()
|
|
88
88
|
const result = await client.request<{status: ConsentStatus}>({
|
|
89
|
-
|
|
89
|
+
url: '/intake/telemetry-status',
|
|
90
90
|
tag: CONSENT_TAG,
|
|
91
91
|
})
|
|
92
92
|
cachedConsent = result
|
|
@@ -120,7 +120,7 @@ export function createTelemetryManager(options: TelemetryManagerOptions): Teleme
|
|
|
120
120
|
const client = getClient()
|
|
121
121
|
log.debug('sending event batch', {batchSize: batch.length, environment})
|
|
122
122
|
return client.request({
|
|
123
|
-
|
|
123
|
+
url: '/intake/batch',
|
|
124
124
|
method: 'POST',
|
|
125
125
|
body: {projectId, batch: enrichBatch(batch)},
|
|
126
126
|
tag: BATCH_TAG,
|
package/src/users/usersStore.ts
CHANGED
|
@@ -30,7 +30,7 @@ import {bindActionGlobally} from '../store/createActionBinder'
|
|
|
30
30
|
import {createStateSourceAction, type SelectorContext} from '../store/createStateSourceAction'
|
|
31
31
|
import {type StoreState} from '../store/createStoreState'
|
|
32
32
|
import {defineStore, type StoreContext} from '../store/defineStore'
|
|
33
|
-
import {
|
|
33
|
+
import {randomId} from '../utils/ids'
|
|
34
34
|
import {setCleanupTimeout} from '../utils/setCleanupTimeout'
|
|
35
35
|
import {
|
|
36
36
|
addSubscription,
|
|
@@ -130,7 +130,7 @@ const listenForLoadMoreAndFetch = ({state, instance}: StoreContext<UsersStoreSta
|
|
|
130
130
|
return client.observable
|
|
131
131
|
.request<PatchedSanityUserFromClient>({
|
|
132
132
|
method: 'GET',
|
|
133
|
-
|
|
133
|
+
url: `/users/${userId}`,
|
|
134
134
|
tag: 'users.get',
|
|
135
135
|
})
|
|
136
136
|
.pipe(
|
|
@@ -184,7 +184,7 @@ const listenForLoadMoreAndFetch = ({state, instance}: StoreContext<UsersStoreSta
|
|
|
184
184
|
return client.observable
|
|
185
185
|
.request<SanityUser | SanityUserResponse>({
|
|
186
186
|
method: 'GET',
|
|
187
|
-
|
|
187
|
+
url: `access/${resourceType}/${resourceId}/users/${userId}`,
|
|
188
188
|
tag: 'users.get',
|
|
189
189
|
})
|
|
190
190
|
.pipe(
|
|
@@ -253,7 +253,7 @@ const listenForLoadMoreAndFetch = ({state, instance}: StoreContext<UsersStoreSta
|
|
|
253
253
|
switchMap(([[resource, client], cursor]) =>
|
|
254
254
|
client.observable.request<SanityUserResponse>({
|
|
255
255
|
method: 'GET',
|
|
256
|
-
|
|
256
|
+
url: `access/${resource.type}/${resource.id}/users`,
|
|
257
257
|
tag: 'users.list',
|
|
258
258
|
query: cursor
|
|
259
259
|
? {nextCursor: cursor, limit: batchSize.toString()}
|
|
@@ -310,7 +310,7 @@ export const getUsersState = bindActionGlobally(
|
|
|
310
310
|
},
|
|
311
311
|
),
|
|
312
312
|
onSubscribe: ({instance, state}, options?: GetUsersOptions) => {
|
|
313
|
-
const subscriptionId =
|
|
313
|
+
const subscriptionId = randomId(16)
|
|
314
314
|
const key = getUsersKey(instance, options)
|
|
315
315
|
state.set('addSubscription', addSubscription(subscriptionId, key))
|
|
316
316
|
return () => {
|
package/src/utils/ids.test.ts
CHANGED
|
@@ -1,26 +1,51 @@
|
|
|
1
|
-
import {describe, expect, it} from 'vitest'
|
|
1
|
+
import {afterEach, describe, expect, it, vi} from 'vitest'
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {randomId, randomUuid} from './ids'
|
|
4
4
|
|
|
5
|
-
describe('
|
|
6
|
-
it('should generate
|
|
7
|
-
expect(
|
|
5
|
+
describe('randomId', () => {
|
|
6
|
+
it('should generate an 8-character base62 id by default', () => {
|
|
7
|
+
expect(randomId()).toMatch(/^[a-zA-Z0-9]{8}$/)
|
|
8
8
|
})
|
|
9
9
|
|
|
10
|
-
it('should generate
|
|
11
|
-
expect(
|
|
10
|
+
it('should generate an id of the given length', () => {
|
|
11
|
+
expect(randomId(16)).toMatch(/^[a-zA-Z0-9]{16}$/)
|
|
12
12
|
})
|
|
13
13
|
|
|
14
14
|
it('should generate different ids on each call', () => {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
expect(randomId()).not.toBe(randomId())
|
|
16
|
+
})
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
describe('randomUuid', () => {
|
|
20
|
+
const uuidV4Pattern = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/
|
|
21
|
+
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
vi.unstubAllGlobals()
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('should generate a v4 UUID', () => {
|
|
27
|
+
expect(randomUuid()).toMatch(uuidV4Pattern)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('should use crypto.randomUUID when available', () => {
|
|
31
|
+
vi.stubGlobal('crypto', {randomUUID: () => 'native-uuid'})
|
|
32
|
+
expect(randomUuid()).toBe('native-uuid')
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('should generate a v4 UUID when crypto.randomUUID is unavailable (insecure contexts)', () => {
|
|
36
|
+
const originalCrypto = globalThis.crypto
|
|
37
|
+
vi.stubGlobal('crypto', {
|
|
38
|
+
getRandomValues: (array: Uint8Array<ArrayBuffer>) => originalCrypto.getRandomValues(array),
|
|
39
|
+
})
|
|
40
|
+
expect(randomUuid()).toMatch(uuidV4Pattern)
|
|
18
41
|
})
|
|
19
42
|
|
|
20
|
-
it('should generate
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
43
|
+
it('should generate unique ids when crypto.randomUUID is unavailable', () => {
|
|
44
|
+
const originalCrypto = globalThis.crypto
|
|
45
|
+
vi.stubGlobal('crypto', {
|
|
46
|
+
getRandomValues: (array: Uint8Array<ArrayBuffer>) => originalCrypto.getRandomValues(array),
|
|
47
|
+
})
|
|
48
|
+
const ids = new Set(Array.from({length: 100}, () => randomUuid()))
|
|
49
|
+
expect(ids.size).toBe(100)
|
|
25
50
|
})
|
|
26
51
|
})
|
package/src/utils/ids.ts
CHANGED
|
@@ -1,3 +1,48 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
const ID_ALPHABET = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Generates a random ID of the given length from the alphabet `[a-zA-Z0-9]`, the same shape
|
|
5
|
+
* `@sanity/client` and Sanity Studio use for short IDs such as release IDs.
|
|
6
|
+
*
|
|
7
|
+
* Ported from `@sanity/client`'s `generateReleaseId`, which uses rejection sampling to keep
|
|
8
|
+
* the output unbiased. Uses `crypto.getRandomValues`, which unlike `crypto.randomUUID` is
|
|
9
|
+
* also available on insecure origins (plain http).
|
|
10
|
+
*
|
|
11
|
+
* @internal
|
|
12
|
+
*/
|
|
13
|
+
export function randomId(length: number = 8): string {
|
|
14
|
+
let id = ''
|
|
15
|
+
while (id.length < length) {
|
|
16
|
+
const bytes = crypto.getRandomValues(new Uint8Array(length - id.length))
|
|
17
|
+
for (const byte of bytes) {
|
|
18
|
+
const index = byte & 63
|
|
19
|
+
if (index < ID_ALPHABET.length) id += ID_ALPHABET.charAt(index)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return id
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Generates a random v4 UUID.
|
|
27
|
+
*
|
|
28
|
+
* Prefers the native `crypto.randomUUID`, which is only available in secure contexts
|
|
29
|
+
* (https, localhost). Falls back to deriving the UUID from `crypto.getRandomValues`,
|
|
30
|
+
* which has no such restriction, so ID generation also works when an app is served over
|
|
31
|
+
* plain http, e.g. when testing from a phone on a local network.
|
|
32
|
+
*
|
|
33
|
+
* @internal
|
|
34
|
+
*/
|
|
35
|
+
export function randomUuid(): string {
|
|
36
|
+
// eslint-disable-next-line no-restricted-properties -- this is the sanctioned wrapper around crypto.randomUUID
|
|
37
|
+
if (typeof crypto.randomUUID === 'function') return crypto.randomUUID()
|
|
38
|
+
|
|
39
|
+
const bytes = crypto.getRandomValues(new Uint8Array(16))
|
|
40
|
+
const hex = Array.from(bytes, (byte, index) => {
|
|
41
|
+
// Set the version (4) and variant (RFC 4122) bits
|
|
42
|
+
if (index === 6) byte = (byte & 0x0f) | 0x40
|
|
43
|
+
if (index === 8) byte = (byte & 0x3f) | 0x80
|
|
44
|
+
return byte.toString(16).padStart(2, '0')
|
|
45
|
+
}).join('')
|
|
46
|
+
|
|
47
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`
|
|
3
48
|
}
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
import { t as __exportAll } from "./rolldown-runtime-vyAXikos.js";
|
|
2
|
-
import { a as defineStore, c as isDatasetResource, i as bindActionGlobally, l as isMediaLibraryResource, s as isCanvasResource, t as createStateSourceAction } from "./createStateSourceAction-BOKtqFNm.js";
|
|
3
|
-
import { T as getStagingApiHost, l as getTokenState, r as getAuthMethodState } from "./authStore-CtE9bLHW.js";
|
|
4
|
-
import { i as pickProperties } from "./object-CxXlSVg7.js";
|
|
5
|
-
import { createClient } from "@sanity/client";
|
|
6
|
-
var clientStore_exports = /* @__PURE__ */ __exportAll({
|
|
7
|
-
getClient: () => getClient,
|
|
8
|
-
getClientState: () => getClientState
|
|
9
|
-
});
|
|
10
|
-
const allowedKeys = Object.keys({
|
|
11
|
-
apiHost: null,
|
|
12
|
-
useCdn: null,
|
|
13
|
-
token: null,
|
|
14
|
-
perspective: null,
|
|
15
|
-
proxy: null,
|
|
16
|
-
withCredentials: null,
|
|
17
|
-
timeout: null,
|
|
18
|
-
maxRetries: null,
|
|
19
|
-
dataset: null,
|
|
20
|
-
projectId: null,
|
|
21
|
-
scope: null,
|
|
22
|
-
apiVersion: null,
|
|
23
|
-
requestTagPrefix: null,
|
|
24
|
-
useProjectHostname: null,
|
|
25
|
-
resource: null
|
|
26
|
-
}), DEFAULT_CLIENT_CONFIG = {
|
|
27
|
-
apiVersion: "2024-11-12",
|
|
28
|
-
useCdn: !1,
|
|
29
|
-
ignoreBrowserTokenWarning: !0,
|
|
30
|
-
allowReconfigure: !1,
|
|
31
|
-
requestTagPrefix: "sanity.sdk"
|
|
32
|
-
}, clientStore = defineStore({
|
|
33
|
-
name: "clientStore",
|
|
34
|
-
getInitialState: (instance) => ({
|
|
35
|
-
clients: {},
|
|
36
|
-
token: getTokenState(instance).getCurrent()
|
|
37
|
-
}),
|
|
38
|
-
initialize(context) {
|
|
39
|
-
let subscription = listenToToken(context), authMethodSubscription = listenToAuthMethod(context);
|
|
40
|
-
return () => {
|
|
41
|
-
subscription.unsubscribe(), authMethodSubscription.unsubscribe();
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
}), listenToToken = ({ instance, state }) => getTokenState(instance).observable.subscribe((token) => {
|
|
45
|
-
state.set("setTokenAndResetClients", {
|
|
46
|
-
token,
|
|
47
|
-
clients: {}
|
|
48
|
-
});
|
|
49
|
-
}), listenToAuthMethod = ({ instance, state }) => getAuthMethodState(instance).observable.subscribe((authMethod) => {
|
|
50
|
-
state.set("setAuthMethod", { authMethod });
|
|
51
|
-
}), getClientConfigKey = (options) => JSON.stringify(pickProperties(options, allowedKeys)), getClient = bindActionGlobally(clientStore, ({ state, instance }, options) => {
|
|
52
|
-
if (!options || typeof options != "object") throw Error("getClient() requires a configuration object with at least an \"apiVersion\" property. Example: getClient(instance, { apiVersion: \"2024-11-12\" })");
|
|
53
|
-
let disallowedKeys = Object.keys(options).filter((key) => !allowedKeys.includes(key));
|
|
54
|
-
if (disallowedKeys.length > 0) {
|
|
55
|
-
let listFormatter = new Intl.ListFormat("en", {
|
|
56
|
-
style: "long",
|
|
57
|
-
type: "conjunction"
|
|
58
|
-
});
|
|
59
|
-
throw Error(`The client options provided contains unsupported properties: ${listFormatter.format(disallowedKeys)}. Allowed keys are: ${listFormatter.format(allowedKeys)}.`);
|
|
60
|
-
}
|
|
61
|
-
let tokenFromState = state.get().token, { clients, authMethod } = state.get(), projectId = options.projectId ?? instance.config.projectId, dataset = options.dataset ?? instance.config.dataset, resource;
|
|
62
|
-
options.resource && (isMediaLibraryResource(options.resource) ? resource = {
|
|
63
|
-
type: "media-library",
|
|
64
|
-
id: options.resource.mediaLibraryId
|
|
65
|
-
} : isCanvasResource(options.resource) ? resource = {
|
|
66
|
-
type: "canvas",
|
|
67
|
-
id: options.resource.canvasId
|
|
68
|
-
} : isDatasetResource(options.resource) && (projectId = options.resource.projectId, dataset = options.resource.dataset));
|
|
69
|
-
let apiHost = options.apiHost ?? instance.config.auth?.apiHost ?? getStagingApiHost(), effectiveOptions = {
|
|
70
|
-
...DEFAULT_CLIENT_CONFIG,
|
|
71
|
-
...(options.scope === "global" || !projectId || resource) && { useProjectHostname: !1 },
|
|
72
|
-
token: authMethod === "cookie" ? void 0 : tokenFromState ?? void 0,
|
|
73
|
-
...options,
|
|
74
|
-
...projectId && { projectId },
|
|
75
|
-
...dataset && { dataset },
|
|
76
|
-
...resource ? { resource } : { resource: void 0 },
|
|
77
|
-
...apiHost && { apiHost }
|
|
78
|
-
};
|
|
79
|
-
resource && (delete effectiveOptions.projectId, delete effectiveOptions.dataset), effectiveOptions.token === null || effectiveOptions.token === void 0 ? (delete effectiveOptions.token, authMethod === "cookie" && (effectiveOptions.withCredentials = !0)) : delete effectiveOptions.withCredentials;
|
|
80
|
-
let key = getClientConfigKey(effectiveOptions);
|
|
81
|
-
if (clients[key]) return clients[key];
|
|
82
|
-
let client = createClient(effectiveOptions);
|
|
83
|
-
return state.set("addClient", (prev) => ({ clients: {
|
|
84
|
-
...prev.clients,
|
|
85
|
-
[key]: client
|
|
86
|
-
} })), client;
|
|
87
|
-
}), getClientState = bindActionGlobally(clientStore, createStateSourceAction(({ instance }, options) => getClient(instance, options)));
|
|
88
|
-
export { getClient as n, getClientState as r, clientStore_exports as t };
|
|
89
|
-
|
|
90
|
-
//# sourceMappingURL=clientStore-BtE2-q2i.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"clientStore-BtE2-q2i.js","names":[],"sources":["../src/client/clientStore.ts"],"sourcesContent":["import {type ClientConfig, createClient, type SanityClient} from '@sanity/client'\n\nimport {getAuthMethodState, getTokenState} from '../auth/authStore'\nimport {\n type DocumentResource,\n isCanvasResource,\n isDatasetResource,\n isMediaLibraryResource,\n} from '../config/sanityConfig'\nimport {bindActionGlobally} from '../store/createActionBinder'\nimport {createStateSourceAction} from '../store/createStateSourceAction'\nimport {defineStore, type StoreContext} from '../store/defineStore'\nimport {getStagingApiHost} from '../utils/getStagingApiHost'\nimport {pickProperties} from '../utils/object'\n\nconst DEFAULT_API_VERSION = '2024-11-12'\nconst DEFAULT_REQUEST_TAG_PREFIX = 'sanity.sdk'\n\ntype AllowedClientConfigKey =\n | 'useCdn'\n | 'token'\n | 'perspective'\n | 'apiHost'\n | 'proxy'\n | 'withCredentials'\n | 'timeout'\n | 'maxRetries'\n | 'dataset'\n | 'projectId'\n | 'requestTagPrefix'\n | 'useProjectHostname'\n\nconst allowedKeys = Object.keys({\n apiHost: null,\n useCdn: null,\n token: null,\n perspective: null,\n proxy: null,\n withCredentials: null,\n timeout: null,\n maxRetries: null,\n dataset: null,\n projectId: null,\n scope: null,\n apiVersion: null,\n requestTagPrefix: null,\n useProjectHostname: null,\n resource: null,\n} satisfies Record<keyof ClientOptions, null>) as (keyof ClientOptions)[]\n\nconst DEFAULT_CLIENT_CONFIG: ClientConfig = {\n apiVersion: DEFAULT_API_VERSION,\n useCdn: false,\n ignoreBrowserTokenWarning: true,\n allowReconfigure: false,\n requestTagPrefix: DEFAULT_REQUEST_TAG_PREFIX,\n}\n\n/**\n * States tracked by the client store\n * @public\n */\nexport interface ClientStoreState {\n token: string | null\n clients: {[TKey in string]?: SanityClient}\n authMethod?: 'localstorage' | 'cookie'\n}\n\n/**\n * Options used when retrieving a client instance from the client store.\n *\n * This interface extends the base {@link ClientConfig} and adds:\n *\n * - **apiVersion:** A required string indicating the API version for the client.\n * - **scope:** An optional flag to choose between the project-specific client\n * ('project') and the global client ('global'). When set to `'global'`, the\n * global client is used.\n *\n * These options are utilized by `getClient` and `getClientState` to configure and\n * return appropriate client instances that automatically handle authentication\n * updates and configuration changes.\n *\n * @public\n */\nexport interface ClientOptions extends Pick<ClientConfig, AllowedClientConfigKey> {\n /**\n * An optional flag to choose between the default client (typically project-level)\n * and the global client ('global'). When set to `'global'`, the global client\n * is used.\n */\n scope?: 'default' | 'global'\n /**\n * A required string indicating the API version for the client.\n */\n apiVersion: string\n\n /**\n * @internal\n * The SDK resource to use for the client -- this will get transformed into a ClientConfig resource.\n */\n resource?: DocumentResource\n}\n\nconst clientStore = defineStore<ClientStoreState>({\n name: 'clientStore',\n\n getInitialState: (instance) => ({\n clients: {},\n token: getTokenState(instance).getCurrent(),\n }),\n\n initialize(context) {\n const subscription = listenToToken(context)\n const authMethodSubscription = listenToAuthMethod(context)\n return () => {\n subscription.unsubscribe()\n authMethodSubscription.unsubscribe()\n }\n },\n})\n\n/**\n * Updates the client store state when a token is received.\n * @internal\n */\nconst listenToToken = ({instance, state}: StoreContext<ClientStoreState>) => {\n return getTokenState(instance).observable.subscribe((token) => {\n state.set('setTokenAndResetClients', {token, clients: {}})\n })\n}\n\nconst listenToAuthMethod = ({instance, state}: StoreContext<ClientStoreState>) => {\n return getAuthMethodState(instance).observable.subscribe((authMethod) => {\n state.set('setAuthMethod', {authMethod})\n })\n}\n\ntype ClientInstanceCacheKeyInput = ClientConfig &\n Partial<Pick<ClientOptions, 'scope'>> & {\n apiVersion: string\n }\n\nconst getClientConfigKey = (options: ClientInstanceCacheKeyInput) =>\n JSON.stringify(pickProperties(options, allowedKeys))\n\n/**\n * Retrieves a Sanity client instance configured with the provided options.\n *\n * This function returns a client instance configured for the project or as a\n * global client based on the options provided. It ensures efficient reuse of\n * client instances by returning the same instance for the same options.\n * For automatic handling of authentication token updates, consider using\n * `getClientState`.\n *\n * @public\n */\nexport const getClient = bindActionGlobally(\n clientStore,\n ({state, instance}, options: ClientOptions) => {\n if (!options || typeof options !== 'object') {\n throw new Error(\n 'getClient() requires a configuration object with at least an \"apiVersion\" property. ' +\n 'Example: getClient(instance, { apiVersion: \"2024-11-12\" })',\n )\n }\n\n // Check for disallowed keys\n const providedKeys = Object.keys(options) as (keyof ClientOptions)[]\n const disallowedKeys = providedKeys.filter((key) => !allowedKeys.includes(key))\n\n if (disallowedKeys.length > 0) {\n const listFormatter = new Intl.ListFormat('en', {style: 'long', type: 'conjunction'})\n throw new Error(\n `The client options provided contains unsupported properties: ${listFormatter.format(disallowedKeys)}. ` +\n `Allowed keys are: ${listFormatter.format(allowedKeys)}.`,\n )\n }\n\n const tokenFromState = state.get().token\n const {clients, authMethod} = state.get()\n let projectId = options.projectId ?? instance.config.projectId\n let dataset = options.dataset ?? instance.config.dataset\n\n let resource: ClientConfig['resource'] | undefined\n\n if (options.resource) {\n if (isMediaLibraryResource(options.resource)) {\n resource = {type: 'media-library', id: options.resource.mediaLibraryId}\n } else if (isCanvasResource(options.resource)) {\n resource = {type: 'canvas', id: options.resource.canvasId}\n } else if (isDatasetResource(options.resource)) {\n // use project-based routes for datasets to avoid existing CORS and Studio auth cookie issues\n projectId = options.resource.projectId\n dataset = options.resource.dataset\n }\n }\n\n const apiHost = options.apiHost ?? instance.config.auth?.apiHost ?? getStagingApiHost()\n\n const effectiveOptions: ClientConfig & {apiVersion: string} = {\n ...DEFAULT_CLIENT_CONFIG,\n ...((options.scope === 'global' || !projectId || resource) && {useProjectHostname: false}),\n token: authMethod === 'cookie' ? undefined : (tokenFromState ?? undefined),\n ...options,\n ...(projectId && {projectId}),\n ...(dataset && {dataset}),\n ...(resource ? {resource} : {resource: undefined}),\n ...(apiHost && {apiHost}),\n }\n\n // When a resource is provided, don't use projectId/dataset - the client should be \"projectless\"\n // The client code itself will ignore the non-resource config, so we do this to prevent confusing the user.\n // (ref: https://github.com/sanity-io/client/blob/5c23f81f5ab93a53f5b22b39845c867988508d84/src/data/dataMethods.ts#L691)\n if (resource) {\n delete effectiveOptions.projectId\n delete effectiveOptions.dataset\n }\n\n if (effectiveOptions.token === null || typeof effectiveOptions.token === 'undefined') {\n delete effectiveOptions.token\n if (authMethod === 'cookie') {\n effectiveOptions.withCredentials = true\n }\n } else {\n delete effectiveOptions.withCredentials\n }\n\n const key = getClientConfigKey(effectiveOptions)\n\n if (clients[key]) return clients[key]\n\n const client = createClient(effectiveOptions)\n state.set('addClient', (prev) => ({clients: {...prev.clients, [key]: client}}))\n\n return client\n },\n)\n\n/**\n * Returns a state source for the Sanity client instance.\n *\n * This function provides a subscribable state source that emits updated client\n * instances whenever relevant configurations change (such as authentication tokens).\n * Use this when you need to react to client configuration changes in your application.\n *\n * @public\n */\nexport const getClientState = bindActionGlobally(\n clientStore,\n createStateSourceAction(({instance}, options: ClientOptions) => getClient(instance, options)),\n)\n"],"mappings":"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"}
|