@sanity/sdk 2.5.0 → 2.7.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 (46) hide show
  1. package/dist/index.d.ts +429 -27
  2. package/dist/index.js +657 -266
  3. package/dist/index.js.map +1 -1
  4. package/package.json +4 -3
  5. package/src/_exports/index.ts +18 -3
  6. package/src/auth/authMode.test.ts +56 -0
  7. package/src/auth/authMode.ts +71 -0
  8. package/src/auth/authStore.test.ts +85 -4
  9. package/src/auth/authStore.ts +63 -125
  10. package/src/auth/authStrategy.ts +39 -0
  11. package/src/auth/dashboardAuth.ts +132 -0
  12. package/src/auth/standaloneAuth.ts +109 -0
  13. package/src/auth/studioAuth.ts +217 -0
  14. package/src/auth/studioModeAuth.test.ts +43 -1
  15. package/src/auth/studioModeAuth.ts +10 -1
  16. package/src/auth/subscribeToStateAndFetchCurrentUser.ts +21 -6
  17. package/src/client/clientStore.test.ts +45 -43
  18. package/src/client/clientStore.ts +23 -9
  19. package/src/config/loggingConfig.ts +149 -0
  20. package/src/config/sanityConfig.ts +82 -22
  21. package/src/projection/getProjectionState.ts +6 -5
  22. package/src/projection/projectionQuery.test.ts +38 -55
  23. package/src/projection/projectionQuery.ts +27 -31
  24. package/src/projection/projectionStore.test.ts +4 -4
  25. package/src/projection/projectionStore.ts +3 -2
  26. package/src/projection/resolveProjection.ts +2 -2
  27. package/src/projection/statusQuery.test.ts +35 -0
  28. package/src/projection/statusQuery.ts +71 -0
  29. package/src/projection/subscribeToStateAndFetchBatches.test.ts +63 -50
  30. package/src/projection/subscribeToStateAndFetchBatches.ts +106 -27
  31. package/src/projection/types.ts +12 -0
  32. package/src/projection/util.ts +0 -1
  33. package/src/query/queryStore.test.ts +64 -0
  34. package/src/query/queryStore.ts +33 -11
  35. package/src/releases/getPerspectiveState.test.ts +17 -14
  36. package/src/releases/getPerspectiveState.ts +58 -38
  37. package/src/releases/releasesStore.test.ts +59 -61
  38. package/src/releases/releasesStore.ts +21 -35
  39. package/src/releases/utils/isReleasePerspective.ts +7 -0
  40. package/src/store/createActionBinder.test.ts +211 -1
  41. package/src/store/createActionBinder.ts +102 -13
  42. package/src/store/createSanityInstance.test.ts +85 -1
  43. package/src/store/createSanityInstance.ts +55 -4
  44. package/src/utils/logger-usage-example.md +141 -0
  45. package/src/utils/logger.test.ts +757 -0
  46. package/src/utils/logger.ts +537 -0
@@ -0,0 +1,757 @@
1
+ import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'
2
+
3
+ import {configureLogging as publicConfigureLogging} from '../config/loggingConfig'
4
+ import {
5
+ configureLogging,
6
+ createLogger,
7
+ createTimer,
8
+ getLoggingConfig,
9
+ type LogHandler,
10
+ parseDebugEnvVar,
11
+ resetLogging,
12
+ } from './logger'
13
+
14
+ describe('logger', () => {
15
+ const mockHandler: LogHandler = {
16
+ error: vi.fn(),
17
+ warn: vi.fn(),
18
+ info: vi.fn(),
19
+ debug: vi.fn(),
20
+ trace: vi.fn(),
21
+ }
22
+
23
+ beforeEach(() => {
24
+ resetLogging()
25
+ vi.clearAllMocks()
26
+ })
27
+
28
+ afterEach(() => {
29
+ resetLogging()
30
+ })
31
+
32
+ describe('configureLogging', () => {
33
+ it('should configure log level', () => {
34
+ configureLogging({level: 'debug', namespaces: ['*']})
35
+ const config = getLoggingConfig()
36
+ expect(config.level).toBe('debug')
37
+ })
38
+
39
+ it('should configure namespaces', () => {
40
+ configureLogging({namespaces: ['auth', 'document']})
41
+ const config = getLoggingConfig()
42
+ expect(config.namespaces).toEqual(['auth', 'document'])
43
+ })
44
+
45
+ it('should configure internal logging', () => {
46
+ configureLogging({internal: true})
47
+ const config = getLoggingConfig()
48
+ expect(config.internal).toBe(true)
49
+ })
50
+
51
+ it('should configure custom handler', () => {
52
+ configureLogging({handler: mockHandler})
53
+ const config = getLoggingConfig()
54
+ expect(config.handler).toBe(mockHandler)
55
+ })
56
+
57
+ it('should log when logging is configured', () => {
58
+ publicConfigureLogging({
59
+ level: 'info',
60
+ namespaces: ['sdk'],
61
+ handler: mockHandler,
62
+ })
63
+
64
+ expect(mockHandler.info).toHaveBeenCalledWith(
65
+ expect.stringContaining('[sdk] Logging configured'),
66
+ expect.objectContaining({
67
+ level: 'info',
68
+ namespaces: ['sdk'],
69
+ internal: false,
70
+ source: 'programmatic',
71
+ }),
72
+ )
73
+ })
74
+
75
+ it('should log to console.info when no custom handler is provided', () => {
76
+ const consoleInfoSpy = vi.spyOn(console, 'info').mockImplementation(() => {})
77
+
78
+ publicConfigureLogging({
79
+ level: 'info',
80
+ namespaces: ['sdk'],
81
+ })
82
+
83
+ expect(consoleInfoSpy).toHaveBeenCalledWith(
84
+ expect.stringContaining('[sdk] Logging configured'),
85
+ expect.objectContaining({
86
+ level: 'info',
87
+ namespaces: ['sdk'],
88
+ internal: false,
89
+ source: 'programmatic',
90
+ }),
91
+ )
92
+
93
+ consoleInfoSpy.mockRestore()
94
+ })
95
+ })
96
+
97
+ describe('createLogger', () => {
98
+ it('should create a logger for a namespace', () => {
99
+ const logger = createLogger('auth')
100
+ expect(logger.namespace).toBe('auth')
101
+ expect(typeof logger.error).toBe('function')
102
+ expect(typeof logger.warn).toBe('function')
103
+ expect(typeof logger.info).toBe('function')
104
+ expect(typeof logger.debug).toBe('function')
105
+ expect(typeof logger.trace).toBe('function')
106
+ })
107
+
108
+ it('should log at enabled levels', () => {
109
+ configureLogging({
110
+ level: 'info',
111
+ namespaces: ['auth'],
112
+ handler: mockHandler,
113
+ })
114
+
115
+ const logger = createLogger('auth')
116
+ logger.info('test message', {foo: 'bar'})
117
+
118
+ expect(mockHandler.info).toHaveBeenCalledWith(
119
+ expect.stringContaining('[INFO]'),
120
+ expect.objectContaining({foo: 'bar'}),
121
+ )
122
+ })
123
+
124
+ it('should not log at disabled levels', () => {
125
+ configureLogging({
126
+ level: 'warn',
127
+ namespaces: ['auth'],
128
+ handler: mockHandler,
129
+ })
130
+
131
+ const logger = createLogger('auth')
132
+ logger.info('test message')
133
+ logger.debug('test message')
134
+
135
+ expect(mockHandler.info).not.toHaveBeenCalled()
136
+ expect(mockHandler.debug).not.toHaveBeenCalled()
137
+ })
138
+
139
+ it('should not log for disabled namespaces', () => {
140
+ configureLogging({
141
+ level: 'info',
142
+ namespaces: ['auth'],
143
+ handler: mockHandler,
144
+ })
145
+
146
+ const logger = createLogger('document')
147
+ logger.info('test message')
148
+
149
+ expect(mockHandler.info).not.toHaveBeenCalled()
150
+ })
151
+
152
+ it('should log for wildcard namespace', () => {
153
+ configureLogging({
154
+ level: 'info',
155
+ namespaces: ['*'],
156
+ handler: mockHandler,
157
+ })
158
+
159
+ const authLogger = createLogger('auth')
160
+ const docLogger = createLogger('document')
161
+
162
+ authLogger.info('auth message')
163
+ docLogger.info('doc message')
164
+
165
+ expect(mockHandler.info).toHaveBeenCalledTimes(2)
166
+ })
167
+
168
+ it('should sanitize sensitive data', () => {
169
+ configureLogging({
170
+ level: 'info',
171
+ namespaces: ['auth'],
172
+ handler: mockHandler,
173
+ })
174
+
175
+ const logger = createLogger('auth')
176
+ logger.info('test', {token: 'secret123', password: 'pass123'})
177
+
178
+ expect(mockHandler.info).toHaveBeenCalledWith(
179
+ expect.any(String),
180
+ expect.objectContaining({
181
+ token: '[REDACTED]',
182
+ password: '[REDACTED]',
183
+ }),
184
+ )
185
+ })
186
+
187
+ it('should support child loggers with base context', () => {
188
+ configureLogging({
189
+ level: 'info',
190
+ namespaces: ['auth'],
191
+ handler: mockHandler,
192
+ })
193
+
194
+ const logger = createLogger('auth')
195
+ const childLogger = logger.child({userId: '123'})
196
+ childLogger.info('test message', {action: 'login'})
197
+
198
+ expect(mockHandler.info).toHaveBeenCalledWith(
199
+ expect.any(String),
200
+ expect.objectContaining({
201
+ userId: '123',
202
+ action: 'login',
203
+ }),
204
+ )
205
+ })
206
+
207
+ it('should check if level is enabled', () => {
208
+ configureLogging({
209
+ level: 'info',
210
+ namespaces: ['auth'],
211
+ })
212
+
213
+ const logger = createLogger('auth')
214
+ expect(logger.isLevelEnabled('info')).toBe(true)
215
+ expect(logger.isLevelEnabled('debug')).toBe(false)
216
+ })
217
+ })
218
+
219
+ describe('log levels', () => {
220
+ it('should respect level hierarchy', () => {
221
+ configureLogging({
222
+ level: 'warn',
223
+ namespaces: ['*'],
224
+ handler: mockHandler,
225
+ })
226
+
227
+ const logger = createLogger('core')
228
+
229
+ logger.error('error')
230
+ logger.warn('warn')
231
+ logger.info('info')
232
+ logger.debug('debug')
233
+ logger.trace('trace')
234
+
235
+ expect(mockHandler.error).toHaveBeenCalled()
236
+ expect(mockHandler.warn).toHaveBeenCalled()
237
+ expect(mockHandler.info).not.toHaveBeenCalled()
238
+ expect(mockHandler.debug).not.toHaveBeenCalled()
239
+ expect(mockHandler.trace).not.toHaveBeenCalled()
240
+ })
241
+ })
242
+
243
+ describe('internal logging', () => {
244
+ it('should not log internal messages by default', () => {
245
+ configureLogging({
246
+ level: 'trace',
247
+ namespaces: ['*'],
248
+ internal: false,
249
+ handler: mockHandler,
250
+ })
251
+
252
+ const logger = createLogger('store')
253
+ logger.trace('internal message')
254
+
255
+ expect(mockHandler.trace).not.toHaveBeenCalled()
256
+ })
257
+
258
+ it('should log internal messages when enabled', () => {
259
+ configureLogging({
260
+ level: 'trace',
261
+ namespaces: ['*'],
262
+ internal: true,
263
+ handler: mockHandler,
264
+ })
265
+
266
+ const logger = createLogger('store')
267
+ logger.trace('internal message')
268
+
269
+ expect(mockHandler.trace).toHaveBeenCalled()
270
+ })
271
+ })
272
+
273
+ describe('createTimer', () => {
274
+ it('should measure operation duration', async () => {
275
+ configureLogging({
276
+ level: 'debug',
277
+ namespaces: ['query'],
278
+ handler: mockHandler,
279
+ })
280
+
281
+ const timer = createTimer('query', 'fetchQuery')
282
+ await new Promise((resolve) => setTimeout(resolve, 10))
283
+ const duration = timer.end()
284
+
285
+ expect(duration).toBeGreaterThanOrEqual(8) // Allow for timing variance
286
+ expect(mockHandler.debug).toHaveBeenCalledWith(
287
+ expect.stringContaining('fetchQuery completed'),
288
+ expect.objectContaining({
289
+ operation: 'fetchQuery',
290
+ duration: expect.any(Number),
291
+ }),
292
+ )
293
+ })
294
+
295
+ it('should accept custom message and context', () => {
296
+ configureLogging({
297
+ level: 'debug',
298
+ namespaces: ['query'],
299
+ handler: mockHandler,
300
+ })
301
+
302
+ const timer = createTimer('query', 'fetchQuery')
303
+ timer.end('Query fetch succeeded', {queryId: '123'})
304
+
305
+ expect(mockHandler.debug).toHaveBeenCalledWith(
306
+ expect.stringContaining('Query fetch succeeded'),
307
+ expect.objectContaining({
308
+ operation: 'fetchQuery',
309
+ queryId: '123',
310
+ }),
311
+ )
312
+ })
313
+ })
314
+
315
+ describe('message formatting', () => {
316
+ it('should include timestamp by default', () => {
317
+ configureLogging({
318
+ level: 'info',
319
+ namespaces: ['auth'],
320
+ timestamps: true,
321
+ handler: mockHandler,
322
+ })
323
+
324
+ const logger = createLogger('auth')
325
+ logger.info('test')
326
+
327
+ expect(mockHandler.info).toHaveBeenCalledWith(
328
+ expect.stringMatching(/\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z\]/),
329
+ undefined,
330
+ )
331
+ })
332
+
333
+ it('should exclude timestamp when disabled', () => {
334
+ configureLogging({
335
+ level: 'info',
336
+ namespaces: ['auth'],
337
+ timestamps: false,
338
+ handler: mockHandler,
339
+ })
340
+
341
+ const logger = createLogger('auth')
342
+ logger.info('test')
343
+
344
+ expect(mockHandler.info).toHaveBeenCalledWith(
345
+ expect.not.stringMatching(/\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z\]/),
346
+ undefined,
347
+ )
348
+ })
349
+
350
+ it('should include log level and namespace', () => {
351
+ configureLogging({
352
+ level: 'info',
353
+ namespaces: ['auth'],
354
+ handler: mockHandler,
355
+ })
356
+
357
+ const logger = createLogger('auth')
358
+ logger.info('test message')
359
+
360
+ expect(mockHandler.info).toHaveBeenCalledWith(expect.stringContaining('[INFO]'), undefined)
361
+ expect(mockHandler.info).toHaveBeenCalledWith(expect.stringContaining('[auth]'), undefined)
362
+ })
363
+ })
364
+
365
+ describe('resetLogging', () => {
366
+ it('should reset to default configuration', () => {
367
+ configureLogging({
368
+ level: 'trace',
369
+ namespaces: ['*'],
370
+ internal: true,
371
+ })
372
+
373
+ resetLogging()
374
+
375
+ const config = getLoggingConfig()
376
+ expect(config.level).toBe('warn')
377
+ expect(config.namespaces).toEqual([])
378
+ expect(config.internal).toBe(false)
379
+ })
380
+ })
381
+
382
+ describe('production environment detection', () => {
383
+ const originalEnv = process.env['NODE_ENV']
384
+
385
+ afterEach(() => {
386
+ // Restore original NODE_ENV
387
+ process.env['NODE_ENV'] = originalEnv
388
+ resetLogging()
389
+ })
390
+
391
+ it('should disable logging in production by default', () => {
392
+ process.env['NODE_ENV'] = 'production'
393
+
394
+ configureLogging({
395
+ level: 'info',
396
+ namespaces: ['*'],
397
+ handler: mockHandler,
398
+ })
399
+
400
+ const logger = createLogger('auth')
401
+ logger.info('test message')
402
+
403
+ // Should not log in production by default
404
+ expect(mockHandler.info).not.toHaveBeenCalled()
405
+ })
406
+
407
+ it('should enable logging in production when enableInProduction is true', () => {
408
+ process.env['NODE_ENV'] = 'production'
409
+
410
+ configureLogging({
411
+ level: 'info',
412
+ namespaces: ['*'],
413
+ enableInProduction: true,
414
+ handler: mockHandler,
415
+ })
416
+
417
+ const logger = createLogger('auth')
418
+ logger.info('test message')
419
+
420
+ // Should log when explicitly enabled
421
+ expect(mockHandler.info).toHaveBeenCalledWith(
422
+ expect.stringContaining('[INFO] [auth] test message'),
423
+ undefined,
424
+ )
425
+ })
426
+
427
+ it('should enable logging in non-production environments', () => {
428
+ process.env['NODE_ENV'] = 'development'
429
+
430
+ configureLogging({
431
+ level: 'info',
432
+ namespaces: ['*'],
433
+ handler: mockHandler,
434
+ })
435
+
436
+ const logger = createLogger('auth')
437
+ logger.info('test message')
438
+
439
+ // Should log in development
440
+ expect(mockHandler.info).toHaveBeenCalledWith(
441
+ expect.stringContaining('[INFO] [auth] test message'),
442
+ undefined,
443
+ )
444
+ })
445
+
446
+ it('should enable logging when NODE_ENV is not set', () => {
447
+ delete process.env['NODE_ENV']
448
+
449
+ configureLogging({
450
+ level: 'info',
451
+ namespaces: ['*'],
452
+ handler: mockHandler,
453
+ })
454
+
455
+ const logger = createLogger('auth')
456
+ logger.info('test message')
457
+
458
+ // Should log when NODE_ENV is undefined
459
+ expect(mockHandler.info).toHaveBeenCalledWith(
460
+ expect.stringContaining('[INFO] [auth] test message'),
461
+ undefined,
462
+ )
463
+ })
464
+
465
+ it('should respect enableInProduction: false in production', () => {
466
+ process.env['NODE_ENV'] = 'production'
467
+
468
+ configureLogging({
469
+ level: 'info',
470
+ namespaces: ['*'],
471
+ enableInProduction: false,
472
+ handler: mockHandler,
473
+ })
474
+
475
+ const logger = createLogger('auth')
476
+ logger.info('test message')
477
+
478
+ // Should not log when explicitly disabled
479
+ expect(mockHandler.info).not.toHaveBeenCalled()
480
+ })
481
+ })
482
+
483
+ describe('parseDebugEnvVar', () => {
484
+ const originalDebug = process.env['DEBUG']
485
+
486
+ afterEach(() => {
487
+ // Restore original DEBUG
488
+ if (originalDebug === undefined) {
489
+ delete process.env['DEBUG']
490
+ } else {
491
+ process.env['DEBUG'] = originalDebug
492
+ }
493
+ })
494
+
495
+ it('should return null when DEBUG is not set', () => {
496
+ delete process.env['DEBUG']
497
+ expect(parseDebugEnvVar()).toBeNull()
498
+ })
499
+
500
+ it('should return null when DEBUG does not include "sanity"', () => {
501
+ process.env['DEBUG'] = 'express:*'
502
+ expect(parseDebugEnvVar()).toBeNull()
503
+ })
504
+
505
+ it('should parse DEBUG=sanity:* to enable all namespaces at debug level', () => {
506
+ process.env['DEBUG'] = 'sanity:*'
507
+ const config = parseDebugEnvVar()
508
+
509
+ expect(config).toEqual({
510
+ level: 'debug',
511
+ namespaces: ['*'],
512
+ })
513
+ })
514
+
515
+ it('should parse DEBUG=sanity to enable all namespaces', () => {
516
+ process.env['DEBUG'] = 'sanity'
517
+ const config = parseDebugEnvVar()
518
+
519
+ expect(config).toEqual({
520
+ level: 'debug',
521
+ namespaces: ['*'],
522
+ })
523
+ })
524
+
525
+ it('should parse DEBUG=sanity:auth,sanity:document for specific namespaces', () => {
526
+ process.env['DEBUG'] = 'sanity:auth,sanity:document'
527
+ const config = parseDebugEnvVar()
528
+
529
+ expect(config).toEqual({
530
+ level: 'debug',
531
+ namespaces: ['auth', 'document'],
532
+ })
533
+ })
534
+
535
+ it('should parse DEBUG=sanity:trace:* to enable trace level', () => {
536
+ process.env['DEBUG'] = 'sanity:trace:*'
537
+ const config = parseDebugEnvVar()
538
+
539
+ expect(config).toEqual({
540
+ level: 'trace',
541
+ namespaces: ['*'],
542
+ })
543
+ })
544
+
545
+ it('should parse DEBUG=sanity:info:auth for custom level with namespace', () => {
546
+ process.env['DEBUG'] = 'sanity:info:auth'
547
+ const config = parseDebugEnvVar()
548
+
549
+ expect(config?.level).toBe('info')
550
+ })
551
+
552
+ it('should parse DEBUG=sanity:*:internal to enable internal logs', () => {
553
+ process.env['DEBUG'] = 'sanity:*:internal'
554
+ const config = parseDebugEnvVar()
555
+
556
+ expect(config).toEqual({
557
+ level: 'debug',
558
+ namespaces: ['*'],
559
+ internal: true,
560
+ })
561
+ })
562
+
563
+ it('should handle mixed DEBUG patterns with non-sanity values', () => {
564
+ process.env['DEBUG'] = 'express:*,sanity:auth,other:*'
565
+ const config = parseDebugEnvVar()
566
+
567
+ expect(config).toEqual({
568
+ level: 'debug',
569
+ namespaces: ['auth'],
570
+ })
571
+ })
572
+
573
+ it('should filter out wildcards from specific namespace list', () => {
574
+ process.env['DEBUG'] = 'sanity:auth,sanity:*'
575
+ const config = parseDebugEnvVar()
576
+
577
+ // Should prefer wildcard mode
578
+ expect(config?.namespaces).toContain('*')
579
+ })
580
+ })
581
+
582
+ describe('DEBUG environment variable integration', () => {
583
+ const originalDebug = process.env['DEBUG']
584
+
585
+ afterEach(() => {
586
+ // Restore original DEBUG
587
+ if (originalDebug === undefined) {
588
+ delete process.env['DEBUG']
589
+ } else {
590
+ process.env['DEBUG'] = originalDebug
591
+ }
592
+ resetLogging()
593
+ })
594
+
595
+ it('should apply DEBUG env var configuration to logging', () => {
596
+ process.env['DEBUG'] = 'sanity:*'
597
+
598
+ // Simulate what happens on module init
599
+ const envConfig = parseDebugEnvVar()
600
+ if (envConfig) {
601
+ configureLogging({
602
+ ...envConfig,
603
+ handler: mockHandler,
604
+ })
605
+ }
606
+
607
+ const logger = createLogger('auth')
608
+ logger.debug('test message')
609
+
610
+ expect(mockHandler.debug).toHaveBeenCalled()
611
+ })
612
+
613
+ it('should allow programmatic config to override env var', () => {
614
+ process.env['DEBUG'] = 'sanity:*'
615
+
616
+ // First apply env var config
617
+ const envConfig = parseDebugEnvVar()
618
+ if (envConfig) {
619
+ configureLogging(envConfig)
620
+ }
621
+
622
+ // Then override with programmatic config
623
+ configureLogging({
624
+ level: 'error',
625
+ namespaces: ['auth'],
626
+ handler: mockHandler,
627
+ })
628
+
629
+ const config = getLoggingConfig()
630
+ expect(config.level).toBe('error')
631
+ expect(config.namespaces).toEqual(['auth'])
632
+ })
633
+ })
634
+
635
+ describe('instance context', () => {
636
+ it('should include instance context in log output', () => {
637
+ configureLogging({
638
+ level: 'info',
639
+ namespaces: ['document'],
640
+ handler: mockHandler,
641
+ })
642
+
643
+ const logger = createLogger('document', {
644
+ instanceContext: {
645
+ instanceId: 'abc123def456',
646
+ projectId: 'my-project',
647
+ dataset: 'production',
648
+ },
649
+ })
650
+
651
+ logger.info('Document updated')
652
+
653
+ expect(mockHandler.info).toHaveBeenCalledWith(
654
+ expect.stringContaining('[project:my-project]'),
655
+ expect.any(Object),
656
+ )
657
+ expect(mockHandler.info).toHaveBeenCalledWith(
658
+ expect.stringContaining('[dataset:production]'),
659
+ expect.any(Object),
660
+ )
661
+ expect(mockHandler.info).toHaveBeenCalledWith(
662
+ expect.stringContaining('[instance:abc123de]'), // Truncated to 8 chars
663
+ expect.any(Object),
664
+ )
665
+ })
666
+
667
+ it('should work without instance context', () => {
668
+ configureLogging({
669
+ level: 'info',
670
+ namespaces: ['auth'],
671
+ handler: mockHandler,
672
+ })
673
+
674
+ const logger = createLogger('auth')
675
+ logger.info('Static operation')
676
+
677
+ expect(mockHandler.info).toHaveBeenCalledWith(
678
+ expect.stringContaining('[INFO] [auth] Static operation'),
679
+ undefined,
680
+ )
681
+ expect(mockHandler.info).not.toHaveBeenCalledWith(
682
+ expect.stringContaining('[project:'),
683
+ expect.any(Object),
684
+ )
685
+ })
686
+
687
+ it('should include partial instance context', () => {
688
+ configureLogging({
689
+ level: 'info',
690
+ namespaces: ['query'],
691
+ handler: mockHandler,
692
+ })
693
+
694
+ const logger = createLogger('query', {
695
+ instanceContext: {
696
+ projectId: 'my-project',
697
+ // No dataset or instanceId
698
+ },
699
+ })
700
+
701
+ logger.info('Query executed')
702
+
703
+ expect(mockHandler.info).toHaveBeenCalledWith(
704
+ expect.stringContaining('[project:my-project]'),
705
+ expect.any(Object),
706
+ )
707
+ expect(mockHandler.info).not.toHaveBeenCalledWith(
708
+ expect.stringContaining('[dataset:'),
709
+ expect.any(Object),
710
+ )
711
+ })
712
+
713
+ it('should return instance context', () => {
714
+ const instanceContext = {
715
+ instanceId: 'test-instance',
716
+ projectId: 'test-project',
717
+ dataset: 'test-dataset',
718
+ }
719
+
720
+ const logger = createLogger('auth', {instanceContext})
721
+
722
+ expect(logger.getInstanceContext()).toEqual(instanceContext)
723
+ })
724
+
725
+ it('should return undefined when no instance context', () => {
726
+ const logger = createLogger('auth')
727
+
728
+ expect(logger.getInstanceContext()).toBeUndefined()
729
+ })
730
+
731
+ it('should merge instance context with other context', () => {
732
+ configureLogging({
733
+ level: 'info',
734
+ namespaces: ['document'],
735
+ handler: mockHandler,
736
+ })
737
+
738
+ const logger = createLogger('document', {
739
+ instanceContext: {
740
+ projectId: 'my-project',
741
+ dataset: 'production',
742
+ },
743
+ })
744
+
745
+ logger.info('Document synced', {documentId: 'doc-123', syncTime: 150})
746
+
747
+ expect(mockHandler.info).toHaveBeenCalledWith(
748
+ expect.stringContaining('[project:my-project]'),
749
+ expect.objectContaining({
750
+ documentId: 'doc-123',
751
+ syncTime: 150,
752
+ instanceContext: expect.any(Object),
753
+ }),
754
+ )
755
+ })
756
+ })
757
+ })