@optimizely-opal/opal-tool-ocp-sdk 1.0.0-OCP-1449.1 → 1.0.0-beta.10

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 (47) hide show
  1. package/README.md +13 -3
  2. package/dist/function/GlobalToolFunction.d.ts +3 -2
  3. package/dist/function/GlobalToolFunction.d.ts.map +1 -1
  4. package/dist/function/GlobalToolFunction.js +7 -4
  5. package/dist/function/GlobalToolFunction.js.map +1 -1
  6. package/dist/function/GlobalToolFunction.test.js +16 -4
  7. package/dist/function/GlobalToolFunction.test.js.map +1 -1
  8. package/dist/function/ToolFunction.d.ts +3 -2
  9. package/dist/function/ToolFunction.d.ts.map +1 -1
  10. package/dist/function/ToolFunction.js +7 -4
  11. package/dist/function/ToolFunction.js.map +1 -1
  12. package/dist/function/ToolFunction.test.js +15 -3
  13. package/dist/function/ToolFunction.test.js.map +1 -1
  14. package/dist/index.d.ts +1 -1
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +2 -1
  17. package/dist/index.js.map +1 -1
  18. package/dist/logging/ToolLogger.d.ts +11 -3
  19. package/dist/logging/ToolLogger.d.ts.map +1 -1
  20. package/dist/logging/ToolLogger.js +114 -13
  21. package/dist/logging/ToolLogger.js.map +1 -1
  22. package/dist/logging/ToolLogger.test.js +177 -71
  23. package/dist/logging/ToolLogger.test.js.map +1 -1
  24. package/dist/types/Models.d.ts +7 -1
  25. package/dist/types/Models.d.ts.map +1 -1
  26. package/dist/types/Models.js +5 -1
  27. package/dist/types/Models.js.map +1 -1
  28. package/dist/types/ToolError.d.ts +13 -0
  29. package/dist/types/ToolError.d.ts.map +1 -1
  30. package/dist/types/ToolError.js +30 -2
  31. package/dist/types/ToolError.js.map +1 -1
  32. package/dist/types/ToolError.test.js +27 -3
  33. package/dist/types/ToolError.test.js.map +1 -1
  34. package/dist/validation/ParameterValidator.test.js +2 -1
  35. package/dist/validation/ParameterValidator.test.js.map +1 -1
  36. package/package.json +3 -3
  37. package/src/function/GlobalToolFunction.test.ts +21 -6
  38. package/src/function/GlobalToolFunction.ts +9 -5
  39. package/src/function/ToolFunction.test.ts +21 -5
  40. package/src/function/ToolFunction.ts +9 -5
  41. package/src/index.ts +1 -1
  42. package/src/logging/ToolLogger.test.ts +225 -74
  43. package/src/logging/ToolLogger.ts +129 -15
  44. package/src/types/Models.ts +8 -1
  45. package/src/types/ToolError.test.ts +33 -3
  46. package/src/types/ToolError.ts +32 -2
  47. package/src/validation/ParameterValidator.test.ts +4 -1
@@ -53,16 +53,16 @@ jest.mock('@zaiusinc/app-sdk', () => ({
53
53
 
54
54
  // Create a concrete implementation for testing
55
55
  class TestToolFunction extends ToolFunction {
56
- private mockReady: jest.MockedFunction<() => Promise<boolean>>;
56
+ private mockReady: jest.MockedFunction<() => Promise<{ ready: boolean; reason?: string }>>;
57
57
 
58
58
  public constructor(request?: any) {
59
59
  super(request || {});
60
60
  (this as any).request = request;
61
- this.mockReady = jest.fn().mockResolvedValue(true);
61
+ this.mockReady = jest.fn().mockResolvedValue({ ready: true });
62
62
  }
63
63
 
64
64
  // Override the ready method with mock implementation for testing
65
- protected ready(): Promise<boolean> {
65
+ protected ready(): Promise<{ ready: boolean; reason?: string }> {
66
66
  return this.mockReady();
67
67
  }
68
68
 
@@ -174,7 +174,7 @@ describe('ToolFunction', () => {
174
174
  const readyRequest = createReadyRequestWithAuth();
175
175
 
176
176
  toolFunction = new TestToolFunction(readyRequest);
177
- toolFunction.getMockReady().mockResolvedValue(true);
177
+ toolFunction.getMockReady().mockResolvedValue({ ready: true });
178
178
 
179
179
  // Act
180
180
  const result = await toolFunction.perform();
@@ -190,7 +190,7 @@ describe('ToolFunction', () => {
190
190
  const readyRequest = createReadyRequestWithAuth();
191
191
 
192
192
  toolFunction = new TestToolFunction(readyRequest);
193
- toolFunction.getMockReady().mockResolvedValue(false);
193
+ toolFunction.getMockReady().mockResolvedValue({ ready: false });
194
194
 
195
195
  // Act
196
196
  const result = await toolFunction.perform();
@@ -201,6 +201,22 @@ describe('ToolFunction', () => {
201
201
  expect(mockProcessRequest).not.toHaveBeenCalled(); // Should not call service
202
202
  });
203
203
 
204
+ it('should return ready: false with reason when ready method returns false with reason', async () => {
205
+ // Arrange
206
+ const readyRequest = createReadyRequestWithAuth();
207
+
208
+ toolFunction = new TestToolFunction(readyRequest);
209
+ toolFunction.getMockReady().mockResolvedValue({ ready: false, reason: 'Database connection failed' });
210
+
211
+ // Act
212
+ const result = await toolFunction.perform();
213
+
214
+ // Assert
215
+ expect(toolFunction.getMockReady()).toHaveBeenCalledTimes(1);
216
+ expect(result).toEqual(new Response(200, { ready: false, reason: 'Database connection failed' }));
217
+ expect(mockProcessRequest).not.toHaveBeenCalled(); // Should not call service
218
+ });
219
+
204
220
  it('should handle ready method throwing an error', async () => {
205
221
  // Arrange
206
222
  const readyRequest = createReadyRequestWithAuth();
@@ -3,6 +3,7 @@ import { authenticateRegularRequest, authenticateInternalRequest } from '../auth
3
3
  import { toolsService } from '../service/Service';
4
4
  import { ToolLogger } from '../logging/ToolLogger';
5
5
  import { ToolError } from '../types/ToolError';
6
+ import { ReadyResponse } from '../types/Models';
6
7
 
7
8
  /**
8
9
  * Abstract base class for tool-based function execution
@@ -14,10 +15,10 @@ export abstract class ToolFunction extends Function {
14
15
  * Override this method to implement any required credentials and/or other configuration
15
16
  * exist and are valid. Reasonable caching should be utilized to prevent excessive requests to external resources.
16
17
  * @async
17
- * @returns true if the opal function is ready to use
18
+ * @returns ReadyResponse containing ready status and optional reason when not ready
18
19
  */
19
- protected ready(): Promise<boolean> {
20
- return Promise.resolve(true);
20
+ protected ready(): Promise<ReadyResponse | boolean> {
21
+ return Promise.resolve({ ready: true });
21
22
  }
22
23
 
23
24
  /**
@@ -67,8 +68,11 @@ export abstract class ToolFunction extends Function {
67
68
  }
68
69
 
69
70
  if (this.request.path === '/ready') {
70
- const isReady = await this.ready();
71
- return new Response(200, { ready: isReady });
71
+ const readyResult = await this.ready();
72
+ const readyResponse = typeof readyResult === 'boolean'
73
+ ? { ready: readyResult }
74
+ : readyResult;
75
+ return new Response(200, readyResponse);
72
76
  }
73
77
 
74
78
  // Pass 'this' as context so decorated methods can use the existing instance
package/src/index.ts CHANGED
@@ -4,4 +4,4 @@ export * from './types/Models';
4
4
  export * from './types/ToolError';
5
5
  export * from './decorator/Decorator';
6
6
  export * from './auth/TokenVerifier';
7
- export { Tool, Interaction, InteractionResult } from './service/Service';
7
+ export { Tool, Interaction, InteractionResult, NestedInteractions } from './service/Service';
@@ -88,6 +88,30 @@ describe('ToolLogger', () => {
88
88
  return response;
89
89
  };
90
90
 
91
+ const createMockResponseWithBody = (
92
+ status: number,
93
+ bodyData: Uint8Array | undefined,
94
+ contentType: string
95
+ ): App.Response => {
96
+ const mockHeaders = {
97
+ get: jest.fn().mockReturnValue(contentType)
98
+ };
99
+
100
+ const response = {
101
+ status,
102
+ headers: mockHeaders
103
+ } as any;
104
+
105
+ Object.defineProperty(response, 'bodyAsU8Array', {
106
+ get() {
107
+ return bodyData;
108
+ },
109
+ enumerable: true
110
+ });
111
+
112
+ return response;
113
+ };
114
+
91
115
  describe('logRequest', () => {
92
116
  it('should log request with parameters', () => {
93
117
  const req = createMockRequest();
@@ -240,7 +264,7 @@ describe('ToolLogger', () => {
240
264
  path: '/test-tool',
241
265
  method: 'POST',
242
266
  parameters: {
243
- description: `${'a'.repeat(100)}... (truncated, 150 chars total)`,
267
+ description: `${'a'.repeat(118)}...[22 truncated]...${'a'.repeat(10)}`,
244
268
  short_field: 'normal'
245
269
  }
246
270
  });
@@ -265,8 +289,8 @@ describe('ToolLogger', () => {
265
289
  method: 'POST',
266
290
  parameters: {
267
291
  items: [
268
- ...largeArray.slice(0, 10),
269
- '... (5 more items truncated)'
292
+ ...largeArray.slice(0, 2),
293
+ '... (13 more items truncated)'
270
294
  ],
271
295
  small_array: ['a', 'b']
272
296
  }
@@ -420,6 +444,21 @@ describe('ToolLogger', () => {
420
444
  // Should not throw error or cause infinite recursion
421
445
  expect(() => ToolLogger.logRequest(req)).not.toThrow();
422
446
  expect(mockLogger.info).toHaveBeenCalled();
447
+
448
+ // Verify that deeply nested parts are replaced with placeholder
449
+ const logCall = mockLogger.info.mock.calls[0];
450
+ const loggedData = JSON.parse(logCall[1]);
451
+
452
+ // Navigate to a deeply nested level that should be truncated
453
+ // At maxDepth=5, objects beyond depth 5 should be replaced
454
+ const nested1 = loggedData.parameters.deep.nested;
455
+ expect(nested1).toBeDefined(); // depth 2
456
+ const nested2 = nested1.nested;
457
+ expect(nested2).toBeDefined(); // depth 3
458
+ const nested3 = nested2.nested;
459
+ expect(nested3).toBeDefined(); // depth 4
460
+ const nested4 = nested3.nested;
461
+ expect(nested4).toBe('[MAX_DEPTH_EXCEEDED]'); // depth 5, should be truncated
423
462
  });
424
463
 
425
464
  it('should replace deeply nested objects with MAX_DEPTH_EXCEEDED placeholder', () => {
@@ -465,6 +504,7 @@ describe('ToolLogger', () => {
465
504
  level1: {
466
505
  items: [
467
506
  {
507
+ shallow: 'data',
468
508
  level2: {
469
509
  level3: {
470
510
  level4: {
@@ -504,9 +544,12 @@ describe('ToolLogger', () => {
504
544
  const loggedData = JSON.parse(logCall[1]);
505
545
 
506
546
  const items = loggedData.parameters.level1.items;
547
+ expect(items.length).toBe(3);
507
548
 
508
549
  // First item: deeply nested object with inner parts replaced by placeholder
550
+ // shallow object should be processed normally
509
551
  expect(items[0]).toEqual({
552
+ shallow: 'data',
510
553
  level2: {
511
554
  level3: {
512
555
  level4: '[MAX_DEPTH_EXCEEDED]'
@@ -516,18 +559,6 @@ describe('ToolLogger', () => {
516
559
 
517
560
  // Second item: simple string should remain unchanged
518
561
  expect(items[1]).toBe('simple-item');
519
-
520
- // Third item: shallow object should be processed normally
521
- expect(items[2]).toEqual({ shallow: 'data' });
522
-
523
- // Fourth item: another deeply nested object with inner parts replaced by placeholder
524
- expect(items[3]).toEqual({
525
- level2: {
526
- level3: {
527
- level4: '[MAX_DEPTH_EXCEEDED]'
528
- }
529
- }
530
- });
531
562
  });
532
563
  });
533
564
 
@@ -545,7 +576,8 @@ describe('ToolLogger', () => {
545
576
  status: 200,
546
577
  contentType: 'application/json',
547
578
  contentLength: 34, // JSON.stringify({ result: 'success', data: 'test' }).length
548
- success: true
579
+ success: true,
580
+ responseBody: { result: 'success', data: 'test' }
549
581
  };
550
582
 
551
583
  expect(mockLogger.info).toHaveBeenCalledWith(
@@ -567,14 +599,14 @@ describe('ToolLogger', () => {
567
599
  status: 400,
568
600
  contentType: 'application/json',
569
601
  contentLength: 23,
570
- success: false
602
+ success: false,
603
+ responseBody: { error: 'Bad request' }
571
604
  });
572
605
  });
573
606
 
574
- it('should handle response without bodyJSON', () => {
607
+ it('should handle response without body data', () => {
575
608
  const req = createMockRequest();
576
- const response = createMockResponse(204);
577
- response.bodyJSON = undefined;
609
+ const response = createMockResponseWithBody(204, undefined, 'application/json');
578
610
 
579
611
  ToolLogger.logResponse(req, response);
580
612
 
@@ -600,11 +632,12 @@ describe('ToolLogger', () => {
600
632
  status: 200,
601
633
  contentType: 'application/json',
602
634
  contentLength: 15,
603
- success: true
635
+ success: true,
636
+ responseBody: { data: 'test' }
604
637
  });
605
638
  });
606
639
 
607
- it('should handle unknown content type', () => {
640
+ it('should handle unknown content type - response body not logged', () => {
608
641
  const req = createMockRequest();
609
642
  const response = createMockResponse(200, { data: 'test' });
610
643
  response.headers.get = jest.fn().mockReturnValue(null);
@@ -623,21 +656,35 @@ describe('ToolLogger', () => {
623
656
 
624
657
  it('should handle content length calculation error', () => {
625
658
  const req = createMockRequest();
626
- const circularObj: any = { name: 'test' };
627
- circularObj.self = circularObj; // Create circular reference
628
659
 
629
- const response = createMockResponse(200, circularObj);
630
-
631
- ToolLogger.logResponse(req, response);
660
+ // Simulate a response that will cause errors when trying to calculate content length
661
+ // by providing a Uint8Array but the underlying data causes issues
662
+ const mockHeaders = {
663
+ get: jest.fn().mockReturnValue('application/json')
664
+ };
632
665
 
633
- expectJsonLog({
634
- event: 'opal_tool_response',
635
- path: '/test-tool',
666
+ const response = {
636
667
  status: 200,
637
- contentType: 'application/json',
638
- contentLength: 'unknown',
639
- success: true
668
+ headers: mockHeaders
669
+ } as any;
670
+
671
+ // Create a getter that throws when accessed (simulating serialization error)
672
+ Object.defineProperty(response, 'bodyAsU8Array', {
673
+ get() {
674
+ throw new Error('Circular structure');
675
+ },
676
+ enumerable: true
640
677
  });
678
+
679
+ ToolLogger.logResponse(req, response);
680
+
681
+ // The error causes both contentLength and responseBody to fail gracefully
682
+ const logCall = mockLogger.info.mock.calls[0];
683
+ const loggedData = JSON.parse(logCall[1]);
684
+
685
+ expect(loggedData.event).toBe('opal_tool_response');
686
+ expect(loggedData.contentLength).toBe('unknown');
687
+ expect(loggedData.responseBody).toBeUndefined();
641
688
  });
642
689
 
643
690
  it('should correctly identify success status codes', () => {
@@ -659,14 +706,16 @@ describe('ToolLogger', () => {
659
706
  const response = createMockResponse(status);
660
707
  ToolLogger.logResponse(req, response);
661
708
 
662
- expectJsonLog({
663
- event: 'opal_tool_response',
664
- path: '/test-tool',
665
- status,
666
- contentType: 'application/json',
667
- contentLength: 2,
668
- success: expected
669
- });
709
+ const logCall = mockLogger.info.mock.calls[0];
710
+ const loggedData = JSON.parse(logCall[1]);
711
+
712
+ expect(loggedData.event).toBe('opal_tool_response');
713
+ expect(loggedData.path).toBe('/test-tool');
714
+ expect(loggedData.status).toBe(status);
715
+ expect(loggedData.contentType).toBe('application/json');
716
+ expect(loggedData.contentLength).toBe(2);
717
+ expect(loggedData.success).toBe(expected);
718
+ expect(loggedData.responseBody).toEqual({});
670
719
  });
671
720
  });
672
721
 
@@ -674,29 +723,148 @@ describe('ToolLogger', () => {
674
723
  const req = createMockRequest();
675
724
 
676
725
  const testCases = [
677
- 'application/json',
678
- 'text/plain',
679
- 'application/xml',
680
- 'text/html'
726
+ { contentType: 'application/json', expectedBody: { data: 'test' } },
727
+ { contentType: 'text/plain', expectedBody: '{"data":"test"}' },
728
+ { contentType: 'text/html', expectedBody: '{"data":"test"}' }
681
729
  ];
682
730
 
683
- testCases.forEach((contentType) => {
731
+ testCases.forEach(({ contentType, expectedBody }) => {
684
732
  mockLogger.info.mockClear();
685
733
  const response = createMockResponse(200, { data: 'test' });
686
734
  response.headers.get = jest.fn().mockReturnValue(contentType);
687
735
 
688
736
  ToolLogger.logResponse(req, response);
689
737
 
690
- expectJsonLog({
691
- event: 'opal_tool_response',
692
- path: '/test-tool',
693
- status: 200,
694
- contentType,
695
- contentLength: 15,
696
- success: true
697
- });
738
+ const logCall = mockLogger.info.mock.calls[0];
739
+ const loggedData = JSON.parse(logCall[1]);
740
+
741
+ expect(loggedData.event).toBe('opal_tool_response');
742
+ expect(loggedData.path).toBe('/test-tool');
743
+ expect(loggedData.status).toBe(200);
744
+ expect(loggedData.contentType).toBe(contentType);
745
+ expect(loggedData.contentLength).toBe(15);
746
+ expect(loggedData.success).toBe(true);
747
+ expect(loggedData.responseBody).toEqual(expectedBody);
748
+ });
749
+ });
750
+
751
+ it('should log short successful response body', () => {
752
+ const req = createMockRequest();
753
+ const response = createMockResponse(200, { result: 'success', data: 'test' });
754
+
755
+ ToolLogger.logResponse(req, response);
756
+
757
+ const logCall = mockLogger.info.mock.calls[0];
758
+ const loggedData = JSON.parse(logCall[1]);
759
+
760
+ expect(loggedData.responseBody).toEqual({ result: 'success', data: 'test' });
761
+ expect(loggedData.success).toBe(true);
762
+ });
763
+
764
+ it('should truncate long successful response body to 256 chars', () => {
765
+ const req = createMockRequest();
766
+ const longData = 'a'.repeat(300);
767
+ const response = createMockResponse(200, { message: longData });
768
+
769
+ ToolLogger.logResponse(req, response);
770
+
771
+ const logCall = mockLogger.info.mock.calls[0];
772
+ const loggedData = JSON.parse(logCall[1]);
773
+
774
+ // The response body should be truncated when stringified
775
+ expect(loggedData.responseBody.message).toContain(' truncated]...');
776
+ });
777
+
778
+ it('truncates long properties of failed responses', () => {
779
+ const req = createMockRequest();
780
+ const longData = 'a'.repeat(150);
781
+ const response = createMockResponse(400, { error: 'Bad request', details: longData });
782
+
783
+ ToolLogger.logResponse(req, response);
784
+
785
+ const logCall = mockLogger.info.mock.calls[0];
786
+ const loggedData = JSON.parse(logCall[1]);
787
+
788
+ // Failed responses should include full body, not truncated
789
+ expect(loggedData.responseBody.error).toEqual('Bad request');
790
+ expect(loggedData.responseBody.details).toContain(' truncated]...');
791
+ expect(loggedData.success).toBe(false);
792
+ });
793
+
794
+ it('should redact sensitive data in response body', () => {
795
+ const req = createMockRequest();
796
+ const response = createMockResponse(200, {
797
+ user: 'john',
798
+ password: 'secret123',
799
+ api_key: 'key456',
800
+ data: 'public'
801
+ });
802
+
803
+ ToolLogger.logResponse(req, response);
804
+
805
+ const logCall = mockLogger.info.mock.calls[0];
806
+ const loggedData = JSON.parse(logCall[1]);
807
+
808
+ expect(loggedData.responseBody).toEqual({
809
+ user: 'john',
810
+ password: '[REDACTED]',
811
+ api_key: '[REDACTED]',
812
+ data: 'public'
698
813
  });
699
814
  });
815
+
816
+ it('should handle response with no body', () => {
817
+ const req = createMockRequest();
818
+ const response = createMockResponseWithBody(204, undefined, 'application/json');
819
+
820
+ ToolLogger.logResponse(req, response);
821
+
822
+ const logCall = mockLogger.info.mock.calls[0];
823
+ const loggedData = JSON.parse(logCall[1]);
824
+
825
+ expect(loggedData.responseBody).toBeUndefined();
826
+ expect(loggedData.success).toBe(true);
827
+ });
828
+
829
+ it('should handle plain text response body', () => {
830
+ const req = createMockRequest();
831
+ const plainText = 'This is a plain text response';
832
+ const response = createMockResponseWithBody(200, new Uint8Array(Buffer.from(plainText)), 'text/plain');
833
+
834
+ ToolLogger.logResponse(req, response);
835
+
836
+ const logCall = mockLogger.info.mock.calls[0];
837
+ const loggedData = JSON.parse(logCall[1]);
838
+
839
+ expect(loggedData.responseBody).toBe(plainText);
840
+ });
841
+
842
+ it('should truncate long plain text successful responses', () => {
843
+ const req = createMockRequest();
844
+ const longText = 'a'.repeat(300);
845
+ const response = createMockResponseWithBody(200, new Uint8Array(Buffer.from(longText)), 'text/plain');
846
+
847
+ ToolLogger.logResponse(req, response);
848
+
849
+ const logCall = mockLogger.info.mock.calls[0];
850
+ const loggedData = JSON.parse(logCall[1]);
851
+
852
+ expect(loggedData.responseBody).toBe(`${'a'.repeat(256)}... (truncated)`);
853
+ });
854
+
855
+ it('should not truncate long plain text failed responses', () => {
856
+ const req = createMockRequest();
857
+ const longText = 'a'.repeat(150);
858
+ const response = createMockResponseWithBody(500, new Uint8Array(Buffer.from(longText)), 'text/plain');
859
+
860
+ ToolLogger.logResponse(req, response);
861
+
862
+ const logCall = mockLogger.info.mock.calls[0];
863
+ const loggedData = JSON.parse(logCall[1]);
864
+
865
+ expect(loggedData.responseBody).toBe(longText);
866
+ expect(loggedData.success).toBe(false);
867
+ });
700
868
  });
701
869
 
702
870
  describe('edge cases', () => {
@@ -743,7 +911,7 @@ describe('ToolLogger', () => {
743
911
  string: 'text',
744
912
  number: 42,
745
913
  boolean: true,
746
- array: [1, 2, 3],
914
+ array: [1, 2],
747
915
  object: { nested: 'value' },
748
916
  nullValue: null,
749
917
  password: 'secret'
@@ -761,7 +929,7 @@ describe('ToolLogger', () => {
761
929
  string: 'text',
762
930
  number: 42,
763
931
  boolean: true,
764
- array: [1, 2, 3],
932
+ array: [1, 2],
765
933
  object: { nested: 'value' },
766
934
  nullValue: null,
767
935
  password: '[REDACTED]'
@@ -841,24 +1009,7 @@ describe('ToolLogger', () => {
841
1009
  description: 'OVERRIDDEN: Enhanced minimum detectable effect calculation',
842
1010
  required: true
843
1011
  },
844
- {
845
- name: 'sigLevel',
846
- type: 'number',
847
- description: 'OVERRIDDEN: Enhanced statistical significance level',
848
- required: true
849
- },
850
- {
851
- name: 'numVariations',
852
- type: 'number',
853
- description: 'OVERRIDDEN: Enhanced number of variations handling',
854
- required: true
855
- },
856
- {
857
- name: 'dailyVisitors',
858
- type: 'number',
859
- description: 'OVERRIDDEN: Enhanced daily visitor count with forecasting',
860
- required: true
861
- }
1012
+ '... (3 more items truncated)'
862
1013
  ]
863
1014
  }
864
1015
  ]