@api3/commons 0.1.0 → 0.3.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 (54) hide show
  1. package/README.md +17 -19
  2. package/dist/eslint/jest.d.ts +1 -0
  3. package/dist/eslint/jest.js +1 -0
  4. package/dist/eslint/jest.js.map +1 -1
  5. package/dist/eslint/react.d.ts +0 -1
  6. package/dist/eslint/react.js +0 -1
  7. package/dist/eslint/react.js.map +1 -1
  8. package/dist/eslint/universal.d.ts +1 -5
  9. package/dist/eslint/universal.js +2 -19
  10. package/dist/eslint/universal.js.map +1 -1
  11. package/dist/index.d.ts +2 -0
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +18 -2
  14. package/dist/index.js.map +1 -1
  15. package/dist/logger/index.d.ts +9 -20
  16. package/dist/logger/index.d.ts.map +1 -1
  17. package/dist/logger/index.js +28 -12
  18. package/dist/logger/index.js.map +1 -1
  19. package/dist/processing/index.d.ts +4 -0
  20. package/dist/processing/index.d.ts.map +1 -0
  21. package/dist/processing/index.js +20 -0
  22. package/dist/processing/index.js.map +1 -0
  23. package/dist/processing/processing.d.ts +39 -0
  24. package/dist/processing/processing.d.ts.map +1 -0
  25. package/dist/processing/processing.js +122 -0
  26. package/dist/processing/processing.js.map +1 -0
  27. package/dist/processing/schema.d.ts +3 -0
  28. package/dist/processing/schema.d.ts.map +1 -0
  29. package/dist/processing/schema.js +12 -0
  30. package/dist/processing/schema.js.map +1 -0
  31. package/dist/processing/unsafe-evaluate.d.ts +50 -0
  32. package/dist/processing/unsafe-evaluate.d.ts.map +1 -0
  33. package/dist/processing/unsafe-evaluate.js +178 -0
  34. package/dist/processing/unsafe-evaluate.js.map +1 -0
  35. package/dist/processing/vm-timers.d.ts +21 -0
  36. package/dist/processing/vm-timers.d.ts.map +1 -0
  37. package/dist/processing/vm-timers.js +54 -0
  38. package/dist/processing/vm-timers.js.map +1 -0
  39. package/package.json +8 -11
  40. package/src/eslint/README.md +0 -5
  41. package/src/eslint/jest.js +1 -0
  42. package/src/eslint/react.js +0 -1
  43. package/src/eslint/universal.js +2 -20
  44. package/src/index.ts +3 -2
  45. package/src/logger/README.md +1 -2
  46. package/src/logger/index.ts +37 -15
  47. package/src/processing/README.md +45 -0
  48. package/src/processing/index.ts +3 -0
  49. package/src/processing/processing.test.ts +272 -0
  50. package/src/processing/processing.ts +160 -0
  51. package/src/processing/schema.ts +10 -0
  52. package/src/processing/unsafe-evaluate.test.ts +103 -0
  53. package/src/processing/unsafe-evaluate.ts +178 -0
  54. package/src/processing/vm-timers.ts +58 -0
@@ -0,0 +1,272 @@
1
+ /* eslint-disable jest/prefer-strict-equal */ // Because the errors are thrown from the "vm" module (different context), they are not strictly equal.
2
+ import { createEndpoint } from '../../test/fixtures';
3
+
4
+ import {
5
+ addReservedParameters,
6
+ postProcessApiCallResponse,
7
+ preProcessApiCallParameters,
8
+ removeReservedParameters,
9
+ } from './processing';
10
+
11
+ describe(preProcessApiCallParameters.name, () => {
12
+ it('valid processing code', async () => {
13
+ const endpoint = createEndpoint({
14
+ preProcessingSpecifications: [
15
+ {
16
+ environment: 'Node',
17
+ value: 'const output = {...input, from: "ETH"};',
18
+ timeoutMs: 5000,
19
+ },
20
+ {
21
+ environment: 'Node',
22
+ value: 'const output = {...input, newProp: "airnode"};',
23
+ timeoutMs: 5000,
24
+ },
25
+ ],
26
+ });
27
+ const parameters = { _type: 'int256', _path: 'price' };
28
+
29
+ const result = await preProcessApiCallParameters(endpoint, parameters);
30
+
31
+ expect(result).toEqual({
32
+ _path: 'price',
33
+ _type: 'int256',
34
+ from: 'ETH',
35
+ newProp: 'airnode',
36
+ });
37
+ });
38
+
39
+ it('invalid processing code', async () => {
40
+ const endpoint = createEndpoint({
41
+ preProcessingSpecifications: [
42
+ {
43
+ environment: 'Node',
44
+ value: 'something invalid; const output = {...input, from: `ETH`};',
45
+ timeoutMs: 5000,
46
+ },
47
+ {
48
+ environment: 'Node',
49
+ value: 'const output = {...input, newProp: "airnode"};',
50
+ timeoutMs: 5000,
51
+ },
52
+ ],
53
+ });
54
+ const parameters = { _type: 'int256', _path: 'price', from: 'TBD' };
55
+
56
+ const throwingFunc = async () => preProcessApiCallParameters(endpoint, parameters);
57
+
58
+ await expect(throwingFunc).rejects.toEqual(new Error('SyntaxError: Unexpected identifier'));
59
+ });
60
+
61
+ it('demonstrates access to endpointParameters, but reserved parameters are inaccessible', async () => {
62
+ const parameters = { _type: 'int256', _path: 'price', to: 'USD' };
63
+ const endpoint = createEndpoint({
64
+ preProcessingSpecifications: [
65
+ {
66
+ environment: 'Node',
67
+ // pretend the user is trying to 1) override _path and 2) set a new parameter based on
68
+ // the presence of the reserved parameter _type (which is inaccessible)
69
+ value:
70
+ 'const output = {...input, from: "ETH", _path: "price.newpath", myVal: input._type ? "123" : "456", newTo: endpointParameters.to };',
71
+ timeoutMs: 5000,
72
+ },
73
+ ],
74
+ });
75
+
76
+ const result = await preProcessApiCallParameters(endpoint, parameters);
77
+
78
+ expect(result).toEqual({
79
+ _path: 'price', // is not overridden
80
+ _type: 'int256',
81
+ from: 'ETH', // originates from the processing code
82
+ to: 'USD', // should be unchanged from the original parameters
83
+ myVal: '456', // is set to "456" because _type is not present in the environment
84
+ newTo: 'USD', // demonstrates access to endpointParameters
85
+ });
86
+ });
87
+
88
+ it('uses native modules for processing', async () => {
89
+ const endpoint = createEndpoint({
90
+ preProcessingSpecifications: [
91
+ {
92
+ environment: 'Node',
93
+ value: `
94
+ const randomValue = crypto.randomBytes(4).toString('hex');
95
+ const output = {...input, randomValue};
96
+ `,
97
+ timeoutMs: 5000,
98
+ },
99
+ ],
100
+ });
101
+ const parameters = { _type: 'int256', _path: 'price' };
102
+
103
+ const result = await preProcessApiCallParameters(endpoint, parameters);
104
+
105
+ // Check that the result contains the original parameters and a valid 8-character hex random value.
106
+ expect(result).toMatchObject({
107
+ _path: 'price',
108
+ _type: 'int256',
109
+ });
110
+ expect(result.randomValue).toHaveLength(8);
111
+ expect(/^[\da-f]{8}$/i.test(result.randomValue)).toBe(true);
112
+ });
113
+
114
+ it('throws error due to processing timeout', async () => {
115
+ const endpoint = createEndpoint({
116
+ preProcessingSpecifications: [
117
+ {
118
+ environment: 'Node async',
119
+ value: `
120
+ const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
121
+ delay(5000);
122
+ const output = {...input, from: 'ETH'};
123
+ `,
124
+ timeoutMs: 100, // This timeout is shorter than the delay in the processing code.
125
+ },
126
+ ],
127
+ });
128
+ const parameters = { _type: 'int256', _path: 'price' };
129
+
130
+ const throwingFunc = async () => preProcessApiCallParameters(endpoint, parameters);
131
+
132
+ await expect(throwingFunc).rejects.toThrow('Timeout exceeded');
133
+ });
134
+ });
135
+
136
+ describe(postProcessApiCallResponse.name, () => {
137
+ it('processes valid code', async () => {
138
+ const parameters = { _type: 'int256', _path: 'price' };
139
+ const endpoint = createEndpoint({
140
+ postProcessingSpecifications: [
141
+ {
142
+ environment: 'Node',
143
+ value: 'const output = parseInt(input.price)*2;',
144
+ timeoutMs: 5000,
145
+ },
146
+ {
147
+ environment: 'Node',
148
+ value: 'const output = parseInt(input)*2;',
149
+ timeoutMs: 5000,
150
+ },
151
+ ],
152
+ });
153
+
154
+ const result = await postProcessApiCallResponse({ price: 1000 }, endpoint, parameters);
155
+
156
+ expect(result).toBe(4000);
157
+ });
158
+
159
+ it('demonstrates access to endpointParameters, but reserved parameters are inaccessible', async () => {
160
+ const myMultiplier = 10;
161
+ const parameters = { _type: 'int256', _path: 'price', myMultiplier };
162
+ const endpoint = createEndpoint({
163
+ postProcessingSpecifications: [
164
+ {
165
+ environment: 'Node',
166
+ value: `
167
+ const reservedMultiplier = endpointParameters._times ? 1 : 2;
168
+ const output = parseInt(input.price) * endpointParameters.myMultiplier * reservedMultiplier
169
+ `,
170
+ timeoutMs: 5000,
171
+ },
172
+ ],
173
+ });
174
+
175
+ const price = 1000;
176
+ const result = await postProcessApiCallResponse({ price }, endpoint, parameters);
177
+
178
+ // reserved parameters (_times) should be inaccessible to post-processing for the
179
+ // http-gateway, hence multiplication by 2 instead of 1
180
+ expect(result).toEqual(price * myMultiplier * 2);
181
+ });
182
+
183
+ it('throws on invalid code', async () => {
184
+ const parameters = { _type: 'int256', _path: 'price' };
185
+ const endpoint = createEndpoint({
186
+ postProcessingSpecifications: [
187
+ {
188
+ environment: 'Node',
189
+ value: 'const output = parseInt(input.price)*1000;',
190
+ timeoutMs: 5000,
191
+ },
192
+ {
193
+ environment: 'Node',
194
+ value: `
195
+ Something Unexpected;
196
+ const output = parseInt(input)*2;
197
+ `,
198
+ timeoutMs: 5000,
199
+ },
200
+ ],
201
+ });
202
+
203
+ const throwingFunc = async () => postProcessApiCallResponse({ price: 1000 }, endpoint, parameters);
204
+
205
+ await expect(throwingFunc).rejects.toEqual(new Error('SyntaxError: Unexpected identifier'));
206
+ });
207
+ });
208
+
209
+ describe(removeReservedParameters.name, () => {
210
+ it('removes all reserved parameters', () => {
211
+ const parameters = {
212
+ normalParam1: 'value1',
213
+ _type: 'int256',
214
+ _path: 'price',
215
+ normalParam2: 'value2',
216
+ };
217
+
218
+ const result = removeReservedParameters(parameters);
219
+
220
+ expect(result).toEqual({
221
+ normalParam1: 'value1',
222
+ normalParam2: 'value2',
223
+ });
224
+ });
225
+
226
+ it('returns same object if no reserved parameters found', () => {
227
+ const parameters = {
228
+ normalParam1: 'value1',
229
+ normalParam2: 'value2',
230
+ };
231
+
232
+ const result = removeReservedParameters(parameters);
233
+
234
+ expect(result).toEqual(parameters);
235
+ });
236
+ });
237
+
238
+ describe(addReservedParameters.name, () => {
239
+ it('adds reserved parameters from initial to modified parameters', () => {
240
+ const initialParameters = {
241
+ _type: 'int256',
242
+ _path: 'price',
243
+ };
244
+ const modifiedParameters = {
245
+ normalParam1: 'value1',
246
+ normalParam2: 'value2',
247
+ };
248
+
249
+ const result = addReservedParameters(initialParameters, modifiedParameters);
250
+
251
+ expect(result).toEqual({
252
+ normalParam1: 'value1',
253
+ normalParam2: 'value2',
254
+ _type: 'int256',
255
+ _path: 'price',
256
+ });
257
+ });
258
+
259
+ it('does not modify modifiedParameters if no reserved parameters in initialParameters', () => {
260
+ const initialParameters = {
261
+ normalParam3: 'value3',
262
+ };
263
+ const modifiedParameters = {
264
+ normalParam1: 'value1',
265
+ normalParam2: 'value2',
266
+ };
267
+
268
+ const result = addReservedParameters(initialParameters, modifiedParameters);
269
+
270
+ expect(result).toEqual(modifiedParameters);
271
+ });
272
+ });
@@ -0,0 +1,160 @@
1
+ import { type Endpoint, RESERVED_PARAMETERS } from '@api3/ois';
2
+ import { type GoAsyncOptions, go } from '@api3/promise-utils';
3
+
4
+ import { type ApiCallParameters, validateApiCallParameters } from './schema';
5
+ import { unsafeEvaluate, unsafeEvaluateAsync } from './unsafe-evaluate';
6
+
7
+ export const DEFAULT_PROCESSING_TIMEOUT_MS = 10_000;
8
+
9
+ const reservedParameters = RESERVED_PARAMETERS as string[]; // To avoid strict TS checks.
10
+
11
+ /**
12
+ * Removes reserved parameters from the parameters object.
13
+ * @param parameters The API call parameters from which reserved parameters will be removed.
14
+ * @returns The parameters object without reserved parameters.
15
+ */
16
+ export const removeReservedParameters = (parameters: ApiCallParameters): ApiCallParameters => {
17
+ const result: ApiCallParameters = {};
18
+
19
+ for (const key in parameters) {
20
+ if (!reservedParameters.includes(key)) {
21
+ result[key] = parameters[key];
22
+ }
23
+ }
24
+
25
+ return result;
26
+ };
27
+
28
+ /**
29
+ * Re-inserts reserved parameters from the initial parameters object into the modified parameters object.
30
+ * @param initialParameters The initial API call parameters that might contain reserved parameters.
31
+ * @param modifiedParameters The modified API call parameters to which reserved parameters will be added.
32
+ * @returns The modified parameters object with re-inserted reserved parameters.
33
+ */
34
+ export const addReservedParameters = (
35
+ initialParameters: ApiCallParameters,
36
+ modifiedParameters: ApiCallParameters
37
+ ): ApiCallParameters => {
38
+ for (const key in initialParameters) {
39
+ if (reservedParameters.includes(key)) {
40
+ modifiedParameters[key] = initialParameters[key];
41
+ }
42
+ }
43
+
44
+ return modifiedParameters;
45
+ };
46
+
47
+ /**
48
+ * Pre-processes API call parameters based on the provided endpoint's processing specifications.
49
+ *
50
+ * @param endpoint The endpoint containing processing specifications.
51
+ * @param apiCallParameters The parameters to be pre-processed.
52
+ * @param processingOptions Options to control the async processing behavior like retries and timeouts.
53
+ *
54
+ * @returns A promise that resolves to the pre-processed parameters.
55
+ */
56
+ export const preProcessApiCallParameters = async (
57
+ endpoint: Endpoint,
58
+ apiCallParameters: ApiCallParameters,
59
+ processingOptions: GoAsyncOptions = { retries: 0, totalTimeoutMs: DEFAULT_PROCESSING_TIMEOUT_MS }
60
+ ): Promise<ApiCallParameters> => {
61
+ const { preProcessingSpecifications } = endpoint;
62
+ if (!preProcessingSpecifications || preProcessingSpecifications.length === 0) {
63
+ return apiCallParameters;
64
+ }
65
+
66
+ // We only wrap the code through "go" utils because of the timeout and retry logic.
67
+ const goProcessedParameters = await go(async () => {
68
+ let currentValue: unknown = removeReservedParameters(apiCallParameters);
69
+
70
+ for (const processing of preProcessingSpecifications) {
71
+ // Provide endpoint parameters without reserved parameters immutably between steps. Recompute them for each
72
+ // snippet independently because processing snippets can modify the parameters.
73
+ const endpointParameters = removeReservedParameters(apiCallParameters);
74
+
75
+ switch (processing.environment) {
76
+ case 'Node': {
77
+ currentValue = await unsafeEvaluate(
78
+ processing.value,
79
+ { input: currentValue, endpointParameters },
80
+ processing.timeoutMs
81
+ );
82
+ break;
83
+ }
84
+ case 'Node async': {
85
+ currentValue = await unsafeEvaluateAsync(
86
+ processing.value,
87
+ { input: currentValue, endpointParameters },
88
+ processing.timeoutMs
89
+ );
90
+ break;
91
+ }
92
+ }
93
+ }
94
+
95
+ return currentValue;
96
+ }, processingOptions);
97
+ if (!goProcessedParameters.success) throw goProcessedParameters.error;
98
+
99
+ // Let this throw if the processed parameters are invalid.
100
+ const parsedParameters = validateApiCallParameters(goProcessedParameters.data);
101
+
102
+ // Having removed reserved parameters for pre-processing, we need to re-insert them for the API call.
103
+ return addReservedParameters(apiCallParameters, parsedParameters);
104
+ };
105
+
106
+ /**
107
+ * Post-processes the API call response based on the provided endpoint's processing specifications.
108
+ *
109
+ * @param apiCallResponse The raw response obtained from the API call.
110
+ * @param endpoint The endpoint containing processing specifications.
111
+ * @param apiCallParameters The parameters used in the API call.
112
+ * @param processingOptions Options to control the async processing behavior like retries and timeouts.
113
+ *
114
+ * @returns A promise that resolves to the post-processed API call response.
115
+ */
116
+ export const postProcessApiCallResponse = async (
117
+ apiCallResponse: unknown,
118
+ endpoint: Endpoint,
119
+ apiCallParameters: ApiCallParameters,
120
+ processingOptions: GoAsyncOptions = { retries: 0, totalTimeoutMs: DEFAULT_PROCESSING_TIMEOUT_MS }
121
+ ) => {
122
+ const { postProcessingSpecifications } = endpoint;
123
+ if (!postProcessingSpecifications || postProcessingSpecifications?.length === 0) {
124
+ return apiCallResponse;
125
+ }
126
+
127
+ // We only wrap the code through "go" utils because of the timeout and retry logic.
128
+ const goResult = await go(async () => {
129
+ let currentValue: unknown = apiCallResponse;
130
+
131
+ for (const processing of postProcessingSpecifications) {
132
+ // Provide endpoint parameters without reserved parameters immutably between steps. Recompute them for each
133
+ // snippet independently because processing snippets can modify the parameters.
134
+ const endpointParameters = removeReservedParameters(apiCallParameters);
135
+ switch (processing.environment) {
136
+ case 'Node': {
137
+ currentValue = await unsafeEvaluate(
138
+ processing.value,
139
+ { input: currentValue, endpointParameters },
140
+ processing.timeoutMs
141
+ );
142
+ break;
143
+ }
144
+ case 'Node async': {
145
+ currentValue = await unsafeEvaluateAsync(
146
+ processing.value,
147
+ { input: currentValue, endpointParameters },
148
+ processing.timeoutMs
149
+ );
150
+ break;
151
+ }
152
+ }
153
+ }
154
+
155
+ return currentValue;
156
+ }, processingOptions);
157
+ if (!goResult.success) throw goResult.error;
158
+
159
+ return goResult.data;
160
+ };
@@ -0,0 +1,10 @@
1
+ export const validateApiCallParameters = (apiCallParameters: unknown): ApiCallParameters => {
2
+ // eslint-disable-next-line lodash/prefer-lodash-typecheck
3
+ if (typeof apiCallParameters !== 'object' || apiCallParameters === null) {
4
+ throw new TypeError('Invalid API call parameters');
5
+ }
6
+
7
+ return apiCallParameters as ApiCallParameters;
8
+ };
9
+
10
+ export type ApiCallParameters = Record<string, any>;
@@ -0,0 +1,103 @@
1
+ /* eslint-disable jest/prefer-strict-equal */ // Because the errors are thrown from the "vm" module (different context), they are not strictly equal.
2
+ import { unsafeEvaluate, unsafeEvaluateAsync } from './unsafe-evaluate';
3
+
4
+ describe('unsafe evaluate - sync', () => {
5
+ it('executes harmless code', () => {
6
+ const result = unsafeEvaluate("const output = {...input, c: 'some-value'}", { input: { a: true, b: 123 } }, 5000);
7
+
8
+ expect(result).toEqual({ a: true, b: 123, c: 'some-value' });
9
+ });
10
+
11
+ it('throws on exception', () => {
12
+ expect(() => unsafeEvaluate("throw new Error('unexpected')", {}, 5000)).toThrow('unexpected');
13
+ });
14
+ });
15
+
16
+ describe('unsafe evaluate - async', () => {
17
+ it('executes harmless code', async () => {
18
+ const result = unsafeEvaluateAsync(
19
+ "const output = {...input, c: 'some-value'}; resolve(output);",
20
+ { input: { a: true, b: 123 } },
21
+ 5000
22
+ );
23
+
24
+ await expect(result).resolves.toEqual({ a: true, b: 123, c: 'some-value' });
25
+ });
26
+
27
+ it('can use setTimeout and setInterval', async () => {
28
+ const result = unsafeEvaluateAsync(
29
+ `
30
+ const fn = async () => {
31
+ const output = input;
32
+ output.push('start')
33
+
34
+ const tickMs = 35
35
+ const bufferMs = 25
36
+ setInterval(() => output.push('ping interval'), tickMs)
37
+ await new Promise((res) => setTimeout(res, tickMs * 4 + bufferMs));
38
+
39
+ output.push('end')
40
+ resolve(output);
41
+ };
42
+
43
+ fn()
44
+ `,
45
+ { input: [] },
46
+ 200
47
+ );
48
+
49
+ await expect(result).resolves.toEqual([
50
+ 'start',
51
+ 'ping interval',
52
+ 'ping interval',
53
+ 'ping interval',
54
+ 'ping interval',
55
+ 'end',
56
+ ]);
57
+ });
58
+
59
+ it('applies timeout when using setTimeout', async () => {
60
+ await expect(async () =>
61
+ unsafeEvaluateAsync(
62
+ `
63
+ const fn = () => {
64
+ setTimeout(() => console.log('ping timeout'), 100)
65
+ };
66
+
67
+ fn()
68
+ `,
69
+ {},
70
+ 50
71
+ )
72
+ ).rejects.toEqual(new Error('Timeout exceeded'));
73
+ });
74
+
75
+ it('applies timeout when using setInterval', async () => {
76
+ await expect(async () =>
77
+ unsafeEvaluateAsync(
78
+ `
79
+ const fn = () => {
80
+ const someFn = () => {}
81
+ setInterval(someFn, 10)
82
+ };
83
+
84
+ fn()
85
+ `,
86
+ {},
87
+ 50
88
+ )
89
+ ).rejects.toEqual(new Error('Timeout exceeded'));
90
+ });
91
+
92
+ it('processing can call reject', async () => {
93
+ await expect(async () =>
94
+ unsafeEvaluateAsync(`reject(new Error('Rejected by processing snippet.'))`, {}, 50)
95
+ ).rejects.toEqual(new Error('Rejected by processing snippet.'));
96
+ });
97
+
98
+ it('throws on exception', async () => {
99
+ await expect(async () => unsafeEvaluateAsync("throw new Error('unexpected')", {}, 5000)).rejects.toEqual(
100
+ new Error('unexpected')
101
+ );
102
+ });
103
+ });