@sanity/sdk 2.5.0 → 2.6.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanity/sdk",
3
- "version": "2.5.0",
3
+ "version": "2.6.0",
4
4
  "private": false,
5
5
  "description": "Sanity SDK",
6
6
  "keywords": [
@@ -51,7 +51,7 @@
51
51
  "@sanity/json-match": "^1.0.5",
52
52
  "@sanity/message-protocol": "^0.18.0",
53
53
  "@sanity/mutate": "^0.12.4",
54
- "@sanity/types": "^3.83.0",
54
+ "@sanity/types": "^5.2.0",
55
55
  "groq": "3.88.1-typegen-experimental.0",
56
56
  "groq-js": "^1.19.0",
57
57
  "lodash-es": "^4.17.21",
@@ -72,10 +72,10 @@
72
72
  "vite": "^6.3.4",
73
73
  "vitest": "^3.2.4",
74
74
  "@repo/config-eslint": "0.0.0",
75
- "@repo/config-test": "0.0.1",
76
75
  "@repo/package.bundle": "3.82.0",
76
+ "@repo/tsconfig": "0.0.1",
77
77
  "@repo/package.config": "0.0.1",
78
- "@repo/tsconfig": "0.0.1"
78
+ "@repo/config-test": "0.0.1"
79
79
  },
80
80
  "engines": {
81
81
  "node": ">=20.19"
@@ -76,13 +76,25 @@ export {
76
76
  createProjectHandle,
77
77
  } from '../config/handles'
78
78
  export {
79
- canvasSource,
79
+ configureLogging,
80
+ type InstanceContext,
81
+ type LogContext,
82
+ type Logger,
83
+ type LoggerConfig,
84
+ type LogLevel,
85
+ type LogNamespace,
86
+ } from '../config/loggingConfig'
87
+ export {
88
+ type CanvasSource,
80
89
  type DatasetHandle,
81
- datasetSource,
90
+ type DatasetSource,
82
91
  type DocumentHandle,
83
92
  type DocumentSource,
84
93
  type DocumentTypeHandle,
85
- mediaLibrarySource,
94
+ isCanvasSource,
95
+ isDatasetSource,
96
+ isMediaLibrarySource,
97
+ type MediaLibrarySource,
86
98
  type PerspectiveHandle,
87
99
  type ProjectHandle,
88
100
  type ReleasePerspective,
@@ -3,7 +3,6 @@ import {Subject} from 'rxjs'
3
3
  import {beforeEach, describe, expect, it, vi} from 'vitest'
4
4
 
5
5
  import {getAuthMethodState, getTokenState} from '../auth/authStore'
6
- import {canvasSource, datasetSource, mediaLibrarySource} from '../config/sanityConfig'
7
6
  import {createSanityInstance, type SanityInstance} from '../store/createSanityInstance'
8
7
  import {getClient, getClientState} from './clientStore'
9
8
 
@@ -34,7 +33,10 @@ beforeEach(() => {
34
33
  vi.mocked(createClient).mockImplementation(
35
34
  (clientConfig) => ({config: () => clientConfig}) as SanityClient,
36
35
  )
37
- instance = createSanityInstance({projectId: 'test-project', dataset: 'test-dataset'})
36
+ instance = createSanityInstance({
37
+ projectId: 'test-project',
38
+ dataset: 'test-dataset',
39
+ })
38
40
  })
39
41
 
40
42
  afterEach(() => {
@@ -176,18 +178,15 @@ describe('clientStore', () => {
176
178
 
177
179
  describe('source handling', () => {
178
180
  it('should create client when source is provided', () => {
179
- const source = datasetSource('source-project', 'source-dataset')
180
- const client = getClient(instance, {apiVersion: '2024-11-12', source})
181
+ const client = getClient(instance, {
182
+ apiVersion: '2024-11-12',
183
+ source: {projectId: 'source-project', dataset: 'source-dataset'},
184
+ })
181
185
 
182
186
  expect(vi.mocked(createClient)).toHaveBeenCalledWith(
183
187
  expect.objectContaining({
184
- apiVersion: '2024-11-12',
185
- source: expect.objectContaining({
186
- __sanity_internal_sourceId: {
187
- projectId: 'source-project',
188
- dataset: 'source-dataset',
189
- },
190
- }),
188
+ 'apiVersion': '2024-11-12',
189
+ '~experimental_resource': {type: 'dataset', id: 'source-project.source-dataset'},
191
190
  }),
192
191
  )
193
192
  // Client should be projectless - no projectId/dataset in config
@@ -195,19 +194,16 @@ describe('clientStore', () => {
195
194
  expect(client.config()).not.toHaveProperty('dataset')
196
195
  expect(client.config()).toEqual(
197
196
  expect.objectContaining({
198
- source: expect.objectContaining({
199
- __sanity_internal_sourceId: {
200
- projectId: 'source-project',
201
- dataset: 'source-dataset',
202
- },
203
- }),
197
+ '~experimental_resource': {type: 'dataset', id: 'source-project.source-dataset'},
204
198
  }),
205
199
  )
206
200
  })
207
201
 
208
202
  it('should create resource when source has array sourceId and be projectless', () => {
209
- const source = mediaLibrarySource('media-lib-123')
210
- const client = getClient(instance, {apiVersion: '2024-11-12', source})
203
+ const client = getClient(instance, {
204
+ apiVersion: '2024-11-12',
205
+ source: {mediaLibraryId: 'media-lib-123'},
206
+ })
211
207
 
212
208
  expect(vi.mocked(createClient)).toHaveBeenCalledWith(
213
209
  expect.objectContaining({
@@ -226,8 +222,10 @@ describe('clientStore', () => {
226
222
  })
227
223
 
228
224
  it('should create resource when canvas source is provided and be projectless', () => {
229
- const source = canvasSource('canvas-123')
230
- const client = getClient(instance, {apiVersion: '2024-11-12', source})
225
+ const client = getClient(instance, {
226
+ apiVersion: '2024-11-12',
227
+ source: {canvasId: 'canvas-123'},
228
+ })
231
229
 
232
230
  expect(vi.mocked(createClient)).toHaveBeenCalledWith(
233
231
  expect.objectContaining({
@@ -246,30 +244,26 @@ describe('clientStore', () => {
246
244
  })
247
245
 
248
246
  it('should create projectless client when source is provided, ignoring instance config', () => {
249
- const source = datasetSource('source-project', 'source-dataset')
250
- const client = getClient(instance, {apiVersion: '2024-11-12', source})
247
+ const client = getClient(instance, {
248
+ apiVersion: '2024-11-12',
249
+ source: {projectId: 'source-project', dataset: 'source-dataset'},
250
+ })
251
251
 
252
252
  // Client should be projectless - source takes precedence, instance config is ignored
253
253
  expect(client.config()).not.toHaveProperty('projectId')
254
254
  expect(client.config()).not.toHaveProperty('dataset')
255
255
  expect(client.config()).toEqual(
256
256
  expect.objectContaining({
257
- source: expect.objectContaining({
258
- __sanity_internal_sourceId: {
259
- projectId: 'source-project',
260
- dataset: 'source-dataset',
261
- },
262
- }),
257
+ '~experimental_resource': {type: 'dataset', id: 'source-project.source-dataset'},
263
258
  }),
264
259
  )
265
260
  })
266
261
 
267
262
  it('should warn when both source and explicit projectId/dataset are provided', () => {
268
263
  const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
269
- const source = datasetSource('source-project', 'source-dataset')
270
264
  const client = getClient(instance, {
271
265
  apiVersion: '2024-11-12',
272
- source,
266
+ source: {projectId: 'source-project', dataset: 'source-dataset'},
273
267
  projectId: 'explicit-project',
274
268
  dataset: 'explicit-dataset',
275
269
  })
@@ -284,13 +278,18 @@ describe('clientStore', () => {
284
278
  })
285
279
 
286
280
  it('should create different clients for different sources', () => {
287
- const source1 = datasetSource('project-1', 'dataset-1')
288
- const source2 = datasetSource('project-2', 'dataset-2')
289
- const source3 = mediaLibrarySource('media-lib-1')
290
-
291
- const client1 = getClient(instance, {apiVersion: '2024-11-12', source: source1})
292
- const client2 = getClient(instance, {apiVersion: '2024-11-12', source: source2})
293
- const client3 = getClient(instance, {apiVersion: '2024-11-12', source: source3})
281
+ const client1 = getClient(instance, {
282
+ apiVersion: '2024-11-12',
283
+ source: {projectId: 'source-project', dataset: 'source-dataset'},
284
+ })
285
+ const client2 = getClient(instance, {
286
+ apiVersion: '2024-11-12',
287
+ source: {mediaLibraryId: 'media-lib-123'},
288
+ })
289
+ const client3 = getClient(instance, {
290
+ apiVersion: '2024-11-12',
291
+ source: {canvasId: 'canvas-123'},
292
+ })
294
293
 
295
294
  expect(client1).not.toBe(client2)
296
295
  expect(client2).not.toBe(client3)
@@ -299,11 +298,14 @@ describe('clientStore', () => {
299
298
  })
300
299
 
301
300
  it('should reuse clients with identical source configurations', () => {
302
- const source = datasetSource('same-project', 'same-dataset')
303
- const options = {apiVersion: '2024-11-12', source}
304
-
305
- const client1 = getClient(instance, options)
306
- const client2 = getClient(instance, options)
301
+ const client1 = getClient(instance, {
302
+ apiVersion: '2024-11-12',
303
+ source: {projectId: 'source-project', dataset: 'source-dataset'},
304
+ })
305
+ const client2 = getClient(instance, {
306
+ apiVersion: '2024-11-12',
307
+ source: {projectId: 'source-project', dataset: 'source-dataset'},
308
+ })
307
309
 
308
310
  expect(client1).toBe(client2)
309
311
  expect(vi.mocked(createClient)).toHaveBeenCalledTimes(1)
@@ -2,7 +2,12 @@ import {type ClientConfig, createClient, type SanityClient} from '@sanity/client
2
2
  import {pick} from 'lodash-es'
3
3
 
4
4
  import {getAuthMethodState, getTokenState} from '../auth/authStore'
5
- import {type DocumentSource, SOURCE_ID} from '../config/sanityConfig'
5
+ import {
6
+ type DocumentSource,
7
+ isCanvasSource,
8
+ isDatasetSource,
9
+ isMediaLibrarySource,
10
+ } from '../config/sanityConfig'
6
11
  import {bindActionGlobally} from '../store/createActionBinder'
7
12
  import {createStateSourceAction} from '../store/createStateSourceAction'
8
13
  import {defineStore, type StoreContext} from '../store/defineStore'
@@ -61,6 +66,11 @@ export interface ClientStoreState {
61
66
  authMethod?: 'localstorage' | 'cookie'
62
67
  }
63
68
 
69
+ interface ClientResource {
70
+ type: 'dataset' | 'media-library' | 'canvas'
71
+ id: string
72
+ }
73
+
64
74
  /**
65
75
  * Options used when retrieving a client instance from the client store.
66
76
  *
@@ -170,13 +180,17 @@ export const getClient = bindActionGlobally(
170
180
 
171
181
  const tokenFromState = state.get().token
172
182
  const {clients, authMethod} = state.get()
173
- const hasSource = !!options.source
174
- let sourceId = options.source?.[SOURCE_ID]
175
183
 
176
- let resource
177
- if (Array.isArray(sourceId)) {
178
- resource = {type: sourceId[0], id: sourceId[1]}
179
- sourceId = undefined
184
+ let resource: ClientResource | undefined
185
+
186
+ if (options.source) {
187
+ if (isDatasetSource(options.source)) {
188
+ resource = {type: 'dataset', id: `${options.source.projectId}.${options.source.dataset}`}
189
+ } else if (isMediaLibrarySource(options.source)) {
190
+ resource = {type: 'media-library', id: options.source.mediaLibraryId}
191
+ } else if (isCanvasSource(options.source)) {
192
+ resource = {type: 'canvas', id: options.source.canvasId}
193
+ }
180
194
  }
181
195
 
182
196
  const projectId = options.projectId ?? instance.config.projectId
@@ -185,7 +199,7 @@ export const getClient = bindActionGlobally(
185
199
 
186
200
  const effectiveOptions: ClientOptions = {
187
201
  ...DEFAULT_CLIENT_CONFIG,
188
- ...((options.scope === 'global' || !projectId || hasSource) && {useProjectHostname: false}),
202
+ ...((options.scope === 'global' || !projectId || resource) && {useProjectHostname: false}),
189
203
  token: authMethod === 'cookie' ? undefined : (tokenFromState ?? undefined),
190
204
  ...options,
191
205
  ...(projectId && {projectId}),
@@ -197,7 +211,7 @@ export const getClient = bindActionGlobally(
197
211
  // When a source is provided, don't use projectId/dataset - the client should be "projectless"
198
212
  // The client code itself will ignore the non-source config, so we do this to prevent confusing the user.
199
213
  // (ref: https://github.com/sanity-io/client/blob/5c23f81f5ab93a53f5b22b39845c867988508d84/src/data/dataMethods.ts#L691)
200
- if (hasSource) {
214
+ if (resource) {
201
215
  if (options.projectId || options.dataset) {
202
216
  // eslint-disable-next-line no-console
203
217
  console.warn(
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Public API for configuring SDK logging
3
+ *
4
+ * @module loggingConfig
5
+ */
6
+
7
+ import {configureLogging as _configureLogging, type LoggerConfig} from '../utils/logger'
8
+
9
+ /**
10
+ * Configure logging for the Sanity SDK
11
+ *
12
+ * This function allows you to control what logs are output by the SDK,
13
+ * making it easier to debug issues in development or production.
14
+ *
15
+ * @remarks
16
+ * **Zero-Config via Environment Variable (Recommended):**
17
+ *
18
+ * The SDK automatically reads the `DEBUG` environment variable, making it
19
+ * easy to enable logging without code changes:
20
+ *
21
+ * ```bash
22
+ * # Enable all SDK logging at debug level
23
+ * DEBUG=sanity:* npm start
24
+ *
25
+ * # Enable specific namespaces
26
+ * DEBUG=sanity:auth,sanity:document npm start
27
+ *
28
+ * # Enable trace level for all namespaces
29
+ * DEBUG=sanity:trace:* npm start
30
+ *
31
+ * # Enable internal/maintainer logs
32
+ * DEBUG=sanity:*:internal npm start
33
+ * ```
34
+ *
35
+ * This matches the pattern used by Sanity CLI and Studio, making it familiar
36
+ * and easy for support teams to help troubleshoot issues.
37
+ *
38
+ * **Programmatic Configuration (Advanced):**
39
+ *
40
+ * For more control (custom handlers, dynamic configuration), call this function
41
+ * explicitly. Programmatic configuration overrides environment variables.
42
+ *
43
+ * **For Application Developers:**
44
+ * Use `info`, `warn`, or `error` levels to see high-level SDK activity
45
+ * without being overwhelmed by internal details.
46
+ *
47
+ * **For SDK Maintainers:**
48
+ * Use `debug` or `trace` levels with `internal: true` to see detailed
49
+ * information about store operations, RxJS streams, and state transitions.
50
+ *
51
+ * **Instance Context:**
52
+ * Logs automatically include instance information (projectId, dataset, instanceId)
53
+ * when available, making it easier to debug multi-instance scenarios:
54
+ * ```
55
+ * [INFO] [auth] [project:abc] [dataset:production] User logged in
56
+ * ```
57
+ *
58
+ * **Available Namespaces:**
59
+ * - `sdk` - SDK initialization, configuration, and lifecycle
60
+ * - `auth` - Authentication and authorization (when instrumented in the future)
61
+ * - And more as logging is added to modules
62
+ *
63
+ * @example Zero-config via environment variable (recommended for debugging)
64
+ * ```bash
65
+ * # Just set DEBUG and run your app - no code changes needed!
66
+ * DEBUG=sanity:* npm start
67
+ * ```
68
+ *
69
+ * @example Programmatic configuration (application developer)
70
+ * ```ts
71
+ * import {configureLogging} from '@sanity/sdk'
72
+ *
73
+ * // Log warnings and errors for auth and document operations
74
+ * configureLogging({
75
+ * level: 'warn',
76
+ * namespaces: ['auth', 'document']
77
+ * })
78
+ * ```
79
+ *
80
+ * @example Programmatic configuration (SDK maintainer)
81
+ * ```ts
82
+ * import {configureLogging} from '@sanity/sdk'
83
+ *
84
+ * // Enable all logs including internal traces
85
+ * configureLogging({
86
+ * level: 'trace',
87
+ * namespaces: ['*'],
88
+ * internal: true
89
+ * })
90
+ * ```
91
+ *
92
+ * @example Custom handler (for testing)
93
+ * ```ts
94
+ * import {configureLogging} from '@sanity/sdk'
95
+ *
96
+ * const logs: string[] = []
97
+ * configureLogging({
98
+ * level: 'info',
99
+ * namespaces: ['*'],
100
+ * handler: {
101
+ * error: (msg) => logs.push(msg),
102
+ * warn: (msg) => logs.push(msg),
103
+ * info: (msg) => logs.push(msg),
104
+ * debug: (msg) => logs.push(msg),
105
+ * trace: (msg) => logs.push(msg),
106
+ * }
107
+ * })
108
+ * ```
109
+ *
110
+ * @public
111
+ */
112
+ export function configureLogging(config: LoggerConfig): void {
113
+ _configureLogging(config)
114
+
115
+ // Always log configuration (bypasses namespace filtering)
116
+ // This ensures users see the message regardless of which namespaces they enable
117
+ const configLevel = config.level || 'warn'
118
+ const shouldLog = ['info', 'debug', 'trace'].includes(configLevel) || configLevel === 'warn'
119
+
120
+ if (shouldLog && config.handler?.info) {
121
+ config.handler.info(`[${new Date().toISOString()}] [INFO] [sdk] Logging configured`, {
122
+ level: configLevel,
123
+ namespaces: config.namespaces || [],
124
+ internal: config.internal || false,
125
+ source: 'programmatic',
126
+ })
127
+ } else if (shouldLog) {
128
+ // eslint-disable-next-line no-console
129
+ console.info(`[${new Date().toISOString()}] [INFO] [sdk] Logging configured`, {
130
+ level: configLevel,
131
+ namespaces: config.namespaces || [],
132
+ internal: config.internal || false,
133
+ source: 'programmatic',
134
+ })
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Re-export types for public API
140
+ * @public
141
+ */
142
+ export type {
143
+ InstanceContext,
144
+ LogContext,
145
+ Logger,
146
+ LoggerConfig,
147
+ LogLevel,
148
+ LogNamespace,
149
+ } from '../utils/logger'
@@ -31,6 +31,16 @@ export interface PerspectiveHandle {
31
31
  export interface DatasetHandle<TDataset extends string = string, TProjectId extends string = string>
32
32
  extends ProjectHandle<TProjectId>, PerspectiveHandle {
33
33
  dataset?: TDataset
34
+ /**
35
+ * @beta
36
+ * The name of the source to use for this operation.
37
+ */
38
+ sourceName?: string
39
+ /**
40
+ * @beta
41
+ * Explicit source object to use for this operation.
42
+ */
43
+ source?: DocumentSource
34
44
  }
35
45
 
36
46
  /**
@@ -85,45 +95,54 @@ export interface SanityConfig extends DatasetHandle, PerspectiveHandle {
85
95
  studioMode?: {
86
96
  enabled: boolean
87
97
  }
88
- }
89
98
 
90
- export const SOURCE_ID = '__sanity_internal_sourceId'
99
+ /**
100
+ * @beta
101
+ * A list of named sources to use for this instance.
102
+ */
103
+ sources?: Record<string, DocumentSource>
104
+ }
91
105
 
92
106
  /**
93
107
  * A document source can be used for querying.
108
+ * This will soon be the default way to identify where you are querying from.
94
109
  *
95
110
  * @beta
96
- * @see datasetSource Construct a document source for a given projectId and dataset.
97
- * @see mediaLibrarySource Construct a document source for a mediaLibraryId.
98
- * @see canvasSource Construct a document source for a canvasId.
99
111
  */
100
- export type DocumentSource = {
101
- [SOURCE_ID]: ['media-library', string] | ['canvas', string] | {projectId: string; dataset: string}
102
- }
112
+ export type DocumentSource = DatasetSource | MediaLibrarySource | CanvasSource
113
+
114
+ /**
115
+ * @beta
116
+ */
117
+ export type DatasetSource = {projectId: string; dataset: string}
118
+
119
+ /**
120
+ * @beta
121
+ */
122
+ export type MediaLibrarySource = {mediaLibraryId: string}
103
123
 
104
124
  /**
105
- * Returns a document source for a projectId and dataset.
106
- *
107
125
  * @beta
108
126
  */
109
- export function datasetSource(projectId: string, dataset: string): DocumentSource {
110
- return {[SOURCE_ID]: {projectId, dataset}}
127
+ export type CanvasSource = {canvasId: string}
128
+
129
+ /**
130
+ * @beta
131
+ */
132
+ export function isDatasetSource(source: DocumentSource): source is DatasetSource {
133
+ return 'projectId' in source && 'dataset' in source
111
134
  }
112
135
 
113
136
  /**
114
- * Returns a document source for a Media Library.
115
- *
116
137
  * @beta
117
138
  */
118
- export function mediaLibrarySource(id: string): DocumentSource {
119
- return {[SOURCE_ID]: ['media-library', id]}
139
+ export function isMediaLibrarySource(source: DocumentSource): source is MediaLibrarySource {
140
+ return 'mediaLibraryId' in source
120
141
  }
121
142
 
122
143
  /**
123
- * Returns a document source for a Canvas.
124
- *
125
144
  * @beta
126
145
  */
127
- export function canvasSource(id: string): DocumentSource {
128
- return {[SOURCE_ID]: ['canvas', id]}
146
+ export function isCanvasSource(source: DocumentSource): source is CanvasSource {
147
+ return 'canvasId' in source
129
148
  }
@@ -58,7 +58,9 @@ export interface QueryOptions<
58
58
  TQuery extends string = string,
59
59
  TDataset extends string = string,
60
60
  TProjectId extends string = string,
61
- > extends Pick<ResponseQueryOptions, 'useCdn' | 'cache' | 'next' | 'cacheMode' | 'tag'>,
61
+ >
62
+ extends
63
+ Pick<ResponseQueryOptions, 'useCdn' | 'cache' | 'next' | 'cacheMode' | 'tag'>,
62
64
  DatasetHandle<TDataset, TProjectId> {
63
65
  query: TQuery
64
66
  params?: Record<string, unknown>
@@ -1,4 +1,9 @@
1
- import {type DocumentSource, SOURCE_ID} from '../config/sanityConfig'
1
+ import {
2
+ type DocumentSource,
3
+ isCanvasSource,
4
+ isDatasetSource,
5
+ isMediaLibrarySource,
6
+ } from '../config/sanityConfig'
2
7
  import {type SanityInstance} from './createSanityInstance'
3
8
  import {createStoreInstance, type StoreInstance} from './createStoreInstance'
4
9
  import {type StoreState} from './createStoreState'
@@ -157,12 +162,18 @@ export const bindActionBySource = createActionBinder<
157
162
  [{source?: DocumentSource}, ...unknown[]]
158
163
  >((instance, {source}) => {
159
164
  if (source) {
160
- const id = source[SOURCE_ID]
161
- if (!id) throw new Error('Invalid source (missing ID information)')
162
- if (Array.isArray(id)) return {name: id.join(':')}
163
- return {name: `${id.projectId}.${id.dataset}`}
164
- }
165
+ let id: string | undefined
166
+ if (isDatasetSource(source)) {
167
+ id = `${source.projectId}.${source.dataset}`
168
+ } else if (isMediaLibrarySource(source)) {
169
+ id = `media-library:${source.mediaLibraryId}`
170
+ } else if (isCanvasSource(source)) {
171
+ id = `canvas:${source.canvasId}`
172
+ }
165
173
 
174
+ if (!id) throw new Error(`Received invalid source: ${JSON.stringify(source)}`)
175
+ return {name: id}
176
+ }
166
177
  const {projectId, dataset} = instance.config
167
178
 
168
179
  if (!projectId || !dataset) {
@@ -1,5 +1,6 @@
1
- import {describe, expect, it, vi} from 'vitest'
1
+ import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'
2
2
 
3
+ import {configureLogging, type LogHandler, resetLogging} from '../utils/logger'
3
4
  import {createSanityInstance} from './createSanityInstance'
4
5
 
5
6
  describe('createSanityInstance', () => {
@@ -81,4 +82,87 @@ describe('createSanityInstance', () => {
81
82
  const child = parent.createChild({auth: {token: 'my-token'}})
82
83
  expect(child.config.auth).toEqual({apiHost: 'api.sanity.work', token: 'my-token'})
83
84
  })
85
+
86
+ describe('logging', () => {
87
+ const mockHandler: LogHandler = {
88
+ error: vi.fn(),
89
+ warn: vi.fn(),
90
+ info: vi.fn(),
91
+ debug: vi.fn(),
92
+ trace: vi.fn(),
93
+ }
94
+
95
+ beforeEach(() => {
96
+ vi.clearAllMocks()
97
+ configureLogging({
98
+ level: 'debug',
99
+ namespaces: ['sdk'],
100
+ handler: mockHandler,
101
+ })
102
+ })
103
+
104
+ afterEach(() => {
105
+ resetLogging()
106
+ })
107
+
108
+ it('should log instance creation at info level', () => {
109
+ createSanityInstance({projectId: 'test-proj', dataset: 'test-ds'})
110
+
111
+ expect(mockHandler.info).toHaveBeenCalledWith(
112
+ expect.stringContaining('[INFO] [sdk]'),
113
+ expect.objectContaining({
114
+ hasProjectId: true,
115
+ hasDataset: true,
116
+ }),
117
+ )
118
+ })
119
+
120
+ it('should log configuration details at debug level', () => {
121
+ createSanityInstance({projectId: 'test-proj', dataset: 'test-ds'})
122
+
123
+ expect(mockHandler.debug).toHaveBeenCalledWith(
124
+ expect.stringContaining('[DEBUG] [sdk]'),
125
+ expect.objectContaining({
126
+ projectId: 'test-proj',
127
+ dataset: 'test-ds',
128
+ }),
129
+ )
130
+ })
131
+
132
+ it('should log instance disposal', () => {
133
+ const instance = createSanityInstance({projectId: 'test-proj'})
134
+ vi.clearAllMocks() // Clear creation logs
135
+
136
+ instance.dispose()
137
+
138
+ expect(mockHandler.info).toHaveBeenCalledWith(
139
+ expect.stringContaining('Instance disposed'),
140
+ expect.anything(),
141
+ )
142
+ })
143
+
144
+ it('should log child instance creation at debug level', () => {
145
+ const parent = createSanityInstance({projectId: 'parent-proj'})
146
+ vi.clearAllMocks() // Clear parent creation logs
147
+
148
+ parent.createChild({dataset: 'child-ds'})
149
+
150
+ expect(mockHandler.debug).toHaveBeenCalledWith(
151
+ expect.stringContaining('Creating child instance'),
152
+ expect.objectContaining({
153
+ overridingDataset: true,
154
+ }),
155
+ )
156
+ })
157
+
158
+ it('should include instance context in logs', () => {
159
+ createSanityInstance({projectId: 'my-project', dataset: 'my-dataset'})
160
+
161
+ // Check that logs include the instance context (project and dataset)
162
+ expect(mockHandler.info).toHaveBeenCalledWith(
163
+ expect.stringMatching(/\[project:my-project\].*\[dataset:my-dataset\]/),
164
+ expect.anything(),
165
+ )
166
+ })
167
+ })
84
168
  })