@sanity/sdk 2.10.0 → 2.11.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 (45) hide show
  1. package/dist/_chunks-dts/utils.d.ts +200 -28
  2. package/dist/_chunks-es/_internal.js +3 -14
  3. package/dist/_chunks-es/_internal.js.map +1 -1
  4. package/dist/_chunks-es/createGroqSearchFilter.js +7 -14
  5. package/dist/_chunks-es/createGroqSearchFilter.js.map +1 -1
  6. package/dist/_chunks-es/version.js +1 -1
  7. package/dist/_exports/_internal.d.ts +16 -2
  8. package/dist/_exports/_internal.js +3 -1
  9. package/dist/index.d.ts +2 -2
  10. package/dist/index.js +168 -88
  11. package/dist/index.js.map +1 -1
  12. package/package.json +7 -9
  13. package/src/_exports/_internal.ts +1 -0
  14. package/src/_exports/index.ts +25 -2
  15. package/src/agent/agentActions.ts +21 -25
  16. package/src/client/clientStore.test.ts +10 -46
  17. package/src/client/clientStore.ts +7 -14
  18. package/src/comlink/node/actions/getOrCreateNode.test.ts +5 -2
  19. package/src/comlink/node/actions/releaseNode.test.ts +3 -3
  20. package/src/config/sanityConfig.ts +0 -1
  21. package/src/document/documentStore.ts +2 -7
  22. package/src/document/sharedListener.ts +3 -5
  23. package/src/organization/organization.test-d.ts +102 -0
  24. package/src/organization/organization.test.ts +138 -0
  25. package/src/organization/organization.ts +166 -0
  26. package/src/organizations/organizations.test-d.ts +77 -0
  27. package/src/organizations/organizations.test.ts +150 -0
  28. package/src/organizations/organizations.ts +132 -0
  29. package/src/presence/presenceStore.test.ts +5 -5
  30. package/src/preview/previewProjectionUtils.ts +2 -3
  31. package/src/project/project.test-d.ts +93 -0
  32. package/src/project/project.test.ts +108 -10
  33. package/src/project/project.ts +152 -26
  34. package/src/projection/subscribeToStateAndFetchBatches.ts +4 -9
  35. package/src/projects/projects.test-d.ts +38 -0
  36. package/src/projects/projects.test.ts +104 -38
  37. package/src/projects/projects.ts +74 -14
  38. package/src/query/queryStore.ts +2 -3
  39. package/src/releases/releasesStore.test.ts +1 -1
  40. package/src/releases/releasesStore.ts +2 -2
  41. package/src/store/createSanityInstance.ts +3 -3
  42. package/src/telemetry/devMode.test.ts +8 -0
  43. package/src/telemetry/devMode.ts +10 -9
  44. package/src/telemetry/initTelemetry.test.ts +0 -17
  45. package/src/telemetry/initTelemetry.ts +2 -12
@@ -0,0 +1,166 @@
1
+ import {switchMap} from 'rxjs'
2
+
3
+ import {getClientState} from '../client/clientStore'
4
+ import {type SanityInstance} from '../store/createSanityInstance'
5
+ import {type StateSource} from '../store/createStateSourceAction'
6
+ import {createFetcherStore} from '../utils/createFetcherStore'
7
+
8
+ const API_VERSION = 'v2025-02-19'
9
+
10
+ /** @public */
11
+ export interface OrganizationMember {
12
+ sanityUserId: string
13
+ isCurrentUser: boolean
14
+ user: {
15
+ id: string
16
+ displayName: string
17
+ familyName: string
18
+ givenName: string
19
+ middleName: string | null
20
+ imageUrl: string | null
21
+ email: string
22
+ loginProvider: string
23
+ }
24
+ roles: Array<{
25
+ name: string
26
+ title: string
27
+ description?: string
28
+ }>
29
+ }
30
+
31
+ /**
32
+ * The base fields returned from `/organizations/<id>` for every organization.
33
+ * @public
34
+ */
35
+ export interface OrganizationBase {
36
+ id: string
37
+ name: string
38
+ slug: string | null
39
+ createdAt: string
40
+ createdByUserId: string
41
+ updatedAt: string
42
+ deletedAt: string | null
43
+ dashboardStatus: 'enabled' | 'disabled'
44
+ aiFeaturesStatus: 'enabled' | 'disabled'
45
+ mediaLibraryStatus: 'enabled' | 'disabled'
46
+ requestAccessStatus: 'allowed' | 'disabled'
47
+ telemetryConsentStatus: 'allowed' | 'msa_denied' | 'customer_denied'
48
+ oauthAppsStatus: 'allowed' | 'blocked'
49
+ defaultRoleName: string
50
+ domains: string[] | null
51
+ }
52
+
53
+ /** @public */
54
+ export interface OrganizationOptions<
55
+ IncludeMembers extends boolean = false,
56
+ IncludeFeatures extends boolean = false,
57
+ > {
58
+ includeMembers?: IncludeMembers
59
+ includeFeatures?: IncludeFeatures
60
+ organizationId: string
61
+ }
62
+
63
+ /**
64
+ * An `Organization` with `members` and/or `features` conditionally included
65
+ * based on the query options used to fetch it.
66
+ * @public
67
+ */
68
+ export type Organization<
69
+ IncludeMembers extends boolean = false,
70
+ IncludeFeatures extends boolean = false,
71
+ > = OrganizationBase &
72
+ // `boolean extends T` is non-distributive — true only when T is the wide
73
+ // `boolean`, in which case the field is optional. Literal `true`/`false`
74
+ // fall through to the strict branch.
75
+ (boolean extends IncludeMembers
76
+ ? {members?: OrganizationMember[]}
77
+ : IncludeMembers extends true
78
+ ? {members: OrganizationMember[]}
79
+ : unknown) &
80
+ (boolean extends IncludeFeatures
81
+ ? {features?: string[]}
82
+ : IncludeFeatures extends true
83
+ ? {features: string[]}
84
+ : unknown)
85
+
86
+ function resolveOrganizationId(options?: OrganizationOptions<boolean, boolean>) {
87
+ const organizationId = options?.organizationId
88
+ if (!organizationId) {
89
+ throw new Error('An organizationId is required to use the organization API.')
90
+ }
91
+ return organizationId
92
+ }
93
+
94
+ function normalizeOrganizationOptions(options?: OrganizationOptions<boolean, boolean>) {
95
+ return {
96
+ includeMembers: options?.includeMembers ?? false,
97
+ includeFeatures: options?.includeFeatures ?? false,
98
+ }
99
+ }
100
+
101
+ /** @internal */
102
+ export function getOrganizationCacheKey(
103
+ _instance: SanityInstance,
104
+ options?: OrganizationOptions<boolean, boolean>,
105
+ ): string {
106
+ const organizationId = resolveOrganizationId(options)
107
+ const {includeMembers, includeFeatures} = normalizeOrganizationOptions(options)
108
+ const membersKey = includeMembers ? ':members' : ''
109
+ const featuresKey = includeFeatures ? ':features' : ''
110
+ return `organization:${organizationId}${membersKey}${featuresKey}`
111
+ }
112
+
113
+ const organization = createFetcherStore({
114
+ name: 'Organization',
115
+ getKey: getOrganizationCacheKey,
116
+ fetcher: (instance) => (options?: OrganizationOptions<boolean, boolean>) => {
117
+ const organizationId = resolveOrganizationId(options)
118
+
119
+ return getClientState(instance, {
120
+ apiVersion: API_VERSION,
121
+ scope: 'global',
122
+ }).observable.pipe(
123
+ switchMap((client) => {
124
+ const normalized = normalizeOrganizationOptions(options)
125
+ const query = Object.fromEntries(
126
+ Object.entries(normalized)
127
+ .filter(([, value]) => value !== undefined)
128
+ .map(([key, value]) => [key, String(value)]),
129
+ )
130
+
131
+ return client.observable.request({
132
+ uri: `/organizations/${organizationId}`,
133
+ query,
134
+ tag: 'organization.get',
135
+ })
136
+ }),
137
+ )
138
+ },
139
+ })
140
+
141
+ /**
142
+ * Public signature for the organization state source. The conditional generics
143
+ * cannot flow through `BoundStoreAction`, so we declare the signature here
144
+ * and assign the (already-correct) runtime function to it.
145
+ */
146
+ type GetOrganizationState = <
147
+ IncludeMembers extends boolean = false,
148
+ IncludeFeatures extends boolean = false,
149
+ >(
150
+ instance: SanityInstance,
151
+ options: OrganizationOptions<IncludeMembers, IncludeFeatures>,
152
+ ) => StateSource<Organization<IncludeMembers, IncludeFeatures> | undefined>
153
+
154
+ type ResolveOrganization = <
155
+ IncludeMembers extends boolean = false,
156
+ IncludeFeatures extends boolean = false,
157
+ >(
158
+ instance: SanityInstance,
159
+ options: OrganizationOptions<IncludeMembers, IncludeFeatures>,
160
+ ) => Promise<Organization<IncludeMembers, IncludeFeatures>>
161
+
162
+ /** @public */
163
+ export const getOrganizationState: GetOrganizationState = organization.getState
164
+
165
+ /** @public */
166
+ export const resolveOrganization: ResolveOrganization = organization.resolveState
@@ -0,0 +1,77 @@
1
+ import {expectTypeOf, test} from 'vitest'
2
+
3
+ import {type OrganizationMember} from '../organization/organization'
4
+ import {type SanityInstance} from '../store/createSanityInstance'
5
+ import {type StateSource} from '../store/createStateSourceAction'
6
+ import {getOrganizationsState, type Organizations, resolveOrganizations} from './organizations'
7
+
8
+ const instance = {} as SanityInstance
9
+
10
+ test('resolveOrganizations — default call: bare list shape', () => {
11
+ expectTypeOf(resolveOrganizations(instance)).resolves.toEqualTypeOf<Organizations<false, false>>()
12
+ })
13
+
14
+ test('resolveOrganizations — includeMembers: true narrows the generic', () => {
15
+ expectTypeOf(resolveOrganizations(instance, {includeMembers: true})).resolves.toEqualTypeOf<
16
+ Organizations<true, false>
17
+ >()
18
+ })
19
+
20
+ test('resolveOrganizations — includeFeatures: true narrows the generic', () => {
21
+ expectTypeOf(resolveOrganizations(instance, {includeFeatures: true})).resolves.toEqualTypeOf<
22
+ Organizations<false, true>
23
+ >()
24
+ })
25
+
26
+ test('resolveOrganizations — both flags true', () => {
27
+ expectTypeOf(
28
+ resolveOrganizations(instance, {includeMembers: true, includeFeatures: true}),
29
+ ).resolves.toEqualTypeOf<Organizations<true, true>>()
30
+ })
31
+
32
+ test('resolveOrganizations — rejects non-boolean flag values', () => {
33
+ // @ts-expect-error — includeMembers must be a boolean
34
+ void resolveOrganizations(instance, {includeMembers: 'yes'})
35
+ })
36
+
37
+ test('resolveOrganizations — includeImplicitMemberships does not change the data shape', () => {
38
+ expectTypeOf(
39
+ resolveOrganizations(instance, {includeImplicitMemberships: true}),
40
+ ).resolves.toEqualTypeOf<Organizations<false, false>>()
41
+ })
42
+
43
+ test('Organizations — list items expose the documented subset of keys', () => {
44
+ type Keys = keyof Organizations<false, false>[number]
45
+ expectTypeOf<Keys>().toEqualTypeOf<
46
+ | 'id'
47
+ | 'name'
48
+ | 'slug'
49
+ | 'createdAt'
50
+ | 'updatedAt'
51
+ | 'defaultRoleName'
52
+ | 'dashboardStatus'
53
+ | 'aiFeaturesStatus'
54
+ >()
55
+ })
56
+
57
+ test('Organizations<true, false>[number] exposes members[]', () => {
58
+ type Item = Organizations<true, false>[number]
59
+ expectTypeOf<Item['members']>().toEqualTypeOf<OrganizationMember[]>()
60
+ })
61
+
62
+ test('Organizations<false, true>[number] exposes features[]', () => {
63
+ type Item = Organizations<false, true>[number]
64
+ expectTypeOf<Item['features']>().toEqualTypeOf<string[]>()
65
+ })
66
+
67
+ test('Organizations<true, true>[number] exposes both members[] and features[]', () => {
68
+ type Item = Organizations<true, true>[number]
69
+ expectTypeOf<Item['members']>().toEqualTypeOf<OrganizationMember[]>()
70
+ expectTypeOf<Item['features']>().toEqualTypeOf<string[]>()
71
+ })
72
+
73
+ test('getOrganizationsState — default call returns the bare-base StateSource', () => {
74
+ expectTypeOf(getOrganizationsState(instance)).toEqualTypeOf<
75
+ StateSource<Organizations<false, false> | undefined>
76
+ >()
77
+ })
@@ -0,0 +1,150 @@
1
+ import {type SanityClient} from '@sanity/client'
2
+ import {of} from 'rxjs'
3
+ import {afterEach, beforeEach, describe, it} from 'vitest'
4
+
5
+ import {getClientState} from '../client/clientStore'
6
+ import {createSanityInstance, type SanityInstance} from '../store/createSanityInstance'
7
+ import {type StateSource} from '../store/createStateSourceAction'
8
+ import {getOrganizationsCacheKey, resolveOrganizations} from './organizations'
9
+
10
+ vi.mock('../client/clientStore')
11
+
12
+ describe('organizations', () => {
13
+ let instance: SanityInstance
14
+
15
+ beforeEach(() => {
16
+ instance = createSanityInstance({projectId: 'p', dataset: 'd'})
17
+ })
18
+
19
+ afterEach(() => {
20
+ instance.dispose()
21
+ })
22
+
23
+ it('calls `client.observable.request` against `/organizations` and returns the result', async () => {
24
+ const organizations = [{id: 'org_1'}, {id: 'org_2'}]
25
+ const request = vi.fn().mockReturnValue(of(organizations))
26
+
27
+ const mockClient = {
28
+ observable: {request} as unknown as SanityClient['observable'],
29
+ } as SanityClient
30
+
31
+ vi.mocked(getClientState).mockReturnValue({
32
+ observable: of(mockClient),
33
+ } as StateSource<SanityClient>)
34
+
35
+ const result = await resolveOrganizations(instance)
36
+ expect(result).toEqual(organizations)
37
+ expect(request).toHaveBeenCalledWith({
38
+ uri: '/organizations',
39
+ query: {
40
+ includeImplicitMemberships: 'false',
41
+ includeMembers: 'false',
42
+ includeFeatures: 'false',
43
+ },
44
+ tag: 'organizations.get',
45
+ })
46
+ })
47
+
48
+ it('serializes query params (booleans → strings) and respects flags', async () => {
49
+ const request = vi.fn().mockReturnValue(of([]))
50
+ const mockClient = {
51
+ observable: {request} as unknown as SanityClient['observable'],
52
+ } as SanityClient
53
+
54
+ vi.mocked(getClientState).mockReturnValue({
55
+ observable: of(mockClient),
56
+ } as StateSource<SanityClient>)
57
+
58
+ await resolveOrganizations(instance, {
59
+ includeMembers: true,
60
+ includeFeatures: true,
61
+ includeImplicitMemberships: true,
62
+ })
63
+
64
+ expect(request).toHaveBeenCalledWith({
65
+ uri: '/organizations',
66
+ query: {
67
+ includeImplicitMemberships: 'true',
68
+ includeMembers: 'true',
69
+ includeFeatures: 'true',
70
+ },
71
+ tag: 'organizations.get',
72
+ })
73
+ })
74
+ })
75
+
76
+ describe('organizations cache key generation', () => {
77
+ let instance: SanityInstance
78
+
79
+ beforeEach(() => {
80
+ instance = createSanityInstance({})
81
+ })
82
+
83
+ afterEach(() => {
84
+ instance.dispose()
85
+ })
86
+
87
+ it('default call excludes all segments (all flags default-false)', () => {
88
+ expect(getOrganizationsCacheKey(instance)).toBe('organizations')
89
+ })
90
+
91
+ it('treats undefined and the matching default as the same key', () => {
92
+ expect(getOrganizationsCacheKey(instance)).toBe(
93
+ getOrganizationsCacheKey(instance, {
94
+ includeMembers: false,
95
+ includeFeatures: false,
96
+ includeImplicitMemberships: false,
97
+ }),
98
+ )
99
+ })
100
+
101
+ it('explicit includeMembers: true appends :members', () => {
102
+ expect(getOrganizationsCacheKey(instance, {includeMembers: true})).toBe('organizations:members')
103
+ })
104
+
105
+ it('explicit includeFeatures: true appends :features', () => {
106
+ expect(getOrganizationsCacheKey(instance, {includeFeatures: true})).toBe(
107
+ 'organizations:features',
108
+ )
109
+ })
110
+
111
+ it('explicit includeImplicitMemberships: true appends :implicit', () => {
112
+ expect(getOrganizationsCacheKey(instance, {includeImplicitMemberships: true})).toBe(
113
+ 'organizations:implicit',
114
+ )
115
+ })
116
+
117
+ it('combines all segments in order', () => {
118
+ expect(
119
+ getOrganizationsCacheKey(instance, {
120
+ includeMembers: true,
121
+ includeFeatures: true,
122
+ includeImplicitMemberships: true,
123
+ }),
124
+ ).toBe('organizations:members:features:implicit')
125
+ })
126
+
127
+ it('produces distinct keys for each meaningful option permutation', () => {
128
+ const keys = new Set([
129
+ getOrganizationsCacheKey(instance),
130
+ getOrganizationsCacheKey(instance, {includeMembers: true}),
131
+ getOrganizationsCacheKey(instance, {includeFeatures: true}),
132
+ getOrganizationsCacheKey(instance, {includeImplicitMemberships: true}),
133
+ getOrganizationsCacheKey(instance, {includeMembers: true, includeFeatures: true}),
134
+ getOrganizationsCacheKey(instance, {
135
+ includeMembers: true,
136
+ includeImplicitMemberships: true,
137
+ }),
138
+ getOrganizationsCacheKey(instance, {
139
+ includeFeatures: true,
140
+ includeImplicitMemberships: true,
141
+ }),
142
+ getOrganizationsCacheKey(instance, {
143
+ includeMembers: true,
144
+ includeFeatures: true,
145
+ includeImplicitMemberships: true,
146
+ }),
147
+ ])
148
+ expect(keys.size).toBe(8)
149
+ })
150
+ })
@@ -0,0 +1,132 @@
1
+ import {switchMap} from 'rxjs'
2
+
3
+ import {getClientState} from '../client/clientStore'
4
+ import {
5
+ type OrganizationBase,
6
+ type OrganizationMember,
7
+ type OrganizationOptions,
8
+ } from '../organization/organization'
9
+ import {type SanityInstance} from '../store/createSanityInstance'
10
+ import {type StateSource} from '../store/createStateSourceAction'
11
+ import {createFetcherStore} from '../utils/createFetcherStore'
12
+
13
+ const API_VERSION = 'v2025-02-19'
14
+
15
+ /**
16
+ * The list shape returned from `/organizations`, with `members` and/or
17
+ * `features` conditionally included based on the query options used.
18
+ * @public
19
+ */
20
+ export type Organizations<
21
+ IncludeMembers extends boolean = false,
22
+ IncludeFeatures extends boolean = false,
23
+ > = (Pick<
24
+ OrganizationBase,
25
+ | 'id'
26
+ | 'name'
27
+ | 'slug'
28
+ | 'createdAt'
29
+ | 'updatedAt'
30
+ | 'defaultRoleName'
31
+ | 'dashboardStatus'
32
+ | 'aiFeaturesStatus'
33
+ > &
34
+ // `boolean extends T` is non-distributive — true only when T is the wide
35
+ // `boolean`, in which case the field is optional. Literal `true`/`false`
36
+ // fall through to the strict branch.
37
+ (boolean extends IncludeMembers
38
+ ? {members?: OrganizationMember[]}
39
+ : IncludeMembers extends true
40
+ ? {members: OrganizationMember[]}
41
+ : unknown) &
42
+ (boolean extends IncludeFeatures
43
+ ? {features?: string[]}
44
+ : IncludeFeatures extends true
45
+ ? {features: string[]}
46
+ : unknown))[]
47
+
48
+ /** @public */
49
+ export interface OrganizationsOptions<
50
+ IncludeMembers extends boolean = false,
51
+ IncludeFeatures extends boolean = false,
52
+ > extends Omit<OrganizationOptions<IncludeMembers, IncludeFeatures>, 'organizationId'> {
53
+ /**
54
+ * When `true`, includes organisations the user has access to via
55
+ * project-level grants, not just direct organisation memberships.
56
+ */
57
+ includeImplicitMemberships?: boolean
58
+ }
59
+
60
+ function normalizeOrganizationOptions(options?: OrganizationsOptions<boolean, boolean>) {
61
+ return {
62
+ includeImplicitMemberships: options?.includeImplicitMemberships ?? false,
63
+ includeMembers: options?.includeMembers ?? false,
64
+ includeFeatures: options?.includeFeatures ?? false,
65
+ }
66
+ }
67
+
68
+ /** @internal */
69
+ export function getOrganizationsCacheKey(
70
+ _instance: SanityInstance,
71
+ options?: OrganizationsOptions<boolean, boolean>,
72
+ ): string {
73
+ const {includeMembers, includeFeatures, includeImplicitMemberships} =
74
+ normalizeOrganizationOptions(options)
75
+ const membersKey = includeMembers ? ':members' : ''
76
+ const featuresKey = includeFeatures ? ':features' : ''
77
+ const implicitKey = includeImplicitMemberships ? ':implicit' : ''
78
+ return `organizations${membersKey}${featuresKey}${implicitKey}`
79
+ }
80
+
81
+ const organizations = createFetcherStore({
82
+ name: 'Organizations',
83
+ getKey: getOrganizationsCacheKey,
84
+ fetcher: (instance) => (options?: OrganizationsOptions<boolean, boolean>) => {
85
+ return getClientState(instance, {
86
+ apiVersion: API_VERSION,
87
+ scope: 'global',
88
+ }).observable.pipe(
89
+ switchMap((client) => {
90
+ const normalized = normalizeOrganizationOptions(options)
91
+ const query = Object.fromEntries(
92
+ Object.entries(normalized)
93
+ .filter(([, value]) => value !== undefined)
94
+ .map(([key, value]) => [key, String(value)]),
95
+ )
96
+
97
+ return client.observable.request({
98
+ uri: `/organizations`,
99
+ query,
100
+ tag: 'organizations.get',
101
+ })
102
+ }),
103
+ )
104
+ },
105
+ })
106
+
107
+ /**
108
+ * Public signature for the organization state source. The conditional generics
109
+ * cannot flow through `BoundStoreAction`, so we declare the signature here
110
+ * and assign the (already-correct) runtime function to it.
111
+ */
112
+ type GetOrganizationsState = <
113
+ IncludeMembers extends boolean = false,
114
+ IncludeFeatures extends boolean = false,
115
+ >(
116
+ instance: SanityInstance,
117
+ options?: OrganizationsOptions<IncludeMembers, IncludeFeatures>,
118
+ ) => StateSource<Organizations<IncludeMembers, IncludeFeatures> | undefined>
119
+
120
+ type ResolveOrganizations = <
121
+ IncludeMembers extends boolean = false,
122
+ IncludeFeatures extends boolean = false,
123
+ >(
124
+ instance: SanityInstance,
125
+ options?: OrganizationsOptions<IncludeMembers, IncludeFeatures>,
126
+ ) => Promise<Organizations<IncludeMembers, IncludeFeatures>>
127
+
128
+ /** @public */
129
+ export const getOrganizationsState: GetOrganizationsState = organizations.getState
130
+
131
+ /** @public */
132
+ export const resolveOrganizations: ResolveOrganizations = organizations.resolveState
@@ -1,6 +1,6 @@
1
1
  import {type SanityClient} from '@sanity/client'
2
- import {delay, firstValueFrom, of, Subject} from 'rxjs'
3
- import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'
2
+ import {delay, firstValueFrom, type Observable, of, Subject} from 'rxjs'
3
+ import {afterEach, beforeEach, describe, expect, it, type Mock, vi} from 'vitest'
4
4
 
5
5
  import {getTokenState} from '../auth/authStore'
6
6
  import {getClient} from '../client/clientStore'
@@ -9,7 +9,7 @@ import {type SanityUser} from '../users/types'
9
9
  import {getUserState} from '../users/usersStore'
10
10
  import {createBifurTransport} from './bifurTransport'
11
11
  import {getPresence} from './presenceStore'
12
- import {type PresenceLocation, type TransportEvent} from './types'
12
+ import {type PresenceLocation, type TransportEvent, type TransportMessage} from './types'
13
13
 
14
14
  vi.mock('../auth/authStore')
15
15
  vi.mock('../client/clientStore')
@@ -21,8 +21,8 @@ describe('presenceStore', () => {
21
21
  let mockClient: SanityClient
22
22
  let mockTokenState: Subject<string | null>
23
23
  let mockIncomingEvents: Subject<TransportEvent>
24
- let mockDispatchMessage: ReturnType<typeof vi.fn>
25
- let mockGetUserState: ReturnType<typeof vi.fn>
24
+ let mockDispatchMessage: Mock<(message: TransportMessage) => Observable<void>>
25
+ let mockGetUserState: Mock<typeof getUserState>
26
26
 
27
27
  const mockUser: SanityUser = {
28
28
  sanityUserId: 'u123',
@@ -2,7 +2,7 @@ import {type SanityClient} from '@sanity/client'
2
2
  import {createImageUrlBuilder} from '@sanity/image-url'
3
3
 
4
4
  import {getClient} from '../client/clientStore'
5
- import {type DocumentResource, isDatasetResource} from '../config/sanityConfig'
5
+ import {type DocumentResource} from '../config/sanityConfig'
6
6
  import {type SanityInstance} from '../store/createSanityInstance'
7
7
  import {isObject} from '../utils/object'
8
8
  import {SUBTITLE_CANDIDATES, TITLE_CANDIDATES} from './previewConstants'
@@ -80,8 +80,7 @@ export function transformProjectionToPreview(
80
80
  // Get a client for the resource (if provided) or use the instance config
81
81
  const client = getClient(instance, {
82
82
  apiVersion: API_VERSION,
83
- // TODO: remove in v3 when we're ready for everything to be queried via resource
84
- resource: resource && !isDatasetResource(resource) ? resource : undefined,
83
+ resource,
85
84
  })
86
85
 
87
86
  return {
@@ -0,0 +1,93 @@
1
+ import {expectTypeOf, test} from 'vitest'
2
+
3
+ import {type SanityInstance} from '../store/createSanityInstance'
4
+ import {type StateSource} from '../store/createStateSourceAction'
5
+ import {
6
+ getProjectState,
7
+ type Project,
8
+ type ProjectBase,
9
+ type ProjectMember,
10
+ resolveProject,
11
+ } from './project'
12
+
13
+ const instance = {} as SanityInstance
14
+
15
+ test('resolveProject — default call: members and features both included by default', () => {
16
+ expectTypeOf(resolveProject(instance)).resolves.toEqualTypeOf<Project<true, true>>()
17
+ type Result = Awaited<ReturnType<typeof resolveProject<true, true>>>
18
+ expectTypeOf<Result['members']>().toEqualTypeOf<ProjectMember[]>()
19
+ })
20
+
21
+ test('resolveProject — includeMembers: false drops members from the type', () => {
22
+ expectTypeOf(resolveProject(instance, {includeMembers: false})).resolves.toEqualTypeOf<
23
+ Project<false, true>
24
+ >()
25
+ })
26
+
27
+ test('resolveProject — includeFeatures: false drops features from the type', () => {
28
+ expectTypeOf(resolveProject(instance, {includeFeatures: false})).resolves.toEqualTypeOf<
29
+ Project<true, false>
30
+ >()
31
+ })
32
+
33
+ test('resolveProject — both flags false → bare base shape', () => {
34
+ expectTypeOf(
35
+ resolveProject(instance, {includeMembers: false, includeFeatures: false}),
36
+ ).resolves.toEqualTypeOf<Project<false, false>>()
37
+ type Result = Awaited<ReturnType<typeof resolveProject<false, false>>>
38
+ expectTypeOf<Result['id']>().toEqualTypeOf<string>()
39
+ })
40
+
41
+ test('resolveProject — rejects non-boolean flag values', () => {
42
+ // @ts-expect-error — includeMembers must be a boolean
43
+ void resolveProject(instance, {includeMembers: 'yes'})
44
+ })
45
+
46
+ test('resolveProject — projectId alone does not change the data shape', () => {
47
+ expectTypeOf(resolveProject(instance, {projectId: 'p'})).resolves.toEqualTypeOf<
48
+ Project<true, true>
49
+ >()
50
+ })
51
+
52
+ test('resolveProject — non-literal boolean flag makes members optional', () => {
53
+ const includeMembers = false as boolean
54
+ expectTypeOf(resolveProject(instance, {includeMembers})).resolves.toEqualTypeOf<
55
+ Project<boolean, true>
56
+ >()
57
+ })
58
+
59
+ test('getProjectState — default call returns members + features StateSource', () => {
60
+ expectTypeOf(getProjectState(instance)).toEqualTypeOf<
61
+ StateSource<Project<true, true> | undefined>
62
+ >()
63
+ })
64
+
65
+ test('getProjectState — both flags false narrows to the bare base shape', () => {
66
+ expectTypeOf(
67
+ getProjectState(instance, {includeMembers: false, includeFeatures: false}),
68
+ ).toEqualTypeOf<StateSource<Project<false, false> | undefined>>()
69
+ })
70
+
71
+ test('Project — wide boolean for IncludeMembers makes members optional', () => {
72
+ expectTypeOf<Project<boolean, true>>().toEqualTypeOf<
73
+ ProjectBase & {members?: ProjectMember[]} & {features: string[]}
74
+ >()
75
+ expectTypeOf<Pick<Project<boolean, true>, 'members'>>().toEqualTypeOf<{
76
+ members?: ProjectMember[]
77
+ }>()
78
+ })
79
+
80
+ test('Project — wide boolean for IncludeFeatures makes features optional', () => {
81
+ expectTypeOf<Project<true, boolean>>().toEqualTypeOf<
82
+ ProjectBase & {members: ProjectMember[]} & {features?: string[]}
83
+ >()
84
+ expectTypeOf<Pick<Project<true, boolean>, 'features'>>().toEqualTypeOf<{
85
+ features?: string[]
86
+ }>()
87
+ })
88
+
89
+ test('Project — both wide booleans make both fields optional', () => {
90
+ expectTypeOf<Project<boolean, boolean>>().toEqualTypeOf<
91
+ ProjectBase & {members?: ProjectMember[]} & {features?: string[]}
92
+ >()
93
+ })