@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
@@ -1,35 +1,95 @@
1
1
  import {switchMap} from 'rxjs'
2
2
 
3
3
  import {getClientState} from '../client/clientStore'
4
+ import {type Project} from '../project/project'
5
+ import {type SanityInstance} from '../store/createSanityInstance'
6
+ import {type StateSource} from '../store/createStateSourceAction'
4
7
  import {createFetcherStore} from '../utils/createFetcherStore'
5
8
 
6
9
  const API_VERSION = 'v2025-02-19'
7
10
 
11
+ /** @public */
12
+ export interface ProjectsOptions<
13
+ IncludeMembers extends boolean = false,
14
+ IncludeFeatures extends boolean = true,
15
+ > {
16
+ organizationId?: string
17
+ includeMembers?: IncludeMembers
18
+ includeFeatures?: IncludeFeatures
19
+ onlyExplicitMembership?: boolean
20
+ }
21
+
22
+ function normalizeProjectsOptions(options?: ProjectsOptions<boolean, boolean>) {
23
+ return {
24
+ organizationId: options?.organizationId,
25
+ includeMembers: options?.includeMembers ?? false,
26
+ includeFeatures: options?.includeFeatures ?? true,
27
+ onlyExplicitMembership: options?.onlyExplicitMembership ?? false,
28
+ }
29
+ }
30
+
31
+ /** @internal */
32
+ export function getProjectsCacheKey(
33
+ _instance: SanityInstance,
34
+ options?: ProjectsOptions<boolean, boolean>,
35
+ ): string {
36
+ const {organizationId, includeMembers, includeFeatures, onlyExplicitMembership} =
37
+ normalizeProjectsOptions(options)
38
+ const orgKey = organizationId ? `:org:${organizationId}` : ''
39
+ const membersKey = includeMembers ? ':members' : ''
40
+ const featuresKey = includeFeatures ? ':features' : ''
41
+ const explicitKey = onlyExplicitMembership ? ':explicit' : ''
42
+ return `projects${orgKey}${membersKey}${featuresKey}${explicitKey}`
43
+ }
44
+
8
45
  const projects = createFetcherStore({
9
46
  name: 'Projects',
10
- getKey: (_instance, options?: {organizationId?: string; includeMembers?: boolean}) => {
11
- const orgKey = options?.organizationId ? `:org:${options.organizationId}` : ''
12
- const membersKey = options?.includeMembers === false ? ':no-members' : ''
13
- return `projects${orgKey}${membersKey}`
14
- },
15
- fetcher: (instance) => (options?: {organizationId?: string; includeMembers?: boolean}) =>
47
+ getKey: getProjectsCacheKey,
48
+ fetcher: (instance) => (options?: ProjectsOptions<boolean, boolean>) =>
16
49
  getClientState(instance, {
17
50
  apiVersion: API_VERSION,
18
51
  scope: 'global',
19
- requestTagPrefix: 'sanity.sdk.projects',
20
52
  }).observable.pipe(
21
53
  switchMap((client) => {
22
- const organizationId = options?.organizationId
23
- return client.observable.projects.list({
24
- // client method has a type that expects false | undefined
25
- includeMembers: !options?.includeMembers ? false : undefined,
26
- organizationId,
54
+ const normalized = normalizeProjectsOptions(options)
55
+ const query = Object.fromEntries(
56
+ Object.entries(normalized)
57
+ .filter(([, value]) => value !== undefined)
58
+ .map(([key, value]) => [key, String(value)]),
59
+ )
60
+
61
+ return client.observable.request({
62
+ uri: '/projects',
63
+ query,
64
+ tag: 'projects.get',
27
65
  })
28
66
  }),
29
67
  ),
30
68
  })
31
69
 
70
+ /**
71
+ * Public signature for the projects state source. The conditional generics
72
+ * cannot flow through `BoundStoreAction`, so we declare the signature here
73
+ * and assign the (already-correct) runtime function to it.
74
+ */
75
+ type GetProjectsState = <
76
+ IncludeMembers extends boolean = false,
77
+ IncludeFeatures extends boolean = true,
78
+ >(
79
+ instance: SanityInstance,
80
+ options?: ProjectsOptions<IncludeMembers, IncludeFeatures>,
81
+ ) => StateSource<Project<IncludeMembers, IncludeFeatures>[] | undefined>
82
+
83
+ type ResolveProjects = <
84
+ IncludeMembers extends boolean = false,
85
+ IncludeFeatures extends boolean = true,
86
+ >(
87
+ instance: SanityInstance,
88
+ options?: ProjectsOptions<IncludeMembers, IncludeFeatures>,
89
+ ) => Promise<Project<IncludeMembers, IncludeFeatures>[]>
90
+
32
91
  /** @public */
33
- export const getProjectsState = projects.getState
92
+ export const getProjectsState: GetProjectsState = projects.getState
93
+
34
94
  /** @public */
35
- export const resolveProjects = projects.resolveState
95
+ export const resolveProjects: ResolveProjects = projects.resolveState
@@ -24,7 +24,7 @@ import {
24
24
  } from 'rxjs'
25
25
 
26
26
  import {getClientState} from '../client/clientStore'
27
- import {type DatasetHandle, isDatasetResource} from '../config/sanityConfig'
27
+ import {type DatasetHandle} from '../config/sanityConfig'
28
28
  /*
29
29
  * Although this is an import dependency cycle, it is not a logical cycle:
30
30
  * 1. queryStore uses getPerspectiveState when resolving release perspectives
@@ -230,8 +230,7 @@ const listenToLiveClientAndSetLastLiveEventIds = ({
230
230
  }: StoreContext<QueryStoreState, BoundResourceKey>) => {
231
231
  const liveMessages$ = getClientState(instance, {
232
232
  apiVersion: QUERY_STORE_API_VERSION,
233
- // temporary guard here until we're ready for everything to be queried via global api
234
- ...(resource && !isDatasetResource(resource) ? {resource} : {}),
233
+ resource,
235
234
  }).observable.pipe(
236
235
  switchMap((client) =>
237
236
  defer(() =>
@@ -39,8 +39,8 @@ describe('releasesStore', () => {
39
39
  expect.objectContaining({
40
40
  query: 'releases::all()',
41
41
  perspective: 'raw',
42
- resource: undefined,
43
42
  tag: 'releases',
43
+ resource: {dataset: 'test', projectId: 'test'},
44
44
  }),
45
45
  )
46
46
  })
@@ -1,7 +1,7 @@
1
1
  import {type SanityDocument} from '@sanity/types'
2
2
  import {map} from 'rxjs'
3
3
 
4
- import {type DocumentResource, isDatasetResource} from '../config/sanityConfig'
4
+ import {type DocumentResource} from '../config/sanityConfig'
5
5
  /*
6
6
  * Although this is an import dependency cycle, it is not a logical cycle:
7
7
  * 1. releasesStore uses queryStore as a data source
@@ -84,7 +84,7 @@ const subscribeToReleases = ({
84
84
  const {observable: releases$} = getQueryState<ReleaseDocument[]>(instance, {
85
85
  query: RELEASES_QUERY,
86
86
  perspective: 'raw',
87
- resource: resource && !isDatasetResource(resource) ? resource : undefined,
87
+ resource,
88
88
  tag: 'releases',
89
89
  })
90
90
  return releases$
@@ -5,7 +5,6 @@ import {pickProperties} from '../utils/object'
5
5
 
6
6
  /**
7
7
  * Represents a Sanity.io resource instance with its own configuration and lifecycle
8
- * @remarks Instances form a hierarchy through parent/child relationships
9
8
  *
10
9
  * @public
11
10
  */
@@ -18,7 +17,6 @@ export interface SanityInstance {
18
17
 
19
18
  /**
20
19
  * Resolved configuration for this instance
21
- * @remarks Merges values from parent instances where appropriate
22
20
  */
23
21
  readonly config: SanityConfig
24
22
 
@@ -44,13 +42,14 @@ export interface SanityInstance {
44
42
  /**
45
43
  * Gets the parent instance in the hierarchy
46
44
  * @returns Parent instance or undefined if this is the root
45
+ * @deprecated The parent/child instance hierarchy is deprecated. Use a single SanityInstance instead.
47
46
  */
48
47
  getParent(): SanityInstance | undefined
49
48
 
50
49
  /**
51
50
  * Creates a child instance with merged configuration
52
51
  * @param config - Configuration to merge with parent values
53
- * @remarks Child instances inherit parent configuration but can override values
52
+ * @deprecated The parent/child instance hierarchy is deprecated. Use a single SanityInstance instead.
54
53
  */
55
54
  createChild(config: SanityConfig): SanityInstance
56
55
 
@@ -59,6 +58,7 @@ export interface SanityInstance {
59
58
  * matches the given target config using a shallow comparison.
60
59
  * @param targetConfig - A partial configuration object containing key-value pairs to match.
61
60
  * @returns The first matching instance or undefined if no match is found.
61
+ * @deprecated The parent/child instance hierarchy is deprecated. Use a single SanityInstance instead.
62
62
  */
63
63
  match(targetConfig: Partial<SanityConfig>): SanityInstance | undefined
64
64
  }
@@ -30,6 +30,14 @@ describe('isDevMode', () => {
30
30
  expect(isDevMode()).toBe(true)
31
31
  })
32
32
 
33
+ it('returns true on localhost even when NODE_ENV is production', () => {
34
+ vi.stubEnv('NODE_ENV', 'production')
35
+ vi.stubGlobal('window', {
36
+ location: {href: 'http://localhost:3000/'},
37
+ })
38
+ expect(isDevMode()).toBe(true)
39
+ })
40
+
33
41
  it('returns false for a non-local URL', () => {
34
42
  vi.stubEnv('NODE_ENV', 'test')
35
43
  vi.stubGlobal('window', {
@@ -17,21 +17,22 @@ function isLocalUrl(win: Window): boolean {
17
17
  }
18
18
 
19
19
  /**
20
- * Determines whether the SDK should enable dev-mode telemetry.
20
+ * Determines whether the SDK should enable dev-mode telemetry for the
21
+ * SDK consumer (i.e. a developer building an app with `@sanity/sdk`).
21
22
  *
22
- * Combines a browser URL check (localhost/127.0.0.1) with a Node.js
23
- * environment variable check (`NODE_ENV === 'development'`). Returns
24
- * false in production environments so bundlers can tree-shake the
25
- * telemetry code path entirely.
23
+ * Browser: returns true only when the URL is `localhost` or `127.0.0.1`.
24
+ * The URL check is the primary signal because consumer bundlers may or
25
+ * may not forward `NODE_ENV` to the browser reliably.
26
+ *
27
+ * Node (scripts / non-browser): falls back to `NODE_ENV === 'development'`.
28
+ *
29
+ * Bracket-notation `process.env['NODE_ENV']` is used to avoid bundler
30
+ * dead-code replacement.
26
31
  *
27
32
  * @returns True if the SDK is running in a development environment
28
33
  * @internal
29
34
  */
30
35
  export function isDevMode(): boolean {
31
- if (typeof process !== 'undefined' && process.env?.['NODE_ENV'] === 'production') {
32
- return false
33
- }
34
-
35
36
  if (typeof window !== 'undefined') {
36
37
  return isLocalUrl(window)
37
38
  }
@@ -205,21 +205,4 @@ describe('initTelemetry', () => {
205
205
 
206
206
  instance.dispose()
207
207
  })
208
-
209
- it('finds manager through parent-child instance chain', async () => {
210
- vi.mocked(isDevMode).mockReturnValue(true)
211
-
212
- const root = createSanityInstance()
213
- const child = root.createChild({})
214
-
215
- initTelemetry(root, 'abc123')
216
- await flushPromises()
217
-
218
- trackHookMounted(child, 'useUsers')
219
-
220
- const manager = vi.mocked(createTelemetryManager).mock.results[0].value
221
- expect(manager.logHookFirstUsed).toHaveBeenCalledWith('useUsers')
222
-
223
- root.dispose()
224
- })
225
208
  })
@@ -187,19 +187,9 @@ export function trackHookMounted(instance: SanityInstance, hookName: string): vo
187
187
  }
188
188
 
189
189
  function findManager(instance: SanityInstance): TelemetryManager | undefined {
190
- let current: SanityInstance | undefined = instance
191
- while (current) {
192
- const manager = telemetryManagers.get(current)
193
- if (manager) return manager
194
- current = typeof current.getParent === 'function' ? current.getParent() : undefined
195
- }
196
- return undefined
190
+ return telemetryManagers.get(instance)
197
191
  }
198
192
 
199
193
  function getRootInstance(instance: SanityInstance): SanityInstance {
200
- let current = instance
201
- while (typeof current.getParent === 'function' && current.getParent()) {
202
- current = current.getParent()!
203
- }
204
- return current
194
+ return instance
205
195
  }