@forge/storage 1.5.15 → 1.6.0-next.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/README.md CHANGED
@@ -7,6 +7,7 @@ import fetch, { RequestInit } from 'node-fetch';
7
7
 
8
8
  import { GlobalStorage } from './global-storage';
9
9
  import { APIResponse, getStorageInstanceWithQuery } from './index';
10
+ import { getMetrics } from './runtime/fetch-and-storage';
10
11
 
11
12
  const API_BASE = 'https://api.atlassian.com';
12
13
 
@@ -37,7 +38,7 @@ async function apiClient(path: string, init: RequestInit): Promise<APIResponse>
37
38
  return fetch(url, init);
38
39
  }
39
40
 
40
- const adapter = new GlobalStorage(() => appContextAri, apiClient);
41
+ const adapter = new GlobalStorage(() => appContextAri, apiClient, getMetrics);
41
42
  const storage = getStorageInstanceWithQuery(adapter);
42
43
 
43
44
  async function demo() {
@@ -3,8 +3,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const errors_1 = require("../errors");
4
4
  const global_storage_1 = require("../global-storage");
5
5
  const gql_queries_1 = require("../gql-queries");
6
+ const mocks_1 = require("@atlassian/metrics-interface/dist/mocks");
6
7
  const contextAri = 'app-ari';
7
- const getStorage = (apiClientMock) => new global_storage_1.GlobalStorage(() => contextAri, apiClientMock);
8
+ const getStorage = (apiClientMock, metrics) => new global_storage_1.GlobalStorage(() => contextAri, apiClientMock, () => metrics);
8
9
  const getApiClientMock = (response, statusCode = 200) => {
9
10
  return jest.fn().mockReturnValue({
10
11
  ok: statusCode === 200,
@@ -12,6 +13,36 @@ const getApiClientMock = (response, statusCode = 200) => {
12
13
  text: jest.fn().mockResolvedValue(JSON.stringify(response))
13
14
  });
14
15
  };
16
+ const getMetricMock = () => {
17
+ const mockMetrics = new mocks_1.MockMetrics();
18
+ const mockCounter = new mocks_1.MockCounter('');
19
+ const mockTiming = new mocks_1.MockTiming('');
20
+ const mockStopTiming = jest.fn();
21
+ const mockMeasure = {
22
+ stop: mockStopTiming
23
+ };
24
+ mockMetrics.counter.mockReturnValue(mockCounter);
25
+ mockMetrics.timing.mockReturnValue(mockTiming);
26
+ mockTiming.measure.mockReturnValue(mockMeasure);
27
+ return {
28
+ mockMetrics,
29
+ mockCounter,
30
+ mockStopTiming
31
+ };
32
+ };
33
+ const checkMetricsFired = ({ mockMetrics, mockCounter, mockStopTiming }, { encrypted, ...restTags }, success) => {
34
+ expect(mockMetrics.counter).toHaveBeenCalledWith('forge.runtime.storage.operation', {
35
+ ...restTags,
36
+ encrypted: String(encrypted),
37
+ success: String(success)
38
+ });
39
+ expect(mockCounter.incr).toHaveBeenCalled();
40
+ expect(mockMetrics.timing).toHaveBeenCalledWith('forge.runtime.storage.operation.latency', {
41
+ ...restTags,
42
+ encrypted: String(encrypted)
43
+ });
44
+ expect(mockStopTiming).toHaveBeenCalledWith({ success: String(success) });
45
+ };
15
46
  const getApiClientMockInvalidJson = (response, statusCode = 200) => {
16
47
  return jest.fn().mockReturnValue({
17
48
  ok: statusCode === 200,
@@ -52,7 +83,8 @@ describe('GlobalStorage', () => {
52
83
  }
53
84
  }
54
85
  });
55
- const globalStorage = getStorage(apiClientMock);
86
+ const metricMocks = getMetricMock();
87
+ const globalStorage = getStorage(apiClientMock, metricMocks.mockMetrics);
56
88
  const returnedValue = await globalStorage.get('testKey');
57
89
  verifyApiClientCalledWith(apiClientMock, {
58
90
  contextAri,
@@ -60,6 +92,7 @@ describe('GlobalStorage', () => {
60
92
  encrypted: false
61
93
  });
62
94
  expect(returnedValue).toEqual('testValue');
95
+ checkMetricsFired(metricMocks, { store: 'untyped', operation: 'get', encrypted: false }, true);
63
96
  });
64
97
  it('should call the storage API, passing the provided key and returning undefined if the key doesnt exist', async () => {
65
98
  const apiClientMock = getApiClientMock({
@@ -69,7 +102,8 @@ describe('GlobalStorage', () => {
69
102
  }
70
103
  }
71
104
  });
72
- const globalStorage = getStorage(apiClientMock);
105
+ const metricMocks = getMetricMock();
106
+ const globalStorage = getStorage(apiClientMock, metricMocks.mockMetrics);
73
107
  const returnedValue = await globalStorage.get('testKey');
74
108
  verifyApiClientCalledWith(apiClientMock, {
75
109
  contextAri,
@@ -77,6 +111,7 @@ describe('GlobalStorage', () => {
77
111
  encrypted: false
78
112
  });
79
113
  expect(returnedValue).toEqual(undefined);
114
+ checkMetricsFired(metricMocks, { store: 'untyped', operation: 'get', encrypted: false }, true);
80
115
  });
81
116
  it('should call the storage API, passing the provided key and returning the stored falsey value 0', async () => {
82
117
  const apiClientMock = getApiClientMock({
@@ -86,7 +121,8 @@ describe('GlobalStorage', () => {
86
121
  }
87
122
  }
88
123
  });
89
- const globalStorage = getStorage(apiClientMock);
124
+ const metricMocks = getMetricMock();
125
+ const globalStorage = getStorage(apiClientMock, metricMocks.mockMetrics);
90
126
  const returnedValue = await globalStorage.get('testKey');
91
127
  verifyApiClientCalledWith(apiClientMock, {
92
128
  contextAri,
@@ -94,6 +130,7 @@ describe('GlobalStorage', () => {
94
130
  encrypted: false
95
131
  });
96
132
  expect(returnedValue).toEqual(0);
133
+ checkMetricsFired(metricMocks, { store: 'untyped', operation: 'get', encrypted: false }, true);
97
134
  });
98
135
  it('should call the storage API, passing the provided key and returning the stored empty string', async () => {
99
136
  const apiClientMock = getApiClientMock({
@@ -103,7 +140,8 @@ describe('GlobalStorage', () => {
103
140
  }
104
141
  }
105
142
  });
106
- const globalStorage = getStorage(apiClientMock);
143
+ const metricMocks = getMetricMock();
144
+ const globalStorage = getStorage(apiClientMock, metricMocks.mockMetrics);
107
145
  const returnedValue = await globalStorage.get('testKey');
108
146
  verifyApiClientCalledWith(apiClientMock, {
109
147
  contextAri,
@@ -111,29 +149,36 @@ describe('GlobalStorage', () => {
111
149
  encrypted: false
112
150
  });
113
151
  expect(returnedValue).toEqual('');
152
+ checkMetricsFired(metricMocks, { store: 'untyped', operation: 'get', encrypted: false }, true);
114
153
  });
115
154
  it('should throw an error with the returned status for non-200 status codes', async () => {
116
155
  const apiClientMock = getApiClientMock(undefined, 400);
117
- const globalStorage = getStorage(apiClientMock);
156
+ const metricMocks = getMetricMock();
157
+ const globalStorage = getStorage(apiClientMock, metricMocks.mockMetrics);
118
158
  const response = globalStorage.get('testKey');
119
159
  expect(apiClientMock).toHaveBeenCalled();
120
160
  await expect(response).rejects.toThrow(errors_1.APIError.forStatus(400));
161
+ checkMetricsFired(metricMocks, { store: 'untyped', operation: 'get', encrypted: false }, false);
121
162
  });
122
163
  it('should throw an error with the returned error message for failed responses', async () => {
123
164
  const apiClientMock = getApiClientMock({
124
165
  errors: [INVALID_CURSOR_ERROR]
125
166
  }, 200);
126
- const globalStorage = getStorage(apiClientMock);
167
+ const metricMocks = getMetricMock();
168
+ const globalStorage = getStorage(apiClientMock, metricMocks.mockMetrics);
127
169
  const response = globalStorage.get('testKey');
128
170
  expect(apiClientMock).toHaveBeenCalled();
129
171
  await expect(response).rejects.toThrow(errors_1.APIError.forErrorCode('CURSOR_INVALID', 'error message'));
172
+ checkMetricsFired(metricMocks, { store: 'untyped', operation: 'get', encrypted: false }, false);
130
173
  });
131
174
  it('should throw an error if the response is not a valid JSON', async () => {
132
175
  const apiClientMock = getApiClientMockInvalidJson('test', 200);
133
- const globalStorage = getStorage(apiClientMock);
176
+ const metricMocks = getMetricMock();
177
+ const globalStorage = getStorage(apiClientMock, metricMocks.mockMetrics);
134
178
  const response = globalStorage.get('testKey');
135
179
  expect(apiClientMock).toHaveBeenCalled();
136
180
  await expect(response).rejects.toThrow(errors_1.APIError.forUnexpected('Response text was not a valid JSON: test'));
181
+ checkMetricsFired(metricMocks, { store: 'untyped', operation: 'get', encrypted: false }, false);
137
182
  });
138
183
  });
139
184
  describe('get secret', () => {
@@ -145,7 +190,8 @@ describe('GlobalStorage', () => {
145
190
  }
146
191
  }
147
192
  });
148
- const globalStorage = getStorage(apiClientMock);
193
+ const metricMocks = getMetricMock();
194
+ const globalStorage = getStorage(apiClientMock, metricMocks.mockMetrics);
149
195
  const returnedValue = await globalStorage.getSecret('testKey');
150
196
  verifyApiClientCalledWith(apiClientMock, {
151
197
  contextAri,
@@ -153,6 +199,7 @@ describe('GlobalStorage', () => {
153
199
  encrypted: true
154
200
  });
155
201
  expect(returnedValue).toEqual('testValue');
202
+ checkMetricsFired(metricMocks, { store: 'untyped', operation: 'get', encrypted: true }, true);
156
203
  });
157
204
  });
158
205
  describe('set', () => {
@@ -166,7 +213,8 @@ describe('GlobalStorage', () => {
166
213
  }
167
214
  }
168
215
  });
169
- const globalStorage = getStorage(apiClientMock);
216
+ const metricMocks = getMetricMock();
217
+ const globalStorage = getStorage(apiClientMock, metricMocks.mockMetrics);
170
218
  await globalStorage.set('testKey', 'testValue');
171
219
  verifyApiClientCalledWith(apiClientMock, {
172
220
  input: {
@@ -176,6 +224,7 @@ describe('GlobalStorage', () => {
176
224
  encrypted: false
177
225
  }
178
226
  });
227
+ checkMetricsFired(metricMocks, { store: 'untyped', operation: 'set', encrypted: false }, true);
179
228
  });
180
229
  it('should throw an error if the storage API returns successful = false', async () => {
181
230
  const apiClientMock = getApiClientMock({
@@ -188,17 +237,21 @@ describe('GlobalStorage', () => {
188
237
  }
189
238
  }
190
239
  });
191
- const globalStorage = getStorage(apiClientMock);
240
+ const metricMocks = getMetricMock();
241
+ const globalStorage = getStorage(apiClientMock, metricMocks.mockMetrics);
192
242
  const response = globalStorage.set('testKey', 'testValue');
193
243
  expect(apiClientMock).toHaveBeenCalled();
194
244
  await expect(response).rejects.toThrow(errors_1.APIError.forErrorCode('INVALID_CURSOR', 'error message'));
245
+ checkMetricsFired(metricMocks, { store: 'untyped', operation: 'set', encrypted: false }, false);
195
246
  });
196
247
  it('should throw an error if the storage API returns a non 200 status code', async () => {
197
248
  const apiClientMock = getApiClientMockInvalidJson('', 400);
198
- const globalStorage = getStorage(apiClientMock);
249
+ const metricMocks = getMetricMock();
250
+ const globalStorage = getStorage(apiClientMock, metricMocks.mockMetrics);
199
251
  const response = globalStorage.set('testKey', 'testValue');
200
252
  expect(apiClientMock).toHaveBeenCalled();
201
253
  await expect(response).rejects.toThrow(errors_1.APIError.forStatus(400));
254
+ checkMetricsFired(metricMocks, { store: 'untyped', operation: 'set', encrypted: false }, false);
202
255
  });
203
256
  it('should throw a 500 error if success=false but no errors were returned', async () => {
204
257
  const apiClientMock = getApiClientMock({
@@ -210,7 +263,8 @@ describe('GlobalStorage', () => {
210
263
  }
211
264
  }
212
265
  });
213
- const globalStorage = getStorage(apiClientMock);
266
+ const metricMocks = getMetricMock();
267
+ const globalStorage = getStorage(apiClientMock, metricMocks.mockMetrics);
214
268
  await expect(globalStorage.set('testKey', 'testValue')).rejects.toThrow(errors_1.APIError.forStatus(500));
215
269
  verifyApiClientCalledWith(apiClientMock, {
216
270
  input: {
@@ -220,6 +274,7 @@ describe('GlobalStorage', () => {
220
274
  encrypted: false
221
275
  }
222
276
  });
277
+ checkMetricsFired(metricMocks, { store: 'untyped', operation: 'set', encrypted: false }, false);
223
278
  });
224
279
  });
225
280
  describe('set secret', () => {
@@ -233,7 +288,8 @@ describe('GlobalStorage', () => {
233
288
  }
234
289
  }
235
290
  });
236
- const globalStorage = getStorage(apiClientMock);
291
+ const metricMocks = getMetricMock();
292
+ const globalStorage = getStorage(apiClientMock, metricMocks.mockMetrics);
237
293
  await globalStorage.setSecret('testKey', 'testValue');
238
294
  verifyApiClientCalledWith(apiClientMock, {
239
295
  input: {
@@ -243,6 +299,7 @@ describe('GlobalStorage', () => {
243
299
  encrypted: true
244
300
  }
245
301
  });
302
+ checkMetricsFired(metricMocks, { store: 'untyped', operation: 'set', encrypted: true }, true);
246
303
  });
247
304
  });
248
305
  describe('delete', () => {
@@ -256,7 +313,8 @@ describe('GlobalStorage', () => {
256
313
  }
257
314
  }
258
315
  });
259
- const globalStorage = getStorage(apiClientMock);
316
+ const metricMocks = getMetricMock();
317
+ const globalStorage = getStorage(apiClientMock, metricMocks.mockMetrics);
260
318
  await globalStorage.delete('testKey');
261
319
  verifyApiClientCalledWith(apiClientMock, {
262
320
  input: {
@@ -265,6 +323,7 @@ describe('GlobalStorage', () => {
265
323
  encrypted: false
266
324
  }
267
325
  });
326
+ checkMetricsFired(metricMocks, { store: 'untyped', operation: 'delete', encrypted: false }, true);
268
327
  });
269
328
  it('should throw an error if the storage API returns successful = false', async () => {
270
329
  const apiClientMock = getApiClientMock({
@@ -277,17 +336,21 @@ describe('GlobalStorage', () => {
277
336
  }
278
337
  }
279
338
  });
280
- const globalStorage = getStorage(apiClientMock);
339
+ const metricMocks = getMetricMock();
340
+ const globalStorage = getStorage(apiClientMock, metricMocks.mockMetrics);
281
341
  const response = globalStorage.delete('testKey');
282
342
  expect(apiClientMock).toHaveBeenCalled();
283
343
  await expect(response).rejects.toThrow(errors_1.APIError.forErrorCode('CURSOR_INVALID', 'error message'));
344
+ checkMetricsFired(metricMocks, { store: 'untyped', operation: 'delete', encrypted: false }, false);
284
345
  });
285
346
  it('should throw an error if the storage API returns a non 200 status code and has no body', async () => {
286
347
  const apiClientMock = getApiClientMockInvalidJson('', 400);
287
- const globalStorage = getStorage(apiClientMock);
348
+ const metricMocks = getMetricMock();
349
+ const globalStorage = getStorage(apiClientMock, metricMocks.mockMetrics);
288
350
  const response = globalStorage.delete('testKey');
289
351
  expect(apiClientMock).toHaveBeenCalled();
290
352
  await expect(response).rejects.toThrow(errors_1.APIError.forStatus(400));
353
+ checkMetricsFired(metricMocks, { store: 'untyped', operation: 'delete', encrypted: false }, false);
291
354
  });
292
355
  });
293
356
  describe('delete secret', () => {
@@ -301,7 +364,8 @@ describe('GlobalStorage', () => {
301
364
  }
302
365
  }
303
366
  });
304
- const globalStorage = getStorage(apiClientMock);
367
+ const metricMocks = getMetricMock();
368
+ const globalStorage = getStorage(apiClientMock, metricMocks.mockMetrics);
305
369
  await globalStorage.deleteSecret('testKey');
306
370
  verifyApiClientCalledWith(apiClientMock, {
307
371
  input: {
@@ -310,6 +374,7 @@ describe('GlobalStorage', () => {
310
374
  encrypted: true
311
375
  }
312
376
  });
377
+ checkMetricsFired(metricMocks, { store: 'untyped', operation: 'delete', encrypted: true }, true);
313
378
  });
314
379
  });
315
380
  describe('getEntity', () => {
@@ -321,7 +386,8 @@ describe('GlobalStorage', () => {
321
386
  }
322
387
  }
323
388
  });
324
- const globalStorage = getStorage(apiClientMock);
389
+ const metricMocks = getMetricMock();
390
+ const globalStorage = getStorage(apiClientMock, metricMocks.mockMetrics);
325
391
  const returnedValue = await globalStorage.getEntity('testEntityName', 'testEntityKey');
326
392
  verifyApiClientCalledWith(apiClientMock, {
327
393
  contextAri,
@@ -329,6 +395,7 @@ describe('GlobalStorage', () => {
329
395
  key: 'testEntityKey'
330
396
  });
331
397
  expect(returnedValue).toEqual('testValue');
398
+ checkMetricsFired(metricMocks, { store: 'typed', operation: 'get', encrypted: false }, true);
332
399
  });
333
400
  it('should call the storage API, passing the provided entity key and returning undefined if the key doesnt exist', async () => {
334
401
  const apiClientMock = getApiClientMock({
@@ -338,7 +405,8 @@ describe('GlobalStorage', () => {
338
405
  }
339
406
  }
340
407
  });
341
- const globalStorage = getStorage(apiClientMock);
408
+ const metricMocks = getMetricMock();
409
+ const globalStorage = getStorage(apiClientMock, metricMocks.mockMetrics);
342
410
  const returnedValue = await globalStorage.getEntity('testEntityName', 'testEntityKey');
343
411
  verifyApiClientCalledWith(apiClientMock, {
344
412
  contextAri,
@@ -346,6 +414,7 @@ describe('GlobalStorage', () => {
346
414
  key: 'testEntityKey'
347
415
  });
348
416
  expect(returnedValue).toEqual(undefined);
417
+ checkMetricsFired(metricMocks, { store: 'typed', operation: 'get', encrypted: false }, true);
349
418
  });
350
419
  it('should call the storage API, passing the provided key and returning the stored falsey value 0', async () => {
351
420
  const apiClientMock = getApiClientMock({
@@ -355,7 +424,8 @@ describe('GlobalStorage', () => {
355
424
  }
356
425
  }
357
426
  });
358
- const globalStorage = getStorage(apiClientMock);
427
+ const metricMocks = getMetricMock();
428
+ const globalStorage = getStorage(apiClientMock, metricMocks.mockMetrics);
359
429
  const returnedValue = await globalStorage.getEntity('testEntityName', 'testEntityKey');
360
430
  verifyApiClientCalledWith(apiClientMock, {
361
431
  contextAri,
@@ -363,6 +433,7 @@ describe('GlobalStorage', () => {
363
433
  key: 'testEntityKey'
364
434
  });
365
435
  expect(returnedValue).toEqual(0);
436
+ checkMetricsFired(metricMocks, { store: 'typed', operation: 'get', encrypted: false }, true);
366
437
  });
367
438
  it('should call the storage API, passing the provided key and returning the stored empty string', async () => {
368
439
  const apiClientMock = getApiClientMock({
@@ -372,7 +443,8 @@ describe('GlobalStorage', () => {
372
443
  }
373
444
  }
374
445
  });
375
- const globalStorage = getStorage(apiClientMock);
446
+ const metricMocks = getMetricMock();
447
+ const globalStorage = getStorage(apiClientMock, metricMocks.mockMetrics);
376
448
  const returnedValue = await globalStorage.getEntity('testEntityName', 'testEntityKey');
377
449
  verifyApiClientCalledWith(apiClientMock, {
378
450
  contextAri,
@@ -380,29 +452,36 @@ describe('GlobalStorage', () => {
380
452
  key: 'testEntityKey'
381
453
  });
382
454
  expect(returnedValue).toEqual('');
455
+ checkMetricsFired(metricMocks, { store: 'typed', operation: 'get', encrypted: false }, true);
383
456
  });
384
457
  it('should throw an error with the returned status for non-200 status codes', async () => {
385
458
  const apiClientMock = getApiClientMock(undefined, 400);
386
- const globalStorage = getStorage(apiClientMock);
459
+ const metricMocks = getMetricMock();
460
+ const globalStorage = getStorage(apiClientMock, metricMocks.mockMetrics);
387
461
  const response = globalStorage.getEntity('testEntityName', 'testEntityKey');
388
462
  expect(apiClientMock).toHaveBeenCalled();
389
463
  await expect(response).rejects.toThrow(errors_1.APIError.forStatus(400));
464
+ checkMetricsFired(metricMocks, { store: 'typed', operation: 'get', encrypted: false }, false);
390
465
  });
391
466
  it('should throw an error with the returned error message for failed responses', async () => {
392
467
  const apiClientMock = getApiClientMock({
393
468
  errors: [INVALID_CURSOR_ERROR]
394
469
  }, 200);
395
- const globalStorage = getStorage(apiClientMock);
470
+ const metricMocks = getMetricMock();
471
+ const globalStorage = getStorage(apiClientMock, metricMocks.mockMetrics);
396
472
  const response = globalStorage.getEntity('testEntityName', 'testEntityKey');
397
473
  expect(apiClientMock).toHaveBeenCalled();
398
474
  await expect(response).rejects.toThrow(errors_1.APIError.forErrorCode('CURSOR_INVALID', 'error message'));
475
+ checkMetricsFired(metricMocks, { store: 'typed', operation: 'get', encrypted: false }, false);
399
476
  });
400
477
  it('should throw an error if the response is not a valid JSON', async () => {
401
478
  const apiClientMock = getApiClientMockInvalidJson('test', 200);
402
- const globalStorage = getStorage(apiClientMock);
479
+ const metricMocks = getMetricMock();
480
+ const globalStorage = getStorage(apiClientMock, metricMocks.mockMetrics);
403
481
  const response = globalStorage.getEntity('testEntityName', 'testEntityKey');
404
482
  expect(apiClientMock).toHaveBeenCalled();
405
483
  await expect(response).rejects.toThrow(errors_1.APIError.forUnexpected('Response text was not a valid JSON: test'));
484
+ checkMetricsFired(metricMocks, { store: 'typed', operation: 'get', encrypted: false }, false);
406
485
  });
407
486
  });
408
487
  describe('setEntity', () => {
@@ -416,7 +495,8 @@ describe('GlobalStorage', () => {
416
495
  }
417
496
  }
418
497
  });
419
- const globalStorage = getStorage(apiClientMock);
498
+ const metricMocks = getMetricMock();
499
+ const globalStorage = getStorage(apiClientMock, metricMocks.mockMetrics);
420
500
  await globalStorage.setEntity('testEntityName', 'testEntityKey', 'testValue');
421
501
  verifyApiClientCalledWith(apiClientMock, {
422
502
  input: {
@@ -426,6 +506,7 @@ describe('GlobalStorage', () => {
426
506
  value: 'testValue'
427
507
  }
428
508
  });
509
+ checkMetricsFired(metricMocks, { store: 'typed', operation: 'set', encrypted: false }, true);
429
510
  });
430
511
  it('should throw an error if the storage API returns successful = false', async () => {
431
512
  const apiClientMock = getApiClientMock({
@@ -438,17 +519,21 @@ describe('GlobalStorage', () => {
438
519
  }
439
520
  }
440
521
  });
441
- const globalStorage = getStorage(apiClientMock);
522
+ const metricMocks = getMetricMock();
523
+ const globalStorage = getStorage(apiClientMock, metricMocks.mockMetrics);
442
524
  const response = globalStorage.setEntity('testEntityName', 'testEntityKey', 'testValue');
443
525
  expect(apiClientMock).toHaveBeenCalled();
444
526
  await expect(response).rejects.toThrow(errors_1.APIError.forErrorCode('INVALID_CURSOR', 'error message'));
527
+ checkMetricsFired(metricMocks, { store: 'typed', operation: 'set', encrypted: false }, false);
445
528
  });
446
529
  it('should throw an error if the storage API returns a non 200 status code', async () => {
447
530
  const apiClientMock = getApiClientMockInvalidJson('', 400);
448
- const globalStorage = getStorage(apiClientMock);
531
+ const metricMocks = getMetricMock();
532
+ const globalStorage = getStorage(apiClientMock, metricMocks.mockMetrics);
449
533
  const response = globalStorage.setEntity('testEntityName', 'testEntityKey', 'testValue');
450
534
  expect(apiClientMock).toHaveBeenCalled();
451
535
  await expect(response).rejects.toThrow(errors_1.APIError.forStatus(400));
536
+ checkMetricsFired(metricMocks, { store: 'typed', operation: 'set', encrypted: false }, false);
452
537
  });
453
538
  it('should throw a 500 error if success=false but no errors were returned', async () => {
454
539
  const apiClientMock = getApiClientMock({
@@ -460,7 +545,8 @@ describe('GlobalStorage', () => {
460
545
  }
461
546
  }
462
547
  });
463
- const globalStorage = getStorage(apiClientMock);
548
+ const metricMocks = getMetricMock();
549
+ const globalStorage = getStorage(apiClientMock, metricMocks.mockMetrics);
464
550
  await expect(globalStorage.setEntity('testEntityName', 'testEntityKey', 'testValue')).rejects.toThrow(errors_1.APIError.forStatus(500));
465
551
  verifyApiClientCalledWith(apiClientMock, {
466
552
  input: {
@@ -470,6 +556,7 @@ describe('GlobalStorage', () => {
470
556
  value: 'testValue'
471
557
  }
472
558
  });
559
+ checkMetricsFired(metricMocks, { store: 'typed', operation: 'set', encrypted: false }, false);
473
560
  });
474
561
  });
475
562
  describe('deleteEntity', () => {
@@ -483,7 +570,8 @@ describe('GlobalStorage', () => {
483
570
  }
484
571
  }
485
572
  });
486
- const globalStorage = getStorage(apiClientMock);
573
+ const metricMocks = getMetricMock();
574
+ const globalStorage = getStorage(apiClientMock, metricMocks.mockMetrics);
487
575
  await globalStorage.deleteEntity('testEntityName', 'testEntityKey');
488
576
  verifyApiClientCalledWith(apiClientMock, {
489
577
  input: {
@@ -492,6 +580,7 @@ describe('GlobalStorage', () => {
492
580
  key: 'testEntityKey'
493
581
  }
494
582
  });
583
+ checkMetricsFired(metricMocks, { store: 'typed', operation: 'delete', encrypted: false }, true);
495
584
  });
496
585
  it('should throw an error if the storage API returns successful = false', async () => {
497
586
  const apiClientMock = getApiClientMock({
@@ -504,17 +593,21 @@ describe('GlobalStorage', () => {
504
593
  }
505
594
  }
506
595
  });
507
- const globalStorage = getStorage(apiClientMock);
596
+ const metricMocks = getMetricMock();
597
+ const globalStorage = getStorage(apiClientMock, metricMocks.mockMetrics);
508
598
  const response = globalStorage.deleteEntity('testEntityKey', 'testEntityKey');
509
599
  expect(apiClientMock).toHaveBeenCalled();
510
600
  await expect(response).rejects.toThrow(errors_1.APIError.forErrorCode('CURSOR_INVALID', 'error message'));
601
+ checkMetricsFired(metricMocks, { store: 'typed', operation: 'delete', encrypted: false }, false);
511
602
  });
512
603
  it('should throw an error if the storage API returns a non 200 status code and has no body', async () => {
513
604
  const apiClientMock = getApiClientMockInvalidJson('', 400);
514
- const globalStorage = getStorage(apiClientMock);
605
+ const metricMocks = getMetricMock();
606
+ const globalStorage = getStorage(apiClientMock, metricMocks.mockMetrics);
515
607
  const response = globalStorage.deleteEntity('testEntityKey', 'testEntityKey');
516
608
  expect(apiClientMock).toHaveBeenCalled();
517
609
  await expect(response).rejects.toThrow(errors_1.APIError.forStatus(400));
610
+ checkMetricsFired(metricMocks, { store: 'typed', operation: 'delete', encrypted: false }, false);
518
611
  });
519
612
  });
520
613
  describe('list', () => {
@@ -529,7 +622,8 @@ describe('GlobalStorage', () => {
529
622
  }
530
623
  }
531
624
  });
532
- const globalStorage = getStorage(apiClientMock);
625
+ const metricMocks = getMetricMock();
626
+ const globalStorage = getStorage(apiClientMock, metricMocks.mockMetrics);
533
627
  const where = [
534
628
  {
535
629
  field: 'key',
@@ -553,6 +647,7 @@ describe('GlobalStorage', () => {
553
647
  ],
554
648
  nextCursor: 'cursor2'
555
649
  }));
650
+ checkMetricsFired(metricMocks, { store: 'untyped', operation: 'query', encrypted: false }, true);
556
651
  });
557
652
  it('should query the appStoredEntitiesForCleanup endpoint given process.env.IS_CLEANUP_FUNCTION is set to true', async () => {
558
653
  process.env.IS_CLEANUP_FUNCTION = 'true';
@@ -566,7 +661,8 @@ describe('GlobalStorage', () => {
566
661
  }
567
662
  }
568
663
  });
569
- const globalStorage = getStorage(apiClientMock);
664
+ const metricMocks = getMetricMock();
665
+ const globalStorage = getStorage(apiClientMock, metricMocks.mockMetrics);
570
666
  const where = [
571
667
  {
572
668
  field: 'key',
@@ -590,6 +686,7 @@ describe('GlobalStorage', () => {
590
686
  ],
591
687
  nextCursor: 'cursor2'
592
688
  }));
689
+ checkMetricsFired(metricMocks, { store: 'untyped', operation: 'query', encrypted: false }, true);
593
690
  process.env.IS_CLEANUP_FUNCTION = '';
594
691
  });
595
692
  it('should use default values', async () => {
@@ -600,7 +697,8 @@ describe('GlobalStorage', () => {
600
697
  }
601
698
  }
602
699
  });
603
- const globalStorage = getStorage(apiClientMock);
700
+ const metricMocks = getMetricMock();
701
+ const globalStorage = getStorage(apiClientMock, metricMocks.mockMetrics);
604
702
  await globalStorage.list({});
605
703
  verifyApiClientCalledWith(apiClientMock, {
606
704
  contextAri,
@@ -608,6 +706,7 @@ describe('GlobalStorage', () => {
608
706
  cursor: null,
609
707
  limit: null
610
708
  }, gql_queries_1.UntypedQueries.listQuery(contextAri, {}).query);
709
+ checkMetricsFired(metricMocks, { store: 'untyped', operation: 'query', encrypted: false }, true);
611
710
  });
612
711
  it('should handle an empty result set', async () => {
613
712
  const apiClientMock = getApiClientMock({
@@ -617,7 +716,8 @@ describe('GlobalStorage', () => {
617
716
  }
618
717
  }
619
718
  });
620
- const globalStorage = getStorage(apiClientMock);
719
+ const metricMocks = getMetricMock();
720
+ const globalStorage = getStorage(apiClientMock, metricMocks.mockMetrics);
621
721
  const where = [
622
722
  {
623
723
  field: 'key',
@@ -630,22 +730,27 @@ describe('GlobalStorage', () => {
630
730
  results: [],
631
731
  nextCursor: undefined
632
732
  }));
733
+ checkMetricsFired(metricMocks, { store: 'untyped', operation: 'query', encrypted: false }, true);
633
734
  });
634
735
  it('should throw an error if the storage API returns an error', async () => {
635
736
  const apiClientMock = getApiClientMock({
636
737
  errors: [INVALID_CURSOR_ERROR]
637
738
  });
638
- const globalStorage = getStorage(apiClientMock);
739
+ const metricMocks = getMetricMock();
740
+ const globalStorage = getStorage(apiClientMock, metricMocks.mockMetrics);
639
741
  const response = globalStorage.list({});
640
742
  expect(apiClientMock).toHaveBeenCalled();
641
743
  await expect(response).rejects.toThrow(errors_1.APIError.forErrorCode('CURSOR_INVALID', 'error message'));
744
+ checkMetricsFired(metricMocks, { store: 'untyped', operation: 'query', encrypted: false }, false);
642
745
  });
643
746
  it('should throw an error if the storage API returns a non 200 status code and has no body', async () => {
644
747
  const apiClientMock = getApiClientMockInvalidJson('', 400);
645
- const globalStorage = getStorage(apiClientMock);
748
+ const metricMocks = getMetricMock();
749
+ const globalStorage = getStorage(apiClientMock, metricMocks.mockMetrics);
646
750
  const response = globalStorage.list({});
647
751
  expect(apiClientMock).toHaveBeenCalled();
648
752
  await expect(response).rejects.toThrow(errors_1.APIError.forStatus(400));
753
+ checkMetricsFired(metricMocks, { store: 'untyped', operation: 'query', encrypted: false }, false);
649
754
  });
650
755
  });
651
756
  describe('listCustomEntities', () => {
@@ -657,7 +762,8 @@ describe('GlobalStorage', () => {
657
762
  }
658
763
  }
659
764
  });
660
- const globalStorage = getStorage(apiClientMock);
765
+ const metricMocks = getMetricMock();
766
+ const globalStorage = getStorage(apiClientMock, metricMocks.mockMetrics);
661
767
  const response = await globalStorage.listCustomEntities({});
662
768
  expect(response).toMatchObject({
663
769
  results: [],
@@ -666,6 +772,7 @@ describe('GlobalStorage', () => {
666
772
  verifyApiClientCalledWith(apiClientMock, {
667
773
  contextAri
668
774
  }, gql_queries_1.CustomEntityQueries.listQuery(contextAri, {}).query);
775
+ checkMetricsFired(metricMocks, { store: 'typed', operation: 'query', encrypted: false }, true);
669
776
  });
670
777
  it('should return cursor when results are not present', async () => {
671
778
  const apiClientMock = getApiClientMock({
@@ -676,7 +783,8 @@ describe('GlobalStorage', () => {
676
783
  }
677
784
  }
678
785
  });
679
- const globalStorage = getStorage(apiClientMock);
786
+ const metricMocks = getMetricMock();
787
+ const globalStorage = getStorage(apiClientMock, metricMocks.mockMetrics);
680
788
  const response = await globalStorage.listCustomEntities({});
681
789
  expect(response).toMatchObject({
682
790
  results: [],
@@ -685,6 +793,27 @@ describe('GlobalStorage', () => {
685
793
  verifyApiClientCalledWith(apiClientMock, {
686
794
  contextAri
687
795
  }, gql_queries_1.CustomEntityQueries.listQuery(contextAri, {}).query);
796
+ checkMetricsFired(metricMocks, { store: 'typed', operation: 'query', encrypted: false }, true);
797
+ });
798
+ it('should throw an error if the storage API returns an error', async () => {
799
+ const apiClientMock = getApiClientMock({
800
+ errors: [INVALID_CURSOR_ERROR]
801
+ });
802
+ const metricMocks = getMetricMock();
803
+ const globalStorage = getStorage(apiClientMock, metricMocks.mockMetrics);
804
+ const response = globalStorage.listCustomEntities({});
805
+ expect(apiClientMock).toHaveBeenCalled();
806
+ await expect(response).rejects.toThrow(errors_1.APIError.forErrorCode('CURSOR_INVALID', 'error message'));
807
+ checkMetricsFired(metricMocks, { store: 'typed', operation: 'query', encrypted: false }, false);
808
+ });
809
+ it('should throw an error if the storage API returns a non 200 status code and has no body', async () => {
810
+ const apiClientMock = getApiClientMockInvalidJson('', 400);
811
+ const metricMocks = getMetricMock();
812
+ const globalStorage = getStorage(apiClientMock, metricMocks.mockMetrics);
813
+ const response = globalStorage.listCustomEntities({});
814
+ expect(apiClientMock).toHaveBeenCalled();
815
+ await expect(response).rejects.toThrow(errors_1.APIError.forStatus(400));
816
+ checkMetricsFired(metricMocks, { store: 'typed', operation: 'query', encrypted: false }, false);
688
817
  });
689
818
  });
690
819
  });
@@ -2,17 +2,26 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.CustomEntityBuilder = exports.CustomEntityIndexBuilder = void 0;
4
4
  class CustomEntityQueryBuilder {
5
+ globalStorage;
6
+ queryOptions;
5
7
  constructor(globalStorage, queryOptions = {}) {
6
8
  this.globalStorage = globalStorage;
7
9
  this.queryOptions = queryOptions;
8
- this.queryOptions = Object.assign({}, queryOptions);
10
+ this.queryOptions = {
11
+ ...queryOptions
12
+ };
9
13
  }
10
14
  clone(overrides) {
11
- return new (Object.getPrototypeOf(this).constructor)(this.globalStorage, Object.assign(Object.assign({}, this.queryOptions), overrides));
15
+ return new (Object.getPrototypeOf(this).constructor)(this.globalStorage, {
16
+ ...this.queryOptions,
17
+ ...overrides
18
+ });
12
19
  }
13
20
  where(condition) {
14
21
  return this.clone({
15
- range: Object.assign({}, condition)
22
+ range: {
23
+ ...condition
24
+ }
16
25
  });
17
26
  }
18
27
  sort(sort) {
@@ -32,7 +41,7 @@ class CustomEntityQueryBuilder {
32
41
  }
33
42
  async getOne() {
34
43
  const { results } = await this.limit(1).getMany();
35
- return results === null || results === void 0 ? void 0 : results[0];
44
+ return results?.[0];
36
45
  }
37
46
  async getMany() {
38
47
  if (!this.queryOptions.entityName) {
@@ -41,7 +50,7 @@ class CustomEntityQueryBuilder {
41
50
  if (!this.queryOptions.indexName) {
42
51
  throw new Error('indexName is mandatory');
43
52
  }
44
- const queryOptions = Object.assign({}, this.queryOptions);
53
+ const queryOptions = { ...this.queryOptions };
45
54
  if (!queryOptions.filterOperator && queryOptions.filters) {
46
55
  queryOptions.filterOperator = 'and';
47
56
  }
@@ -49,41 +58,55 @@ class CustomEntityQueryBuilder {
49
58
  }
50
59
  }
51
60
  class CustomEntityAndFilterQueryBuilder extends CustomEntityQueryBuilder {
61
+ globalStorage;
62
+ queryOptions;
52
63
  constructor(globalStorage, queryOptions = {}) {
53
64
  super(globalStorage, queryOptions);
54
65
  this.globalStorage = globalStorage;
55
66
  this.queryOptions = queryOptions;
56
- this.queryOptions = Object.assign({}, queryOptions);
67
+ this.queryOptions = {
68
+ ...queryOptions
69
+ };
57
70
  }
58
71
  andFilter(field, condition) {
59
- var _a;
60
- const newQueryOptions = Object.assign({}, this.queryOptions);
61
- newQueryOptions.filters = [...((_a = this.queryOptions.filters) !== null && _a !== void 0 ? _a : []), Object.assign({ property: field }, condition)];
72
+ const newQueryOptions = {
73
+ ...this.queryOptions
74
+ };
75
+ newQueryOptions.filters = [...(this.queryOptions.filters ?? []), { property: field, ...condition }];
62
76
  newQueryOptions.filterOperator = 'and';
63
77
  return new CustomEntityAndFilterQueryBuilder(this.globalStorage, newQueryOptions);
64
78
  }
65
79
  }
66
80
  class CustomEntityOrFilterQueryBuilder extends CustomEntityQueryBuilder {
81
+ globalStorage;
82
+ queryOptions;
67
83
  constructor(globalStorage, queryOptions = {}) {
68
84
  super(globalStorage, queryOptions);
69
85
  this.globalStorage = globalStorage;
70
86
  this.queryOptions = queryOptions;
71
- this.queryOptions = Object.assign({}, queryOptions);
87
+ this.queryOptions = {
88
+ ...queryOptions
89
+ };
72
90
  }
73
91
  orFilter(field, condition) {
74
- var _a;
75
- const newQueryOptions = Object.assign({}, this.queryOptions);
76
- newQueryOptions.filters = [...((_a = this.queryOptions.filters) !== null && _a !== void 0 ? _a : []), Object.assign({ property: field }, condition)];
92
+ const newQueryOptions = {
93
+ ...this.queryOptions
94
+ };
95
+ newQueryOptions.filters = [...(this.queryOptions.filters ?? []), { property: field, ...condition }];
77
96
  newQueryOptions.filterOperator = 'or';
78
97
  return new CustomEntityOrFilterQueryBuilder(this.globalStorage, newQueryOptions);
79
98
  }
80
99
  }
81
100
  class CustomEntityFilterQueryBuilder extends CustomEntityQueryBuilder {
101
+ globalStorage;
102
+ queryOptions;
82
103
  constructor(globalStorage, queryOptions = {}) {
83
104
  super(globalStorage, queryOptions);
84
105
  this.globalStorage = globalStorage;
85
106
  this.queryOptions = queryOptions;
86
- this.queryOptions = Object.assign({}, queryOptions);
107
+ this.queryOptions = {
108
+ ...queryOptions
109
+ };
87
110
  }
88
111
  andFilter(field, condition) {
89
112
  return new CustomEntityAndFilterQueryBuilder(this.globalStorage, this.queryOptions).andFilter(field, condition);
@@ -93,25 +116,39 @@ class CustomEntityFilterQueryBuilder extends CustomEntityQueryBuilder {
93
116
  }
94
117
  }
95
118
  class CustomEntityIndexBuilder {
119
+ globalStorage;
120
+ queryOptions;
96
121
  constructor(globalStorage, queryOptions = {}) {
97
122
  this.globalStorage = globalStorage;
98
123
  this.queryOptions = queryOptions;
99
- this.queryOptions = Object.assign({}, queryOptions);
124
+ this.queryOptions = {
125
+ ...queryOptions
126
+ };
100
127
  }
101
128
  index(name, indexOptions) {
102
- const indexProperties = indexOptions ? Object.assign({ indexName: name }, indexOptions) : { indexName: name };
103
- return new CustomEntityFilterQueryBuilder(this.globalStorage, Object.assign(Object.assign({}, this.queryOptions), indexProperties));
129
+ const indexProperties = indexOptions ? { indexName: name, ...indexOptions } : { indexName: name };
130
+ return new CustomEntityFilterQueryBuilder(this.globalStorage, {
131
+ ...this.queryOptions,
132
+ ...indexProperties
133
+ });
104
134
  }
105
135
  }
106
136
  exports.CustomEntityIndexBuilder = CustomEntityIndexBuilder;
107
137
  class CustomEntityBuilder {
138
+ globalStorage;
139
+ queryOptions;
108
140
  constructor(globalStorage, queryOptions = {}) {
109
141
  this.globalStorage = globalStorage;
110
142
  this.queryOptions = queryOptions;
111
- this.queryOptions = Object.assign({}, queryOptions);
143
+ this.queryOptions = {
144
+ ...queryOptions
145
+ };
112
146
  }
113
147
  entity(name) {
114
- return new CustomEntityIndexBuilder(this.globalStorage, Object.assign(Object.assign({}, this.queryOptions), { entityName: name }));
148
+ return new CustomEntityIndexBuilder(this.globalStorage, {
149
+ ...this.queryOptions,
150
+ entityName: name
151
+ });
115
152
  }
116
153
  }
117
154
  exports.CustomEntityBuilder = CustomEntityBuilder;
@@ -3,6 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.EntityStorageBuilder = void 0;
4
4
  const query_api_1 = require("./query-api");
5
5
  class EntityStorageBuilder {
6
+ entityName;
7
+ globalStorage;
6
8
  constructor(entityName, globalStorage) {
7
9
  this.entityName = entityName;
8
10
  this.globalStorage = globalStorage;
package/out/errors.js CHANGED
@@ -2,7 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.APIError = exports.getErrorMessage = exports.getErrorMessageFromCode = void 0;
4
4
  const getErrorMessageFromCode = (code, message) => {
5
- return message !== null && message !== void 0 ? message : code;
5
+ return message ?? code;
6
6
  };
7
7
  exports.getErrorMessageFromCode = getErrorMessageFromCode;
8
8
  const getErrorMessage = (statusCode) => {
@@ -1,6 +1,7 @@
1
1
  import { FetchMethod } from './index';
2
2
  import { CustomEntityListOptions, ListOptions } from './query-interfaces';
3
3
  import { SharedStorageAdapter } from './storage-adapter';
4
+ import type { Metrics } from '@forge/util/packages/metrics-interface';
4
5
  interface ListResults {
5
6
  results: {
6
7
  key: string;
@@ -8,11 +9,14 @@ interface ListResults {
8
9
  }[];
9
10
  nextCursor?: string;
10
11
  }
12
+ export declare type StoreType = 'typed' | 'untyped';
13
+ export declare type OperationType = 'get' | 'set' | 'query' | 'delete';
11
14
  export declare class GlobalStorage implements SharedStorageAdapter {
12
15
  private getAppContextAri;
13
16
  private apiClient;
17
+ private readonly getMetrics;
14
18
  private readonly endpoint;
15
- constructor(getAppContextAri: (() => string) | string, apiClient: FetchMethod);
19
+ constructor(getAppContextAri: (() => string) | string, apiClient: FetchMethod, getMetrics: () => Metrics);
16
20
  private doGetAppContextAri;
17
21
  get(key: string): Promise<any>;
18
22
  getSecret(key: string): Promise<any>;
@@ -30,6 +34,7 @@ export declare class GlobalStorage implements SharedStorageAdapter {
30
34
  private buildRequest;
31
35
  private query;
32
36
  private mutation;
37
+ private wrapInMetric;
33
38
  }
34
39
  export {};
35
40
  //# sourceMappingURL=global-storage.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"global-storage.d.ts","sourceRoot":"","sources":["../src/global-storage.ts"],"names":[],"mappings":"AAAA,OAAO,EAAe,WAAW,EAAE,MAAM,SAAS,CAAC;AAInD,OAAO,EAAE,uBAAuB,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAC1E,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AAEzD,UAAU,WAAW;IACnB,OAAO,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,GAAG,CAAA;KAAE,EAAE,CAAC;IACvC,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AA2CD,qBAAa,aAAc,YAAW,oBAAoB;IAGtD,OAAO,CAAC,gBAAgB;IACxB,OAAO,CAAC,SAAS;IAHnB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA6B;gBAE5C,gBAAgB,EAAE,CAAC,MAAM,MAAM,CAAC,GAAG,MAAM,EACzC,SAAS,EAAE,WAAW;IAGhC,OAAO,CAAC,kBAAkB;IAIpB,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC;IAI9B,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC;IAIpC,IAAI,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC;IAqBhD,kBAAkB,CAAC,OAAO,EAAE,uBAAuB,GAAG,OAAO,CAAC,WAAW,CAAC;IAc1E,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC;IAK3C,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC;IAKjD,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAKlC,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAKxC,SAAS,CAAC,CAAC,EAAE,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC;IAI/D,SAAS,CAAC,CAAC,EAAE,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAK5E,YAAY,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;YAK1D,WAAW;YAUX,iBAAiB;IAU/B,OAAO,CAAC,YAAY;YAUN,KAAK;YAML,QAAQ;CAqBvB"}
1
+ {"version":3,"file":"global-storage.d.ts","sourceRoot":"","sources":["../src/global-storage.ts"],"names":[],"mappings":"AAAA,OAAO,EAAe,WAAW,EAAE,MAAM,SAAS,CAAC;AAInD,OAAO,EAAE,uBAAuB,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAC1E,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AACzD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,wCAAwC,CAAC;AAEtE,UAAU,WAAW;IACnB,OAAO,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,GAAG,CAAA;KAAE,EAAE,CAAC;IACvC,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAcD,oBAAY,SAAS,GAAG,OAAO,GAAG,SAAS,CAAC;AAC5C,oBAAY,aAAa,GAAG,KAAK,GAAG,KAAK,GAAG,OAAO,GAAG,QAAQ,CAAC;AA+B/D,qBAAa,aAAc,YAAW,oBAAoB;IAGtD,OAAO,CAAC,gBAAgB;IACxB,OAAO,CAAC,SAAS;IACjB,OAAO,CAAC,QAAQ,CAAC,UAAU;IAJ7B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA6B;gBAE5C,gBAAgB,EAAE,CAAC,MAAM,MAAM,CAAC,GAAG,MAAM,EACzC,SAAS,EAAE,WAAW,EACb,UAAU,EAAE,MAAM,OAAO;IAG5C,OAAO,CAAC,kBAAkB;IAIpB,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC;IAI9B,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC;IAIpC,IAAI,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC;IAqBhD,kBAAkB,CAAC,OAAO,EAAE,uBAAuB,GAAG,OAAO,CAAC,WAAW,CAAC;IAc1E,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC;IAU3C,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC;IAUjD,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAOlC,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAOxC,SAAS,CAAC,CAAC,EAAE,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC;IAI/D,SAAS,CAAC,CAAC,EAAE,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAO5E,YAAY,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;YAU1D,WAAW;YAUX,iBAAiB;IAU/B,OAAO,CAAC,YAAY;YAUN,KAAK;YAML,QAAQ;YAsBR,YAAY;CAoC3B"}
@@ -25,10 +25,14 @@ async function getResponseBody(response) {
25
25
  return responseBody.data;
26
26
  }
27
27
  class GlobalStorage {
28
- constructor(getAppContextAri, apiClient) {
28
+ getAppContextAri;
29
+ apiClient;
30
+ getMetrics;
31
+ endpoint = '/forge/entities/graphql';
32
+ constructor(getAppContextAri, apiClient, getMetrics) {
29
33
  this.getAppContextAri = getAppContextAri;
30
34
  this.apiClient = apiClient;
31
- this.endpoint = '/forge/entities/graphql';
35
+ this.getMetrics = getMetrics;
32
36
  }
33
37
  doGetAppContextAri() {
34
38
  return typeof this.getAppContextAri === 'function' ? this.getAppContextAri() : this.getAppContextAri;
@@ -43,7 +47,7 @@ class GlobalStorage {
43
47
  const requestBody = process.env.IS_CLEANUP_FUNCTION === 'true'
44
48
  ? gql_queries_1.UntypedQueries.listQueryForCleanup(this.doGetAppContextAri(), options)
45
49
  : gql_queries_1.UntypedQueries.listQuery(this.doGetAppContextAri(), options);
46
- const response = await this.query(requestBody);
50
+ const response = await this.wrapInMetric('untyped', 'query', false, async () => await this.query(requestBody));
47
51
  const edges = process.env.IS_CLEANUP_FUNCTION === 'true'
48
52
  ? response.appStoredEntitiesForCleanup.edges
49
53
  : response.appStoredEntities.edges;
@@ -56,7 +60,7 @@ class GlobalStorage {
56
60
  }
57
61
  async listCustomEntities(options) {
58
62
  const requestBody = gql_queries_1.CustomEntityQueries.listQuery(this.doGetAppContextAri(), options);
59
- const response = await this.query(requestBody);
63
+ const response = await this.wrapInMetric('typed', 'query', false, async () => await this.query(requestBody));
60
64
  const edges = response.appStoredCustomEntities.edges;
61
65
  const results = edges.map(({ node }) => node);
62
66
  return {
@@ -66,40 +70,40 @@ class GlobalStorage {
66
70
  }
67
71
  async set(key, value) {
68
72
  const requestBody = gql_queries_1.UntypedQueries.set(this.doGetAppContextAri(), key, value, false);
69
- await this.mutation(requestBody, 'appStorage', 'setAppStoredEntity');
73
+ await this.wrapInMetric('untyped', 'set', false, async () => await this.mutation(requestBody, 'appStorage', 'setAppStoredEntity'));
70
74
  }
71
75
  async setSecret(key, value) {
72
76
  const requestBody = gql_queries_1.UntypedQueries.set(this.doGetAppContextAri(), key, value, true);
73
- await this.mutation(requestBody, 'appStorage', 'setAppStoredEntity');
77
+ await this.wrapInMetric('untyped', 'set', true, async () => await this.mutation(requestBody, 'appStorage', 'setAppStoredEntity'));
74
78
  }
75
79
  async delete(key) {
76
80
  const requestBody = gql_queries_1.UntypedQueries.delete(this.doGetAppContextAri(), key, false);
77
- await this.mutation(requestBody, 'appStorage', 'deleteAppStoredEntity');
81
+ await this.wrapInMetric('untyped', 'delete', false, async () => this.mutation(requestBody, 'appStorage', 'deleteAppStoredEntity'));
78
82
  }
79
83
  async deleteSecret(key) {
80
84
  const requestBody = gql_queries_1.UntypedQueries.delete(this.doGetAppContextAri(), key, true);
81
- await this.mutation(requestBody, 'appStorage', 'deleteAppStoredEntity');
85
+ await this.wrapInMetric('untyped', 'delete', true, async () => this.mutation(requestBody, 'appStorage', 'deleteAppStoredEntity'));
82
86
  }
83
87
  async getEntity(entityName, entityKey) {
84
88
  return this.getEntityInternal(entityName, entityKey);
85
89
  }
86
90
  async setEntity(entityName, entityKey, value) {
87
91
  const requestBody = gql_queries_1.CustomEntityQueries.set(this.doGetAppContextAri(), entityName, entityKey, value);
88
- await this.mutation(requestBody, 'appStorageCustomEntity', 'setAppStoredCustomEntity');
92
+ await this.wrapInMetric('typed', 'set', false, async () => this.mutation(requestBody, 'appStorageCustomEntity', 'setAppStoredCustomEntity'));
89
93
  }
90
94
  async deleteEntity(entityName, entityKey) {
91
95
  const requestBody = gql_queries_1.CustomEntityQueries.delete(this.doGetAppContextAri(), entityName, entityKey);
92
- await this.mutation(requestBody, 'appStorageCustomEntity', 'deleteAppStoredCustomEntity');
96
+ await this.wrapInMetric('typed', 'delete', false, async () => await this.mutation(requestBody, 'appStorageCustomEntity', 'deleteAppStoredCustomEntity'));
93
97
  }
94
98
  async getInternal(key, encrypted) {
95
99
  const requestBody = gql_queries_1.UntypedQueries.get(this.doGetAppContextAri(), key, encrypted);
96
- const { appStoredEntity: { value } } = await this.query(requestBody);
97
- return value !== null && value !== void 0 ? value : undefined;
100
+ const { appStoredEntity: { value } } = await this.wrapInMetric('untyped', 'get', encrypted, async () => await this.query(requestBody));
101
+ return value ?? undefined;
98
102
  }
99
103
  async getEntityInternal(entityName, entityKey) {
100
104
  const requestBody = gql_queries_1.CustomEntityQueries.get(this.doGetAppContextAri(), entityName, entityKey);
101
- const { appStoredCustomEntity: { value } } = await this.query(requestBody);
102
- return value !== null && value !== void 0 ? value : undefined;
105
+ const { appStoredCustomEntity: { value } } = await this.wrapInMetric('typed', 'get', false, async () => await this.query(requestBody));
106
+ return value ?? undefined;
103
107
  }
104
108
  buildRequest(requestBody) {
105
109
  return {
@@ -123,5 +127,36 @@ class GlobalStorage {
123
127
  }
124
128
  return response;
125
129
  }
130
+ async wrapInMetric(store, operation, encrypted, fn) {
131
+ const metrics = this.getMetrics();
132
+ const timer = metrics
133
+ .timing('forge.runtime.storage.operation.latency', { store, operation, encrypted: String(encrypted) })
134
+ .measure();
135
+ try {
136
+ const result = await fn();
137
+ timer.stop({ success: 'true' });
138
+ metrics
139
+ .counter('forge.runtime.storage.operation', {
140
+ store,
141
+ operation,
142
+ encrypted: String(encrypted),
143
+ success: 'true'
144
+ })
145
+ .incr();
146
+ return result;
147
+ }
148
+ catch (error) {
149
+ timer.stop({ success: 'false' });
150
+ metrics
151
+ .counter('forge.runtime.storage.operation', {
152
+ store,
153
+ operation,
154
+ encrypted: String(encrypted),
155
+ success: 'false'
156
+ })
157
+ .incr();
158
+ throw error;
159
+ }
160
+ }
126
161
  }
127
162
  exports.GlobalStorage = GlobalStorage;
@@ -2,10 +2,8 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.CustomEntityQueries = exports.UntypedQueries = void 0;
4
4
  class UntypedQueries {
5
- }
6
- exports.UntypedQueries = UntypedQueries;
7
- UntypedQueries.get = (contextAri, key, encrypted) => ({
8
- query: `
5
+ static get = (contextAri, key, encrypted) => ({
6
+ query: `
9
7
  query forge_app_getApplicationStorageEntity($contextAri: ID!, $key: ID!, $encrypted: Boolean!) {
10
8
  appStoredEntity(contextAri: $contextAri, key: $key, encrypted: $encrypted) {
11
9
  key
@@ -13,14 +11,14 @@ UntypedQueries.get = (contextAri, key, encrypted) => ({
13
11
  }
14
12
  }
15
13
  `,
16
- variables: {
17
- contextAri,
18
- key,
19
- encrypted
20
- }
21
- });
22
- UntypedQueries.set = (contextAri, key, value, encrypted) => ({
23
- query: `
14
+ variables: {
15
+ contextAri,
16
+ key,
17
+ encrypted
18
+ }
19
+ });
20
+ static set = (contextAri, key, value, encrypted) => ({
21
+ query: `
24
22
  mutation forge_app_setApplicationStorageEntity($input: SetAppStoredEntityMutationInput!) {
25
23
  appStorage{
26
24
  setAppStoredEntity(input: $input) {
@@ -37,17 +35,17 @@ UntypedQueries.set = (contextAri, key, value, encrypted) => ({
37
35
  }
38
36
  }
39
37
  `,
40
- variables: {
41
- input: {
42
- contextAri,
43
- key,
44
- value,
45
- encrypted
38
+ variables: {
39
+ input: {
40
+ contextAri,
41
+ key,
42
+ value,
43
+ encrypted
44
+ }
46
45
  }
47
- }
48
- });
49
- UntypedQueries.delete = (contextAri, key, encrypted) => ({
50
- query: `
46
+ });
47
+ static delete = (contextAri, key, encrypted) => ({
48
+ query: `
51
49
  mutation forge_app_deleteApplicationStorageEntity($input: DeleteAppStoredEntityMutationInput!) {
52
50
  appStorage {
53
51
  deleteAppStoredEntity(input: $input) {
@@ -64,17 +62,15 @@ UntypedQueries.delete = (contextAri, key, encrypted) => ({
64
62
  }
65
63
  }
66
64
  `,
67
- variables: {
68
- input: {
69
- contextAri,
70
- key,
71
- encrypted
65
+ variables: {
66
+ input: {
67
+ contextAri,
68
+ key,
69
+ encrypted
70
+ }
72
71
  }
73
- }
74
- });
75
- UntypedQueries.listQuery = (contextAri, options) => {
76
- var _a, _b, _c;
77
- return ({
72
+ });
73
+ static listQuery = (contextAri, options) => ({
78
74
  query: `
79
75
  query forge_app_getApplicationStorageEntities($contextAri: ID!, $where: [AppStoredEntityFilter!], $cursor: String, $limit: Int) {
80
76
  appStoredEntities(contextAri: $contextAri, where: $where, after: $cursor, first: $limit) {
@@ -91,15 +87,12 @@ UntypedQueries.listQuery = (contextAri, options) => {
91
87
  `,
92
88
  variables: {
93
89
  contextAri,
94
- where: (_a = options.where) !== null && _a !== void 0 ? _a : null,
95
- cursor: (_b = options.cursor) !== null && _b !== void 0 ? _b : null,
96
- limit: (_c = options.limit) !== null && _c !== void 0 ? _c : null
90
+ where: options.where ?? null,
91
+ cursor: options.cursor ?? null,
92
+ limit: options.limit ?? null
97
93
  }
98
94
  });
99
- };
100
- UntypedQueries.listQueryForCleanup = (contextAri, options) => {
101
- var _a, _b, _c;
102
- return ({
95
+ static listQueryForCleanup = (contextAri, options) => ({
103
96
  query: `
104
97
  query forge_app_getApplicationStorageEntitiesForCleanup($contextAri: ID!, $where: [AppStoredEntityFilter!], $cursor: String, $limit: Int) {
105
98
  appStoredEntitiesForCleanup(contextAri: $contextAri, where: $where, after: $cursor, first: $limit) {
@@ -116,17 +109,16 @@ UntypedQueries.listQueryForCleanup = (contextAri, options) => {
116
109
  `,
117
110
  variables: {
118
111
  contextAri,
119
- where: (_a = options.where) !== null && _a !== void 0 ? _a : null,
120
- cursor: (_b = options.cursor) !== null && _b !== void 0 ? _b : null,
121
- limit: (_c = options.limit) !== null && _c !== void 0 ? _c : null
112
+ where: options.where ?? null,
113
+ cursor: options.cursor ?? null,
114
+ limit: options.limit ?? null
122
115
  }
123
116
  });
124
- };
125
- class CustomEntityQueries {
126
117
  }
127
- exports.CustomEntityQueries = CustomEntityQueries;
128
- CustomEntityQueries.get = (contextAri, entityName, key) => ({
129
- query: `
118
+ exports.UntypedQueries = UntypedQueries;
119
+ class CustomEntityQueries {
120
+ static get = (contextAri, entityName, key) => ({
121
+ query: `
130
122
  query forge_app_getApplicationStorageCustomEntity ($contextAri: ID!, $key: ID!, $entityName: String!) {
131
123
  appStoredCustomEntity(contextAri: $contextAri, key: $key, entityName: $entityName) {
132
124
  value
@@ -135,14 +127,14 @@ CustomEntityQueries.get = (contextAri, entityName, key) => ({
135
127
  }
136
128
  }
137
129
  `,
138
- variables: {
139
- contextAri,
140
- entityName,
141
- key
142
- }
143
- });
144
- CustomEntityQueries.set = (contextAri, entityName, key, value) => ({
145
- query: `
130
+ variables: {
131
+ contextAri,
132
+ entityName,
133
+ key
134
+ }
135
+ });
136
+ static set = (contextAri, entityName, key, value) => ({
137
+ query: `
146
138
  mutation forge_app_setApplicationStorageCustomEntity($input: SetAppStoredCustomEntityMutationInput!) {
147
139
  appStorageCustomEntity{
148
140
  setAppStoredCustomEntity(input: $input) {
@@ -159,17 +151,17 @@ CustomEntityQueries.set = (contextAri, entityName, key, value) => ({
159
151
  }
160
152
  }
161
153
  `,
162
- variables: {
163
- input: {
164
- contextAri,
165
- entityName,
166
- key,
167
- value
154
+ variables: {
155
+ input: {
156
+ contextAri,
157
+ entityName,
158
+ key,
159
+ value
160
+ }
168
161
  }
169
- }
170
- });
171
- CustomEntityQueries.delete = (contextAri, entityName, key) => ({
172
- query: `
162
+ });
163
+ static delete = (contextAri, entityName, key) => ({
164
+ query: `
173
165
  mutation forge_app_deleteApplicationStorageCustomEntity($input: DeleteAppStoredCustomEntityMutationInput!) {
174
166
  appStorageCustomEntity {
175
167
  deleteAppStoredCustomEntity(input: $input) {
@@ -186,17 +178,17 @@ CustomEntityQueries.delete = (contextAri, entityName, key) => ({
186
178
  }
187
179
  }
188
180
  `,
189
- variables: {
190
- input: {
191
- contextAri,
192
- entityName,
193
- key
181
+ variables: {
182
+ input: {
183
+ contextAri,
184
+ entityName,
185
+ key
186
+ }
194
187
  }
195
- }
196
- });
197
- CustomEntityQueries.listQuery = (contextAri, options) => {
198
- return {
199
- query: `
188
+ });
189
+ static listQuery = (contextAri, options) => {
190
+ return {
191
+ query: `
200
192
  query AppStorageCustomEntityQueries ($contextAri: ID!, $entityName: String!, $indexName: String!, $range: AppStoredCustomEntityRange, $filters: AppStoredCustomEntityFilters, $sort:SortOrder, $limit: Int, $cursor: String, $partition: [AppStoredCustomEntityFieldValue!]) {
201
193
  appStoredCustomEntities(contextAri: $contextAri, entityName: $entityName, indexName: $indexName, range: $range, filters: $filters, sort:$sort, limit: $limit, cursor: $cursor, partition: $partition) {
202
194
  edges {
@@ -215,12 +207,24 @@ CustomEntityQueries.listQuery = (contextAri, options) => {
215
207
  }
216
208
  }
217
209
  `,
218
- variables: Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({ contextAri, entityName: options.entityName, indexName: options.indexName, range: options.range }, (options.filters && options.filters.length
219
- ? {
220
- filters: {
221
- [options.filterOperator || 'and']: options.filters
222
- }
210
+ variables: {
211
+ contextAri,
212
+ entityName: options.entityName,
213
+ indexName: options.indexName,
214
+ range: options.range,
215
+ ...(options.filters && options.filters.length
216
+ ? {
217
+ filters: {
218
+ [options.filterOperator || 'and']: options.filters
219
+ }
220
+ }
221
+ : {}),
222
+ ...(options.partition ? { partition: options.partition } : {}),
223
+ ...(options.sort ? { sort: options.sort } : {}),
224
+ ...(options.cursor ? { cursor: options.cursor } : {}),
225
+ ...(options.limit ? { limit: options.limit } : {})
223
226
  }
224
- : {})), (options.partition ? { partition: options.partition } : {})), (options.sort ? { sort: options.sort } : {})), (options.cursor ? { cursor: options.cursor } : {})), (options.limit ? { limit: options.limit } : {}))
227
+ };
225
228
  };
226
- };
229
+ }
230
+ exports.CustomEntityQueries = CustomEntityQueries;
package/out/query-api.js CHANGED
@@ -2,20 +2,34 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.DefaultQueryBuilder = void 0;
4
4
  class DefaultQueryBuilder {
5
+ globalStorage;
6
+ queryOptions;
5
7
  constructor(globalStorage, queryOptions = {}) {
6
8
  this.globalStorage = globalStorage;
7
9
  this.queryOptions = queryOptions;
8
10
  }
9
11
  where(field, where) {
10
- return new DefaultQueryBuilder(this.globalStorage, Object.assign(Object.assign({}, this.queryOptions), { where: [
11
- Object.assign({ field }, where)
12
- ] }));
12
+ return new DefaultQueryBuilder(this.globalStorage, {
13
+ ...this.queryOptions,
14
+ where: [
15
+ {
16
+ field,
17
+ ...where
18
+ }
19
+ ]
20
+ });
13
21
  }
14
22
  cursor(cursor) {
15
- return new DefaultQueryBuilder(this.globalStorage, Object.assign(Object.assign({}, this.queryOptions), { cursor }));
23
+ return new DefaultQueryBuilder(this.globalStorage, {
24
+ ...this.queryOptions,
25
+ cursor
26
+ });
16
27
  }
17
28
  limit(limit) {
18
- return new DefaultQueryBuilder(this.globalStorage, Object.assign(Object.assign({}, this.queryOptions), { limit }));
29
+ return new DefaultQueryBuilder(this.globalStorage, {
30
+ ...this.queryOptions,
31
+ limit
32
+ });
19
33
  }
20
34
  async getOne() {
21
35
  const { results } = await this.limit(1).getMany();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forge/storage",
3
- "version": "1.5.15",
3
+ "version": "1.6.0-next.0",
4
4
  "description": "Forge Storage methods",
5
5
  "author": "Atlassian",
6
6
  "license": "UNLICENSED",
@@ -17,6 +17,8 @@
17
17
  "devDependencies": {
18
18
  "@types/node": "14.18.63",
19
19
  "@types/node-fetch": "^2.6.11",
20
- "node-fetch": "2.7.0"
20
+ "node-fetch": "2.7.0",
21
+ "@forge/util": "1.4.4",
22
+ "@atlassian/metrics-interface": "4.0.0"
21
23
  }
22
- }
24
+ }