zapier-platform-core 11.0.0 → 11.2.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/LICENSE +1 -2
- package/index.d.ts +12 -6
- package/package.json +10 -6
- package/src/constants.js +0 -12
- package/src/create-command-handler.js +2 -2
- package/src/errors.js +15 -0
- package/src/tools/cleaner.js +7 -5
- package/src/tools/create-app-tester.js +21 -27
- package/src/tools/create-file-stasher.js +244 -113
- package/src/tools/create-lambda-handler.js +2 -1
- package/src/tools/create-logger.js +38 -68
- package/src/tools/data.js +1 -18
- package/src/tools/fetch.js +27 -3
- package/src/tools/resolve-method-path.js +10 -23
- package/src/tools/schema-tools.js +5 -1
- package/src/tools/should-paginate.js +47 -0
package/LICENSE
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
1
|
Copyright (c) Zapier, Inc.
|
|
2
2
|
|
|
3
|
-
This repository is part of Zapier Platform, of
|
|
4
|
-
https://zapier.com/platform/tos.
|
|
3
|
+
This repository is part of Zapier Platform. By downloading, installing, accessing, or using any part of the Zapier Platform, including this repository, you agree to the Zapier Platform Agreement, which can be found at: https://zapier.com/platform/tos. If you do not agree to the Zapier Platform Agreement, you may not download, install, access, or use any part of the Zapier Platform, including this repository.
|
package/index.d.ts
CHANGED
|
@@ -37,6 +37,7 @@ export interface Bundle<InputData = { [x: string]: any }> {
|
|
|
37
37
|
inputData: InputData;
|
|
38
38
|
inputDataRaw: { [x: string]: string };
|
|
39
39
|
meta: {
|
|
40
|
+
isBulkRead: boolean;
|
|
40
41
|
isFillingDynamicDropdown: boolean;
|
|
41
42
|
isLoadingSample: boolean;
|
|
42
43
|
isPopulatingDedupe: boolean;
|
|
@@ -70,6 +71,9 @@ declare class AppError extends Error {
|
|
|
70
71
|
declare class HaltedError extends Error {}
|
|
71
72
|
declare class ExpiredAuthError extends Error {}
|
|
72
73
|
declare class RefreshAuthError extends Error {}
|
|
74
|
+
declare class ThrottledError extends Error {
|
|
75
|
+
constructor(message: string, delay?: number);
|
|
76
|
+
}
|
|
73
77
|
|
|
74
78
|
// copied http stuff from external typings
|
|
75
79
|
export interface HttpRequestOptions {
|
|
@@ -123,12 +127,13 @@ type DehydrateFunc = <T>(
|
|
|
123
127
|
export interface ZObject {
|
|
124
128
|
request: {
|
|
125
129
|
// most specific overloads go first
|
|
126
|
-
(
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
130
|
+
(
|
|
131
|
+
url: string,
|
|
132
|
+
options: HttpRequestOptions & { raw: true }
|
|
133
|
+
): Promise<RawHttpResponse>;
|
|
134
|
+
(
|
|
135
|
+
options: HttpRequestOptions & { raw: true; url: string }
|
|
136
|
+
): Promise<RawHttpResponse>;
|
|
132
137
|
|
|
133
138
|
(url: string, options?: HttpRequestOptions): Promise<HttpResponse>;
|
|
134
139
|
(options: HttpRequestOptions & { url: string }): Promise<HttpResponse>;
|
|
@@ -186,5 +191,6 @@ export interface ZObject {
|
|
|
186
191
|
HaltedError: typeof HaltedError;
|
|
187
192
|
ExpiredAuthError: typeof ExpiredAuthError;
|
|
188
193
|
RefreshAuthError: typeof RefreshAuthError;
|
|
194
|
+
ThrottledError: typeof ThrottledError;
|
|
189
195
|
};
|
|
190
196
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zapier-platform-core",
|
|
3
|
-
"version": "11.
|
|
3
|
+
"version": "11.2.0",
|
|
4
4
|
"description": "The core SDK for CLI apps in the Zapier Developer Platform.",
|
|
5
5
|
"repository": "zapier/zapier-platform",
|
|
6
6
|
"homepage": "https://platform.zapier.com/",
|
|
@@ -18,10 +18,11 @@
|
|
|
18
18
|
"preversion": "git pull && yarn test",
|
|
19
19
|
"version": "node bin/bump-dependencies.js && yarn && git add package.json yarn.lock",
|
|
20
20
|
"postversion": "git push && git push --tags",
|
|
21
|
-
"
|
|
21
|
+
"main-tests": "mocha -t 20000 --recursive test",
|
|
22
|
+
"solo-test": "test $(OPT_OUT_PATCH_TEST_ONLY=yes mocha --recursive test -g 'should be able to opt out of patch' -R json | jq '.stats.passes') -eq 1 && echo 'Ran 1 test and it passed!'",
|
|
23
|
+
"test": "yarn main-tests && yarn solo-test",
|
|
22
24
|
"debug": "mocha -t 10000 --inspect-brk --recursive test",
|
|
23
25
|
"test:w": "mocha -t 10000 --recursive test --watch",
|
|
24
|
-
"plain-test": "mocha -t 5000 --recursive test",
|
|
25
26
|
"integration-test": "mocha -t 20000 integration-test",
|
|
26
27
|
"local-integration-test": "mocha -t 10000 integration-test --local",
|
|
27
28
|
"lambda-integration-test": "mocha -t 10000 integration-test --lambda",
|
|
@@ -34,24 +35,27 @@
|
|
|
34
35
|
"validate": "yarn test && yarn smoke-test && yarn lint"
|
|
35
36
|
},
|
|
36
37
|
"engines": {
|
|
37
|
-
"node": ">=
|
|
38
|
+
"node": ">=12",
|
|
38
39
|
"npm": ">=5.6.0"
|
|
39
40
|
},
|
|
40
41
|
"engineStrict": true,
|
|
41
42
|
"dependencies": {
|
|
43
|
+
"@zapier/secret-scrubber": "^1.0.3",
|
|
42
44
|
"bluebird": "3.7.2",
|
|
43
45
|
"content-disposition": "0.5.3",
|
|
44
46
|
"dotenv": "9.0.2",
|
|
45
47
|
"form-data": "4.0.0",
|
|
46
48
|
"lodash": "4.17.21",
|
|
47
|
-
"
|
|
49
|
+
"mime-types": "2.1.34",
|
|
50
|
+
"node-fetch": "2.6.6",
|
|
48
51
|
"oauth-sign": "0.9.0",
|
|
49
52
|
"semver": "7.3.5",
|
|
50
|
-
"zapier-platform-schema": "11.
|
|
53
|
+
"zapier-platform-schema": "11.2.0"
|
|
51
54
|
},
|
|
52
55
|
"devDependencies": {
|
|
53
56
|
"adm-zip": "0.5.5",
|
|
54
57
|
"aws-sdk": "^2.905.0",
|
|
58
|
+
"dicer": "^0.3.0",
|
|
55
59
|
"fs-extra": "^10.0.0",
|
|
56
60
|
"mock-fs": "^4.14.0"
|
|
57
61
|
},
|
package/src/constants.js
CHANGED
|
@@ -23,17 +23,6 @@ const REQUEST_OBJECT_SHORTHAND_OPTIONS = { isShorthand: true, replace: true };
|
|
|
23
23
|
const DEFAULT_LOGGING_HTTP_ENDPOINT = 'https://httplogger.zapier.com/input';
|
|
24
24
|
const DEFAULT_LOGGING_HTTP_API_KEY = 'R24hzu86v3jntwtX2DtYECeWAB'; // It's ok, this isn't PROD
|
|
25
25
|
|
|
26
|
-
const SENSITIVE_KEYS = [
|
|
27
|
-
'api_key',
|
|
28
|
-
'apikey',
|
|
29
|
-
'auth',
|
|
30
|
-
'passwd',
|
|
31
|
-
'password',
|
|
32
|
-
'secret',
|
|
33
|
-
'signature',
|
|
34
|
-
'token',
|
|
35
|
-
];
|
|
36
|
-
|
|
37
26
|
const SAFE_LOG_KEYS = [
|
|
38
27
|
'account_id',
|
|
39
28
|
'api_title',
|
|
@@ -76,6 +65,5 @@ module.exports = {
|
|
|
76
65
|
REQUEST_OBJECT_SHORTHAND_OPTIONS,
|
|
77
66
|
RESPONSE_SIZE_LIMIT,
|
|
78
67
|
SAFE_LOG_KEYS,
|
|
79
|
-
SENSITIVE_KEYS,
|
|
80
68
|
STATUSES,
|
|
81
69
|
};
|
|
@@ -18,7 +18,7 @@ const commandHandlers = {
|
|
|
18
18
|
commands like 'execute', 'validate', 'definition', 'request'.
|
|
19
19
|
*/
|
|
20
20
|
const createCommandHandler = (compiledApp) => {
|
|
21
|
-
return (input) => {
|
|
21
|
+
return async (input) => {
|
|
22
22
|
const command = input._zapier.event.command || 'execute'; // validate || definition || request
|
|
23
23
|
const handler = commandHandlers[command];
|
|
24
24
|
if (!handler) {
|
|
@@ -26,7 +26,7 @@ const createCommandHandler = (compiledApp) => {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
try {
|
|
29
|
-
return handler(compiledApp, input);
|
|
29
|
+
return await handler(compiledApp, input);
|
|
30
30
|
} catch (err) {
|
|
31
31
|
return handleError(err);
|
|
32
32
|
}
|
package/src/errors.js
CHANGED
|
@@ -32,6 +32,7 @@ class ResponseError extends Error {
|
|
|
32
32
|
status: response.status,
|
|
33
33
|
headers: {
|
|
34
34
|
'content-type': response.headers.get('content-type'),
|
|
35
|
+
'retry-after': response.headers.get('retry-after'),
|
|
35
36
|
},
|
|
36
37
|
content,
|
|
37
38
|
request: {
|
|
@@ -44,6 +45,19 @@ class ResponseError extends Error {
|
|
|
44
45
|
}
|
|
45
46
|
}
|
|
46
47
|
|
|
48
|
+
class ThrottledError extends Error {
|
|
49
|
+
constructor(message, delay) {
|
|
50
|
+
super(
|
|
51
|
+
JSON.stringify({
|
|
52
|
+
message,
|
|
53
|
+
delay,
|
|
54
|
+
})
|
|
55
|
+
);
|
|
56
|
+
this.name = 'ThrottledError';
|
|
57
|
+
this.doNotContextify = true;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
47
61
|
// Make some of the errors we'll use!
|
|
48
62
|
const createError = (name) => {
|
|
49
63
|
const NewError = function (message = '') {
|
|
@@ -77,6 +91,7 @@ const exceptions = _.reduce(
|
|
|
77
91
|
{
|
|
78
92
|
Error: AppError,
|
|
79
93
|
ResponseError,
|
|
94
|
+
ThrottledError,
|
|
80
95
|
}
|
|
81
96
|
);
|
|
82
97
|
|
package/src/tools/cleaner.js
CHANGED
|
@@ -45,6 +45,12 @@ const recurseCleanFuncs = (obj, path) => {
|
|
|
45
45
|
|
|
46
46
|
// Recurse a nested object replace all instances of keys->vals in the bank.
|
|
47
47
|
const recurseReplaceBank = (obj, bank = {}) => {
|
|
48
|
+
const matchesCurlies = /({{.*?}})/;
|
|
49
|
+
const matchesKeyRegexMap = Object.keys(bank).reduce((acc, key) => {
|
|
50
|
+
// Escape characters (ex. {{foo}} => \\{\\{foo\\}\\} )
|
|
51
|
+
acc[key] = new RegExp(key.replace(/[-[\]/{}()\\*+?.^$|]/g, '\\$&'), 'g');
|
|
52
|
+
return acc;
|
|
53
|
+
}, {});
|
|
48
54
|
const replacer = (out) => {
|
|
49
55
|
if (!['string', 'number'].includes(typeof out)) {
|
|
50
56
|
return out;
|
|
@@ -57,15 +63,11 @@ const recurseReplaceBank = (obj, bank = {}) => {
|
|
|
57
63
|
let maybeChangedString = originalValueStr;
|
|
58
64
|
|
|
59
65
|
Object.keys(bank).forEach((key) => {
|
|
60
|
-
|
|
61
|
-
const escapedKey = key.replace(/[-[\]/{}()\\*+?.^$|]/g, '\\$&');
|
|
62
|
-
const matchesKey = new RegExp(escapedKey, 'g');
|
|
63
|
-
|
|
66
|
+
const matchesKey = matchesKeyRegexMap[key];
|
|
64
67
|
if (!matchesKey.test(maybeChangedString)) {
|
|
65
68
|
return;
|
|
66
69
|
}
|
|
67
70
|
|
|
68
|
-
const matchesCurlies = /({{.*?}})/;
|
|
69
71
|
const valueParts = maybeChangedString
|
|
70
72
|
.split(matchesCurlies)
|
|
71
73
|
.filter(Boolean);
|
|
@@ -3,32 +3,9 @@
|
|
|
3
3
|
const createLambdaHandler = require('./create-lambda-handler');
|
|
4
4
|
const resolveMethodPath = require('./resolve-method-path');
|
|
5
5
|
const ZapierPromise = require('./promise');
|
|
6
|
-
const {
|
|
6
|
+
const { isFunction } = require('lodash');
|
|
7
7
|
const { genId } = require('./data');
|
|
8
|
-
|
|
9
|
-
// this is (annoyingly) mirrored in cli/api_base, so that test functions only
|
|
10
|
-
// have a storeKey when canPaginate is true. otherwise, a test would work but a
|
|
11
|
-
// poll on site would fail. this is only used in test handlers
|
|
12
|
-
|
|
13
|
-
// there are 2 places you can put a method that can interact with cursors:
|
|
14
|
-
// triggers.contact.operation.perform, if it's a poll trigger
|
|
15
|
-
// resources.contact.list.operation.perform if it's a resource
|
|
16
|
-
// schema doesn't currently allow cursor use on hook trigger `performList`, so we don't need to account for it
|
|
17
|
-
const shouldPaginate = (appRaw, method) => {
|
|
18
|
-
const methodParts = method.split('.');
|
|
19
|
-
|
|
20
|
-
if (
|
|
21
|
-
method.endsWith('perform') &&
|
|
22
|
-
((methodParts[0] === 'resources' && methodParts[2] === 'list') ||
|
|
23
|
-
methodParts[0] === 'triggers' ||
|
|
24
|
-
methodParts[0] === 'bulkReads')
|
|
25
|
-
) {
|
|
26
|
-
methodParts.pop();
|
|
27
|
-
return get(appRaw, [...methodParts, 'canPaginate']);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
return false;
|
|
31
|
-
};
|
|
8
|
+
const { shouldPaginate } = require('./should-paginate');
|
|
32
9
|
|
|
33
10
|
// Convert a app handler to promise for convenience.
|
|
34
11
|
const promisifyHandler = (handler) => {
|
|
@@ -55,7 +32,21 @@ const createAppTester = (appRaw, { customStoreKey } = {}) => {
|
|
|
55
32
|
return (methodOrFunc, bundle) => {
|
|
56
33
|
bundle = bundle || {};
|
|
57
34
|
|
|
58
|
-
|
|
35
|
+
let method = resolveMethodPath(appRaw, methodOrFunc, false);
|
|
36
|
+
if (!method) {
|
|
37
|
+
if (isFunction(methodOrFunc)) {
|
|
38
|
+
// definitely have a function but didn't find it on the app; it's an adhoc
|
|
39
|
+
appRaw._testRequest = (z, bundle) => methodOrFunc(z, bundle);
|
|
40
|
+
method = resolveMethodPath(appRaw, appRaw._testRequest);
|
|
41
|
+
} else {
|
|
42
|
+
throw new Error(
|
|
43
|
+
`Unable to find the following on your App instance: ${JSON.stringify(
|
|
44
|
+
methodOrFunc
|
|
45
|
+
)}`
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
59
50
|
const storeKey = shouldPaginate(appRaw, method)
|
|
60
51
|
? customStoreKey
|
|
61
52
|
? `testKey-${customStoreKey}`
|
|
@@ -77,7 +68,10 @@ const createAppTester = (appRaw, { customStoreKey } = {}) => {
|
|
|
77
68
|
event.detailedLogToStdout = true;
|
|
78
69
|
}
|
|
79
70
|
|
|
80
|
-
return createHandlerPromise(event).then((resp) =>
|
|
71
|
+
return createHandlerPromise(event).then((resp) => {
|
|
72
|
+
delete appRaw._testRequest; // clear adHocFunc so tests can't affect each other
|
|
73
|
+
return resp.results;
|
|
74
|
+
});
|
|
81
75
|
};
|
|
82
76
|
};
|
|
83
77
|
|
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
4
5
|
const path = require('path');
|
|
5
|
-
const
|
|
6
|
+
const { pipeline } = require('stream');
|
|
7
|
+
const { promisify } = require('util');
|
|
8
|
+
const { randomBytes } = require('crypto');
|
|
9
|
+
|
|
10
|
+
const _ = require('lodash');
|
|
6
11
|
const contentDisposition = require('content-disposition');
|
|
12
|
+
const FormData = require('form-data');
|
|
13
|
+
const mime = require('mime-types');
|
|
7
14
|
|
|
8
15
|
const request = require('./request-client-internal');
|
|
9
|
-
const ZapierPromise = require('./promise');
|
|
10
|
-
|
|
11
|
-
const isPromise = (obj) => obj && typeof obj.then === 'function';
|
|
12
16
|
|
|
13
17
|
const UPLOAD_MAX_SIZE = 1000 * 1000 * 150; // 150mb, in zapier backend too
|
|
14
18
|
|
|
@@ -19,32 +23,186 @@ const LENGTH_ERR_MESSAGE =
|
|
|
19
23
|
const DEFAULT_FILE_NAME = 'unnamedfile';
|
|
20
24
|
const DEFAULT_CONTENT_TYPE = 'application/octet-stream';
|
|
21
25
|
|
|
22
|
-
const
|
|
26
|
+
const streamPipeline = promisify(pipeline);
|
|
27
|
+
|
|
28
|
+
const filenameFromURL = (url) => {
|
|
29
|
+
try {
|
|
30
|
+
return decodeURIComponent(path.posix.basename(new URL(url).pathname));
|
|
31
|
+
} catch (error) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const filenameFromHeader = (response) => {
|
|
37
|
+
const cd = response.headers.get('content-disposition');
|
|
38
|
+
let filename;
|
|
39
|
+
if (cd) {
|
|
40
|
+
try {
|
|
41
|
+
filename = contentDisposition.parse(cd).parameters.filename;
|
|
42
|
+
} catch (error) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return filename || null;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const resolveRemoteStream = async (stream) => {
|
|
50
|
+
// Download to a temp file, get the file size, and create a readable stream
|
|
51
|
+
// from the temp file.
|
|
52
|
+
//
|
|
53
|
+
// The streamPipeline usage is taken from
|
|
54
|
+
// https://github.com/node-fetch/node-fetch#streams
|
|
55
|
+
const tmpFilePath = path.join(
|
|
56
|
+
os.tmpdir(),
|
|
57
|
+
'stash-' + randomBytes(16).toString('hex')
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
await streamPipeline(stream, fs.createWriteStream(tmpFilePath));
|
|
62
|
+
} catch (error) {
|
|
63
|
+
try {
|
|
64
|
+
fs.unlinkSync(tmpFilePath);
|
|
65
|
+
} catch (e) {
|
|
66
|
+
// File doesn't exist? Probably okay
|
|
67
|
+
}
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const length = fs.statSync(tmpFilePath).size;
|
|
72
|
+
const readStream = fs.createReadStream(tmpFilePath);
|
|
73
|
+
|
|
74
|
+
readStream.on('end', () => {
|
|
75
|
+
// Burn after reading
|
|
76
|
+
try {
|
|
77
|
+
fs.unlinkSync(tmpFilePath);
|
|
78
|
+
} catch (e) {
|
|
79
|
+
// TODO: We probably want to log warning here
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
streamOrData: readStream,
|
|
85
|
+
length,
|
|
86
|
+
};
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const resolveResponseToStream = async (response) => {
|
|
90
|
+
// Get filename from content-disposition header or URL
|
|
91
|
+
let filename =
|
|
92
|
+
filenameFromHeader(response) ||
|
|
93
|
+
filenameFromURL(response.url || _.get(response, ['request', 'url'])) ||
|
|
94
|
+
DEFAULT_FILE_NAME;
|
|
95
|
+
|
|
96
|
+
const contentType = response.headers.get('content-type');
|
|
97
|
+
if (contentType && !path.extname(filename)) {
|
|
98
|
+
const ext = mime.extension(contentType);
|
|
99
|
+
if (ext && ext !== 'bin') {
|
|
100
|
+
filename += '.' + ext;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (response.body && typeof response.body.pipe === 'function') {
|
|
105
|
+
// streamable response created by z.request({ raw: true })
|
|
106
|
+
return {
|
|
107
|
+
...(await resolveRemoteStream(response.body)),
|
|
108
|
+
contentType: contentType || DEFAULT_CONTENT_TYPE,
|
|
109
|
+
filename,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// regular response created by z.request({ raw: false })
|
|
114
|
+
return {
|
|
115
|
+
streamOrData: response.content,
|
|
116
|
+
length: Buffer.byteLength(response.content),
|
|
117
|
+
contentType: contentType || DEFAULT_CONTENT_TYPE,
|
|
118
|
+
filename,
|
|
119
|
+
};
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const resolveStreamWithMeta = async (stream) => {
|
|
123
|
+
const isLocalFile = stream.path && fs.existsSync(stream.path);
|
|
124
|
+
if (isLocalFile) {
|
|
125
|
+
const filename = path.basename(stream.path);
|
|
126
|
+
return {
|
|
127
|
+
streamOrData: stream,
|
|
128
|
+
length: fs.statSync(stream.path).size,
|
|
129
|
+
contentType: mime.lookup(filename) || DEFAULT_CONTENT_TYPE,
|
|
130
|
+
filename,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
...(await resolveRemoteStream(stream)),
|
|
136
|
+
contentType: DEFAULT_CONTENT_TYPE,
|
|
137
|
+
filename: DEFAULT_FILE_NAME,
|
|
138
|
+
};
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
// Returns an object with fields:
|
|
142
|
+
// * streamOrData: a readable stream, a string, or a Buffer
|
|
143
|
+
// * length: content length in bytes
|
|
144
|
+
// * contentType
|
|
145
|
+
// * filename
|
|
146
|
+
const resolveToBufferStringStream = async (responseOrData) => {
|
|
147
|
+
if (typeof responseOrData === 'string' || responseOrData instanceof String) {
|
|
148
|
+
// The .toString() call only makes a difference for the String object case.
|
|
149
|
+
// It converts a String object to a regular string.
|
|
150
|
+
const str = responseOrData.toString();
|
|
151
|
+
return {
|
|
152
|
+
streamOrData: str,
|
|
153
|
+
length: Buffer.byteLength(str),
|
|
154
|
+
contentType: 'text/plain',
|
|
155
|
+
filename: `${DEFAULT_FILE_NAME}.txt`,
|
|
156
|
+
};
|
|
157
|
+
} else if (Buffer.isBuffer(responseOrData)) {
|
|
158
|
+
return {
|
|
159
|
+
streamOrData: responseOrData,
|
|
160
|
+
length: responseOrData.length,
|
|
161
|
+
contentType: DEFAULT_CONTENT_TYPE,
|
|
162
|
+
filename: DEFAULT_FILE_NAME,
|
|
163
|
+
};
|
|
164
|
+
} else if (
|
|
165
|
+
(responseOrData.body && typeof responseOrData.body.pipe === 'function') ||
|
|
166
|
+
typeof responseOrData.content === 'string'
|
|
167
|
+
) {
|
|
168
|
+
return resolveResponseToStream(responseOrData);
|
|
169
|
+
} else if (typeof responseOrData.pipe === 'function') {
|
|
170
|
+
return resolveStreamWithMeta(responseOrData);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
throw new TypeError(
|
|
174
|
+
`z.stashFile() cannot stash type '${typeof responseOrData}'. ` +
|
|
175
|
+
'Pass it a request, readable stream, string, or Buffer.'
|
|
176
|
+
);
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const uploader = async (
|
|
23
180
|
signedPostData,
|
|
24
181
|
bufferStringStream,
|
|
25
182
|
knownLength,
|
|
26
183
|
filename,
|
|
27
184
|
contentType
|
|
28
185
|
) => {
|
|
29
|
-
const form = new FormData();
|
|
30
|
-
|
|
31
186
|
if (knownLength && knownLength > UPLOAD_MAX_SIZE) {
|
|
32
|
-
|
|
33
|
-
new Error(`${knownLength} is too big, ${UPLOAD_MAX_SIZE} is the max`)
|
|
34
|
-
);
|
|
187
|
+
throw new Error(`${knownLength} is too big, ${UPLOAD_MAX_SIZE} is the max`);
|
|
35
188
|
}
|
|
189
|
+
filename = path.basename(filename).replace('"', '');
|
|
36
190
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
191
|
+
const fields = {
|
|
192
|
+
...signedPostData.fields,
|
|
193
|
+
'Content-Disposition': contentDisposition(filename),
|
|
194
|
+
'Content-Type': contentType,
|
|
195
|
+
};
|
|
40
196
|
|
|
41
|
-
|
|
197
|
+
const form = new FormData();
|
|
42
198
|
|
|
43
|
-
|
|
199
|
+
Object.entries(fields).forEach(([key, value]) => {
|
|
200
|
+
form.append(key, value);
|
|
201
|
+
});
|
|
44
202
|
|
|
45
203
|
form.append('file', bufferStringStream, {
|
|
46
|
-
contentType,
|
|
47
204
|
knownLength,
|
|
205
|
+
contentType,
|
|
48
206
|
filename,
|
|
49
207
|
});
|
|
50
208
|
|
|
@@ -56,120 +214,93 @@ const uploader = (
|
|
|
56
214
|
}
|
|
57
215
|
|
|
58
216
|
// Send to S3 with presigned request.
|
|
59
|
-
|
|
217
|
+
const response = await request({
|
|
60
218
|
url: signedPostData.url,
|
|
61
219
|
method: 'POST',
|
|
62
220
|
body: form,
|
|
63
|
-
}).then((res) => {
|
|
64
|
-
if (res.status === 204) {
|
|
65
|
-
return `${signedPostData.url}${signedPostData.fields.key}`;
|
|
66
|
-
}
|
|
67
|
-
if (
|
|
68
|
-
res.content.indexOf(
|
|
69
|
-
'You must provide the Content-Length HTTP header.'
|
|
70
|
-
) !== -1
|
|
71
|
-
) {
|
|
72
|
-
throw new Error(LENGTH_ERR_MESSAGE);
|
|
73
|
-
}
|
|
74
|
-
throw new Error(`Got ${res.status} - ${res.content}`);
|
|
75
221
|
});
|
|
222
|
+
|
|
223
|
+
if (response.status === 204) {
|
|
224
|
+
return new URL(signedPostData.fields.key, signedPostData.url).href;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (
|
|
228
|
+
response.content &&
|
|
229
|
+
response.content.includes &&
|
|
230
|
+
response.content.includes(
|
|
231
|
+
'You must provide the Content-Length HTTP header.'
|
|
232
|
+
)
|
|
233
|
+
) {
|
|
234
|
+
throw new Error(LENGTH_ERR_MESSAGE);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
throw new Error(`Got ${response.status} - ${response.content}`);
|
|
76
238
|
};
|
|
77
239
|
|
|
78
240
|
// Designed to be some user provided function/api.
|
|
79
241
|
const createFileStasher = (input) => {
|
|
80
242
|
const rpc = _.get(input, '_zapier.rpc');
|
|
81
243
|
|
|
82
|
-
return (
|
|
244
|
+
return async (requestOrData, knownLength, filename, contentType) => {
|
|
83
245
|
// TODO: maybe this could be smart?
|
|
84
246
|
// if it is already a public url, do we pass through? or upload?
|
|
85
247
|
if (!rpc) {
|
|
86
|
-
|
|
248
|
+
throw new Error('rpc is not available');
|
|
87
249
|
}
|
|
88
250
|
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
251
|
+
const isRunningOnHydrator = _.get(
|
|
252
|
+
input,
|
|
253
|
+
'_zapier.event.method',
|
|
254
|
+
''
|
|
255
|
+
).startsWith('hydrators.');
|
|
256
|
+
const isRunningOnCreate = _.get(
|
|
257
|
+
input,
|
|
258
|
+
'_zapier.event.method',
|
|
259
|
+
''
|
|
260
|
+
).startsWith('creates.');
|
|
261
|
+
|
|
262
|
+
if (!isRunningOnHydrator && !isRunningOnCreate) {
|
|
263
|
+
throw new Error(
|
|
264
|
+
'Files can only be stashed within a create or hydration function/method.'
|
|
101
265
|
);
|
|
102
266
|
}
|
|
103
267
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
return uploader(
|
|
141
|
-
result,
|
|
142
|
-
newBufferStringStream,
|
|
143
|
-
knownLength,
|
|
144
|
-
filename,
|
|
145
|
-
fileContentType
|
|
146
|
-
);
|
|
147
|
-
};
|
|
148
|
-
|
|
149
|
-
const formResponse = (response) => {
|
|
150
|
-
// determine if string is streamed based on if raw:true
|
|
151
|
-
const isStreamed = _.get(response, 'request.raw', false);
|
|
152
|
-
|
|
153
|
-
// if it's streamed, buffer first
|
|
154
|
-
if (isStreamed) {
|
|
155
|
-
return response.buffer().then((buffer) => {
|
|
156
|
-
response.dataBuffer = buffer;
|
|
157
|
-
return parseFinalResponse(response);
|
|
158
|
-
});
|
|
159
|
-
} else {
|
|
160
|
-
return parseFinalResponse(response);
|
|
161
|
-
}
|
|
162
|
-
};
|
|
163
|
-
|
|
164
|
-
// if stream is a promise, wait until resolved
|
|
165
|
-
if (isPromise(bufferStringStream)) {
|
|
166
|
-
return bufferStringStream.then((maybeResponse) => {
|
|
167
|
-
return formResponse(maybeResponse);
|
|
168
|
-
});
|
|
169
|
-
} else {
|
|
170
|
-
return formResponse(bufferStringStream);
|
|
171
|
-
}
|
|
172
|
-
}
|
|
268
|
+
// requestOrData can be one of these:
|
|
269
|
+
// * string
|
|
270
|
+
// * Buffer
|
|
271
|
+
// * z.request() - a Promise of a regular response
|
|
272
|
+
// * z.request({ raw: true }) - a Promise of a "streamable" response
|
|
273
|
+
// * await z.request() - a regular response
|
|
274
|
+
// * await z.request({ raw: true }) - a streamable response
|
|
275
|
+
//
|
|
276
|
+
// After the following, requestOrData is resolved to responseOrData, which
|
|
277
|
+
// is either:
|
|
278
|
+
// - string
|
|
279
|
+
// - Buffer
|
|
280
|
+
// - a regular response
|
|
281
|
+
// - a streamable response
|
|
282
|
+
const [signedPostData, responseOrData] = await Promise.all([
|
|
283
|
+
rpc('get_presigned_upload_post_data'),
|
|
284
|
+
requestOrData,
|
|
285
|
+
]);
|
|
286
|
+
|
|
287
|
+
if (responseOrData.throwForStatus) {
|
|
288
|
+
responseOrData.throwForStatus();
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const {
|
|
292
|
+
streamOrData,
|
|
293
|
+
length,
|
|
294
|
+
contentType: _contentType,
|
|
295
|
+
filename: _filename,
|
|
296
|
+
} = await resolveToBufferStringStream(responseOrData);
|
|
297
|
+
|
|
298
|
+
return uploader(
|
|
299
|
+
signedPostData,
|
|
300
|
+
streamOrData,
|
|
301
|
+
knownLength || length,
|
|
302
|
+
filename || _filename,
|
|
303
|
+
contentType || _contentType
|
|
173
304
|
);
|
|
174
305
|
};
|
|
175
306
|
};
|
|
@@ -205,7 +205,8 @@ const createLambdaHandler = (appRawOrPath) => {
|
|
|
205
205
|
// Adds logging for _all_ kinds of http(s) requests, no matter the library
|
|
206
206
|
if (!skipHttpPatch) {
|
|
207
207
|
const httpPatch = createHttpPatch(event);
|
|
208
|
-
httpPatch(require('http'));
|
|
208
|
+
httpPatch(require('http'));
|
|
209
|
+
httpPatch(require('https')); // 'https' needs to be patched separately
|
|
209
210
|
}
|
|
210
211
|
|
|
211
212
|
// TODO: Avoid calling prepareApp(appRaw) repeatedly here as createApp()
|
|
@@ -3,19 +3,33 @@
|
|
|
3
3
|
const _ = require('lodash');
|
|
4
4
|
|
|
5
5
|
const request = require('./request-client-internal');
|
|
6
|
-
const
|
|
7
|
-
const dataTools = require('./data');
|
|
8
|
-
const hashing = require('./hashing');
|
|
6
|
+
const { simpleTruncate, recurseReplace } = require('./data');
|
|
9
7
|
const ZapierPromise = require('./promise');
|
|
10
8
|
const {
|
|
11
9
|
DEFAULT_LOGGING_HTTP_API_KEY,
|
|
12
10
|
DEFAULT_LOGGING_HTTP_ENDPOINT,
|
|
13
11
|
SAFE_LOG_KEYS,
|
|
14
|
-
SENSITIVE_KEYS,
|
|
15
12
|
} = require('../constants');
|
|
16
13
|
const { unheader } = require('./http');
|
|
14
|
+
const {
|
|
15
|
+
scrub,
|
|
16
|
+
findSensitiveValues,
|
|
17
|
+
recurseExtract,
|
|
18
|
+
} = require('@zapier/secret-scrubber');
|
|
19
|
+
// not really a public function, but it came from here originally
|
|
20
|
+
const { isUrlWithSecrets } = require('@zapier/secret-scrubber/lib/convenience');
|
|
21
|
+
|
|
22
|
+
const isUrl = (url) => {
|
|
23
|
+
try {
|
|
24
|
+
// eslint-disable-next-line no-new
|
|
25
|
+
new URL(url);
|
|
26
|
+
return true;
|
|
27
|
+
} catch (e) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
17
31
|
|
|
18
|
-
const truncate = (str) =>
|
|
32
|
+
const truncate = (str) => simpleTruncate(str, 3500, ' [...]');
|
|
19
33
|
|
|
20
34
|
const formatHeaders = (headers = {}) => {
|
|
21
35
|
if (_.isEmpty(headers)) {
|
|
@@ -89,66 +103,22 @@ const toStdout = (event, msg, data) => {
|
|
|
89
103
|
}
|
|
90
104
|
};
|
|
91
105
|
|
|
92
|
-
const
|
|
93
|
-
let url;
|
|
94
|
-
try {
|
|
95
|
-
url = new URL(value);
|
|
96
|
-
} catch (err) {
|
|
97
|
-
return false;
|
|
98
|
-
}
|
|
99
|
-
return !(url.username || url.password || url.search);
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
const makeSensitiveBank = (event, data) => {
|
|
106
|
+
const buildSensitiveValues = (event, data) => {
|
|
103
107
|
const bundle = event.bundle || {};
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
}
|
|
111
|
-
// Allow uncensored if the value is a safe URL and the key is not in
|
|
112
|
-
// SENSITIVE_KEYS
|
|
113
|
-
return [...values];
|
|
114
|
-
},
|
|
115
|
-
[]
|
|
116
|
-
);
|
|
117
|
-
|
|
118
|
-
const matcher = (key, value) => {
|
|
119
|
-
if (_.isString(value)) {
|
|
120
|
-
const lowerKey = key.toLowerCase();
|
|
121
|
-
return _.some(SENSITIVE_KEYS, (k) => lowerKey.indexOf(k) >= 0);
|
|
108
|
+
const authData = bundle.authData || {};
|
|
109
|
+
// for the most part, we should censor all the values from authData
|
|
110
|
+
// the exception is safe urls, which should be filtered out - we want those to be logged
|
|
111
|
+
const sensitiveAuthData = recurseExtract(authData, (key, value) => {
|
|
112
|
+
if (isUrl(value) && !isUrlWithSecrets(value)) {
|
|
113
|
+
return false;
|
|
122
114
|
}
|
|
123
|
-
return
|
|
124
|
-
};
|
|
125
|
-
|
|
126
|
-
dataTools.recurseExtract(data, matcher).forEach((value) => {
|
|
127
|
-
sensitiveValues.push(value);
|
|
115
|
+
return true;
|
|
128
116
|
});
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
(
|
|
133
|
-
|
|
134
|
-
// see https://github.com/zapier/zapier-platform-core/issues/4#issuecomment-277855071
|
|
135
|
-
if (val && String(val).length > 5) {
|
|
136
|
-
const censored = hashing.snipify(val);
|
|
137
|
-
bank[val] = censored;
|
|
138
|
-
bank[encodeURIComponent(val)] = censored;
|
|
139
|
-
try {
|
|
140
|
-
bank[Buffer.from(String(val)).toString('base64')] = censored;
|
|
141
|
-
} catch (e) {
|
|
142
|
-
if (e.name !== 'TypeError') {
|
|
143
|
-
throw e;
|
|
144
|
-
}
|
|
145
|
-
// ignore; Buffer is semi-selective about what types it takes
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
return bank;
|
|
149
|
-
},
|
|
150
|
-
{}
|
|
151
|
-
);
|
|
117
|
+
return [
|
|
118
|
+
...sensitiveAuthData,
|
|
119
|
+
...findSensitiveValues(process.env),
|
|
120
|
+
...findSensitiveValues(data),
|
|
121
|
+
];
|
|
152
122
|
};
|
|
153
123
|
|
|
154
124
|
const sendLog = (options, event, message, data) => {
|
|
@@ -158,16 +128,16 @@ const sendLog = (options, event, message, data) => {
|
|
|
158
128
|
data.request_headers = unheader(data.request_headers);
|
|
159
129
|
data.response_headers = unheader(data.response_headers);
|
|
160
130
|
|
|
161
|
-
const
|
|
131
|
+
const sensitiveValues = buildSensitiveValues(event, data);
|
|
132
|
+
// scrub throws an error if there are no secrets
|
|
162
133
|
const safeMessage = truncate(
|
|
163
|
-
|
|
134
|
+
sensitiveValues.length ? scrub(message, sensitiveValues) : message
|
|
164
135
|
);
|
|
165
|
-
const safeData =
|
|
166
|
-
|
|
136
|
+
const safeData = recurseReplace(
|
|
137
|
+
sensitiveValues.length ? scrub(data, sensitiveValues) : data,
|
|
167
138
|
truncate
|
|
168
139
|
);
|
|
169
|
-
const unsafeData =
|
|
170
|
-
|
|
140
|
+
const unsafeData = recurseReplace(data, truncate);
|
|
171
141
|
// Keep safe log keys uncensored
|
|
172
142
|
Object.keys(safeData).forEach((key) => {
|
|
173
143
|
if (SAFE_LOG_KEYS.includes(key)) {
|
package/src/tools/data.js
CHANGED
|
@@ -123,22 +123,6 @@ const recurseReplace = (obj, replacer, options = {}) => {
|
|
|
123
123
|
return obj;
|
|
124
124
|
};
|
|
125
125
|
|
|
126
|
-
// Recursively extract values from a nested object based on the matcher function.
|
|
127
|
-
const recurseExtract = (obj, matcher) => {
|
|
128
|
-
const values = [];
|
|
129
|
-
Object.keys(obj).forEach((key) => {
|
|
130
|
-
const value = obj[key];
|
|
131
|
-
if (matcher(key, value)) {
|
|
132
|
-
values.push(value);
|
|
133
|
-
} else if (isPlainObj(value)) {
|
|
134
|
-
recurseExtract(value, matcher).forEach((v) => {
|
|
135
|
-
values.push(v);
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
|
-
});
|
|
139
|
-
return values;
|
|
140
|
-
};
|
|
141
|
-
|
|
142
126
|
const _IGNORE = {};
|
|
143
127
|
|
|
144
128
|
// Flatten a nested object.
|
|
@@ -169,7 +153,7 @@ const flattenPaths = (data, { preserve = {} } = {}) => {
|
|
|
169
153
|
|
|
170
154
|
// A simpler, and memory-friendlier version of _.truncate()
|
|
171
155
|
const simpleTruncate = (string, length, suffix) => {
|
|
172
|
-
if (string
|
|
156
|
+
if (string == null) {
|
|
173
157
|
return string;
|
|
174
158
|
}
|
|
175
159
|
if (!string || !string.toString) {
|
|
@@ -200,7 +184,6 @@ module.exports = {
|
|
|
200
184
|
isPlainObj,
|
|
201
185
|
jsonCopy,
|
|
202
186
|
memoizedFindMapDeep,
|
|
203
|
-
recurseExtract,
|
|
204
187
|
recurseReplace,
|
|
205
188
|
simpleTruncate,
|
|
206
189
|
};
|
package/src/tools/fetch.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const { Writable } = require('stream');
|
|
4
|
+
|
|
3
5
|
const fetch = require('node-fetch');
|
|
4
6
|
|
|
5
7
|
// XXX: PatchedRequest is to get past node-fetch's check that forbids GET requests
|
|
@@ -7,10 +9,10 @@ const fetch = require('node-fetch');
|
|
|
7
9
|
// https://github.com/node-fetch/node-fetch/blob/v2.6.0/src/request.js#L75-L78
|
|
8
10
|
class PatchedRequest extends fetch.Request {
|
|
9
11
|
constructor(url, opts) {
|
|
10
|
-
const origMethod = (opts.method || 'GET').toUpperCase();
|
|
12
|
+
const origMethod = ((opts && opts.method) || 'GET').toUpperCase();
|
|
11
13
|
|
|
12
14
|
const isGetWithBody =
|
|
13
|
-
(origMethod === 'GET' || origMethod === 'HEAD') && opts.body;
|
|
15
|
+
(origMethod === 'GET' || origMethod === 'HEAD') && opts && opts.body;
|
|
14
16
|
let newOpts = opts;
|
|
15
17
|
if (isGetWithBody) {
|
|
16
18
|
// Temporary remove body to fool fetch.Request constructor
|
|
@@ -50,9 +52,31 @@ class PatchedRequest extends fetch.Request {
|
|
|
50
52
|
|
|
51
53
|
const newFetch = (url, opts) => {
|
|
52
54
|
const request = new PatchedRequest(url, opts);
|
|
55
|
+
|
|
53
56
|
// fetch actually accepts a Request object as an argument. It'll clone the
|
|
54
57
|
// request internally, that's why the PatchedRequest.body hack works.
|
|
55
|
-
|
|
58
|
+
const responsePromise = fetch(request);
|
|
59
|
+
|
|
60
|
+
// node-fetch clones request.body and use the cloned body internally. We need
|
|
61
|
+
// to make sure to consume the original body stream so its internal buffer is
|
|
62
|
+
// not filled up, which causes it to pause.
|
|
63
|
+
// See https://github.com/node-fetch/node-fetch/issues/151
|
|
64
|
+
//
|
|
65
|
+
// Exclude form-data object to be consistent with
|
|
66
|
+
// https://github.com/node-fetch/node-fetch/blob/v2.6.6/src/body.js#L403-L412
|
|
67
|
+
if (
|
|
68
|
+
request.body &&
|
|
69
|
+
typeof request.body.pipe === 'function' &&
|
|
70
|
+
typeof request.body.getBoundary !== 'function'
|
|
71
|
+
) {
|
|
72
|
+
const nullStream = new Writable();
|
|
73
|
+
nullStream._write = function (chunk, encoding, done) {
|
|
74
|
+
done();
|
|
75
|
+
};
|
|
76
|
+
request.body.pipe(nullStream);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return responsePromise;
|
|
56
80
|
};
|
|
57
81
|
|
|
58
82
|
newFetch.Promise = require('./promise');
|
|
@@ -1,45 +1,32 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const
|
|
4
|
-
const
|
|
3
|
+
const { isEqual } = require('lodash');
|
|
4
|
+
const { memoizedFindMapDeep } = require('./data');
|
|
5
5
|
|
|
6
6
|
const isRequestMethod = (needle) =>
|
|
7
7
|
typeof needle === 'object' && typeof needle.url === 'string';
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
Verifies a object in an app tree, returning the path to it if found (
|
|
11
|
-
|
|
12
|
-
// a findable function/array/method
|
|
13
|
-
resolveMethodPath(app, app.resources.contact.get.operation.perform)
|
|
14
|
-
|
|
9
|
+
/**
|
|
10
|
+
Verifies a object exists in an app tree, returning the path to it if found (optionally throwing an error if not found)
|
|
15
11
|
*/
|
|
16
|
-
const resolveMethodPath = (app, needle) => {
|
|
17
|
-
// temporary warning for all those with old code
|
|
18
|
-
if (typeof needle === 'string') {
|
|
19
|
-
console.log(
|
|
20
|
-
'In version 0.9.10 we removed string path resolution. Read more here https://github.com/zapier/zapier-platform-core/blob/master/CHANGELOG.md#0910'
|
|
21
|
-
);
|
|
22
|
-
}
|
|
23
|
-
|
|
12
|
+
const resolveMethodPath = (app, needle, explodeIfMissing = true) => {
|
|
24
13
|
if (
|
|
25
14
|
!(
|
|
26
15
|
typeof needle === 'function' ||
|
|
27
|
-
|
|
16
|
+
Array.isArray(needle) ||
|
|
28
17
|
isRequestMethod(needle)
|
|
29
18
|
)
|
|
30
19
|
) {
|
|
31
20
|
throw new Error(
|
|
32
|
-
|
|
33
|
-
typeof needle +
|
|
34
|
-
' instead.'
|
|
21
|
+
`You must pass in a function/array/object. We got ${typeof needle} instead.`
|
|
35
22
|
);
|
|
36
23
|
}
|
|
37
24
|
|
|
38
25
|
// incurs roughly ~10ms penalty for _.isEqual fallback on a === miss on an averagish app
|
|
39
26
|
const path =
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
if (!path) {
|
|
27
|
+
memoizedFindMapDeep(app, needle) ||
|
|
28
|
+
memoizedFindMapDeep(app, needle, isEqual);
|
|
29
|
+
if (!path && explodeIfMissing) {
|
|
43
30
|
throw new Error(
|
|
44
31
|
'We could not find your function/array/object anywhere on your App definition.'
|
|
45
32
|
);
|
|
@@ -2,9 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
const dataTools = require('./data');
|
|
4
4
|
|
|
5
|
+
// AsyncFunction is not a global object and can be obtained in this way
|
|
6
|
+
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncFunction
|
|
7
|
+
const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor;
|
|
8
|
+
|
|
5
9
|
const makeFunction = (source, args = []) => {
|
|
6
10
|
try {
|
|
7
|
-
return
|
|
11
|
+
return new AsyncFunction(...args, source);
|
|
8
12
|
} catch (err) {
|
|
9
13
|
return () => {
|
|
10
14
|
throw err;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
const { get } = require('lodash');
|
|
2
|
+
|
|
3
|
+
// this is (annoyingly) mirrored in cli/api_base, so that test functions only
|
|
4
|
+
// have a storeKey when canPaginate is true. otherwise, a test would work but a
|
|
5
|
+
// poll on site would fail. this is only used in test handlers
|
|
6
|
+
|
|
7
|
+
// there are 4 places you can put a method that can interact with cursors:
|
|
8
|
+
// triggers.contact.operation.perform, if it's a poll trigger
|
|
9
|
+
// triggers.contact.operation.performList, if it's a hook trigger
|
|
10
|
+
// resources.contact.list.operation.perform if it's a resource
|
|
11
|
+
// resources.contact.hook.operation.performList if it's a resource
|
|
12
|
+
|
|
13
|
+
const shouldPaginate = (appRaw, method) => {
|
|
14
|
+
const methodParts = method.split('.');
|
|
15
|
+
const methodName = methodParts.pop();
|
|
16
|
+
const operation = get(appRaw, methodParts);
|
|
17
|
+
|
|
18
|
+
if (methodParts[0] === 'triggers') {
|
|
19
|
+
// Polling operations may not specify type
|
|
20
|
+
if (
|
|
21
|
+
['polling', undefined].includes(operation.type) &&
|
|
22
|
+
methodName === 'perform'
|
|
23
|
+
) {
|
|
24
|
+
return !!operation.canPaginate;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (operation.type === 'hook' && methodName === 'performList') {
|
|
28
|
+
return !!operation.canPaginate;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (methodParts[0] === 'resources') {
|
|
33
|
+
if (methodParts[2] === 'list' && methodName === 'perform') {
|
|
34
|
+
return !!operation.canPaginate;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (methodParts[2] === 'hook' && methodName === 'performList') {
|
|
38
|
+
return !!operation.canPaginate;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return false;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
module.exports = {
|
|
46
|
+
shouldPaginate,
|
|
47
|
+
};
|