zapier-platform-core 15.9.1 → 15.11.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/package.json CHANGED
@@ -1,26 +1,27 @@
1
1
  {
2
2
  "name": "zapier-platform-core",
3
- "version": "15.9.1",
3
+ "version": "15.11.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/",
7
7
  "author": "Zapier Engineering <contact@zapier.com>",
8
8
  "license": "SEE LICENSE IN LICENSE",
9
9
  "main": "index.js",
10
- "typings": "index.d.ts",
10
+ "types": "types/index.d.ts",
11
11
  "files": [
12
12
  "/include/",
13
- "/index.d.ts",
14
13
  "/index.js",
15
- "/src/"
14
+ "/src/",
15
+ "/types/"
16
16
  ],
17
17
  "scripts": {
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 20s --recursive test --exit",
22
+ "type-tests": "tsd",
22
23
  "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",
24
+ "test": "yarn main-tests && yarn solo-test && yarn type-tests",
24
25
  "test:debug": "mocha inspect -t 10s --recursive test",
25
26
  "debug": "mocha -t 10s --inspect-brk --recursive test",
26
27
  "test:w": "mocha -t 10s --recursive test --watch",
@@ -52,7 +53,7 @@
52
53
  "node-fetch": "2.6.7",
53
54
  "oauth-sign": "0.9.0",
54
55
  "semver": "7.5.2",
55
- "zapier-platform-schema": "15.9.1"
56
+ "zapier-platform-schema": "15.11.0"
56
57
  },
57
58
  "devDependencies": {
58
59
  "@types/node-fetch": "^2.6.11",
@@ -61,7 +62,8 @@
61
62
  "dicer": "^0.3.1",
62
63
  "fs-extra": "^11.1.1",
63
64
  "mock-fs": "^5.2.0",
64
- "nock": "^13.5.4"
65
+ "nock": "^13.5.4",
66
+ "tsd": "^0.31.1"
65
67
  },
66
68
  "optionalDependencies": {
67
69
  "@types/node": "^20.3.1"
@@ -1,5 +1,8 @@
1
1
  const zlib = require('zlib');
2
2
  const _ = require('lodash');
3
+
4
+ const { ALLOWED_HTTP_DATA_CONTENT_TYPES, getContentType } = require('./http');
5
+
3
6
  const constants = require('../constants');
4
7
 
5
8
  const createHttpPatch = (event) => {
@@ -54,19 +57,34 @@ const createHttpPatch = (event) => {
54
57
  const newCallback = function (response) {
55
58
  const chunks = [];
56
59
 
60
+ // Only include request or response data for specific content types
61
+ // which we are able to read in logs and which are not typically too large
62
+ const requestContentType = getContentType(options.headers || {});
63
+ const responseContentType = getContentType(response.headers || {});
64
+
65
+ const shouldIncludeRequestData =
66
+ ALLOWED_HTTP_DATA_CONTENT_TYPES.has(requestContentType);
67
+ const shouldIncludeResponseData =
68
+ ALLOWED_HTTP_DATA_CONTENT_TYPES.has(responseContentType);
69
+
57
70
  const sendToLogger = (responseBody) => {
58
71
  // Prepare data for GL
59
72
  const logData = {
60
73
  log_type: 'http',
61
- request_type: 'devplatform-outbound',
74
+ // Using a custom request_type to differentiate from z.request() since `request_via_client` is not logged in GL
75
+ request_type: 'patched-devplatform-outbound',
62
76
  request_url: requestUrl,
63
77
  request_method: options.method || 'GET',
64
78
  request_headers: options.headers,
65
- request_data: options.body || '',
79
+ request_data: shouldIncludeRequestData
80
+ ? options.body || ''
81
+ : '<unsupported format>',
66
82
  request_via_client: false,
67
83
  response_status_code: response.statusCode,
68
84
  response_headers: response.headers,
69
- response_content: responseBody,
85
+ response_content: shouldIncludeResponseData
86
+ ? responseBody
87
+ : '<unsupported format>',
70
88
  };
71
89
 
72
90
  object.zapierLogger(
@@ -19,42 +19,84 @@ const environmentTools = require('./environment');
19
19
  const schemaTools = require('./schema');
20
20
  const ZapierPromise = require('./promise');
21
21
 
22
- const RequestSchema = require('zapier-platform-schema/lib/schemas/RequestSchema');
23
- const FunctionSchema = require('zapier-platform-schema/lib/schemas/FunctionSchema');
24
-
25
- const isRequestOrFunction = (obj) => {
22
+ const isDefinedPrimitive = (value) => {
26
23
  return (
27
- RequestSchema.validate(obj).valid || FunctionSchema.validate(obj).valid
24
+ value === null ||
25
+ typeof value === 'string' ||
26
+ typeof value === 'number' ||
27
+ typeof value === 'boolean'
28
28
  );
29
29
  };
30
30
 
31
- const extendAppRaw = (base, extension) => {
32
- const keysToOverride = [
33
- 'test',
34
- 'perform',
35
- 'performList',
36
- 'performSubscribe',
37
- 'performUnsubscribe',
38
- ];
39
- const concatArrayAndOverrideKeys = (objValue, srcValue, key) => {
40
- if (Array.isArray(objValue) && Array.isArray(srcValue)) {
41
- return objValue.concat(srcValue);
31
+ const shouldFullyReplace = (path) => {
32
+ // covers inputFields, outputFields, sample, throttle, etc
33
+ const isOperation = path[path.length - 2] === 'operation';
34
+ return isOperation;
35
+ };
36
+
37
+ const extendAppRaw = (base, extension, path) => {
38
+ if (extension === undefined) {
39
+ return base;
40
+ } else if (isDefinedPrimitive(extension)) {
41
+ return extension;
42
+ } else if (Array.isArray(extension)) {
43
+ return [...extension];
44
+ } else if (_.isPlainObject(extension)) {
45
+ path = path || [];
46
+ if (shouldFullyReplace(path)) {
47
+ return extension;
48
+ } else {
49
+ const baseObject = _.isPlainObject(base) ? base : {};
50
+ const result = { ...baseObject };
51
+ for (const [key, value] of Object.entries(extension)) {
52
+ const newPath = [...path, key];
53
+ result[key] = extendAppRaw(baseObject[key], value, newPath);
54
+ }
55
+ return result;
42
56
  }
57
+ }
58
+ throw new TypeError('Unexpected extension type');
59
+ };
43
60
 
44
- if (
45
- // Do full replacement when it comes to keysToOverride
46
- keysToOverride.indexOf(key) !== -1 &&
47
- _.isPlainObject(srcValue) &&
48
- _.isPlainObject(objValue) &&
49
- isRequestOrFunction(srcValue) &&
50
- isRequestOrFunction(objValue)
51
- ) {
52
- return srcValue;
61
+ const mayMoveCreatesToResourcesInExtension = (base, extension) => {
62
+ // The backend sends an extension for creates.KEY.operation.perform as a
63
+ // special case for legacy-scripting-runner.
64
+ // For details, see the MR description at
65
+ // https://gitlab.com/zapier/zapier/-/merge_requests/57964
66
+ //
67
+ // We need to make sure that 'creates.{key}Create' in extension won't collide
68
+ // with 'resources.{key}.create' in base. Otherwise, the checks in
69
+ // compileApp() will throw an error. So here we move 'creates.{key}Create' to
70
+ // 'resources.{key}.create' in extension if base has 'resources.{key}.create'.
71
+ //
72
+ // There's a regression test: Search for 'resource key collision' in
73
+ // integration-test.js.
74
+ if (
75
+ !_.isPlainObject(base.resources) ||
76
+ !_.isPlainObject(extension.creates) ||
77
+ _.isEmpty(base.resources) ||
78
+ _.isEmpty(extension.creates)
79
+ ) {
80
+ return extension;
81
+ }
82
+
83
+ const creates = extension.creates;
84
+ extension.creates = {};
85
+ extension.resources = extension.resources || {};
86
+
87
+ for (const [key, resource] of Object.entries(base.resources)) {
88
+ const standaloneCreate = creates[key + 'Create'];
89
+ if (resource.create && standaloneCreate) {
90
+ delete standaloneCreate.key;
91
+ delete standaloneCreate.noun;
92
+ extension.resources[key] = {
93
+ ...extension.resources[key],
94
+ create: standaloneCreate,
95
+ };
53
96
  }
97
+ }
54
98
 
55
- return undefined;
56
- };
57
- return _.mergeWith(base, extension, concatArrayAndOverrideKeys);
99
+ return extension;
58
100
  };
59
101
 
60
102
  const getAppRawOverride = (rpc, appRawOverride) => {
@@ -69,6 +111,10 @@ const getAppRawOverride = (rpc, appRawOverride) => {
69
111
  appRawOverride = appRawOverride[0];
70
112
 
71
113
  if (typeof appRawOverride !== 'string') {
114
+ appRawExtension = mayMoveCreatesToResourcesInExtension(
115
+ appRawOverride,
116
+ appRawExtension
117
+ );
72
118
  appRawOverride = extendAppRaw(appRawOverride, appRawExtension);
73
119
  resolve(appRawOverride);
74
120
  return;
@@ -116,17 +162,25 @@ const getAppRawOverride = (rpc, appRawOverride) => {
116
162
  // so allow for that, and an event.appRawOverride for "buildless" apps.
117
163
  const loadApp = (event, rpc, appRawOrPath) => {
118
164
  return new ZapierPromise((resolve, reject) => {
165
+ const appRaw = _.isString(appRawOrPath)
166
+ ? require(appRawOrPath)
167
+ : appRawOrPath;
168
+
119
169
  if (event && event.appRawOverride) {
170
+ if (
171
+ Array.isArray(event.appRawOverride) &&
172
+ event.appRawOverride.length > 1 &&
173
+ !event.appRawOverride[0]
174
+ ) {
175
+ event.appRawOverride[0] = appRaw;
176
+ }
177
+
120
178
  return getAppRawOverride(rpc, event.appRawOverride)
121
179
  .then((appRawOverride) => resolve(appRawOverride))
122
180
  .catch((err) => reject(err));
123
181
  }
124
182
 
125
- if (_.isString(appRawOrPath)) {
126
- return resolve(require(appRawOrPath));
127
- }
128
-
129
- return resolve(appRawOrPath);
183
+ return resolve(appRaw);
130
184
  });
131
185
  };
132
186
 
package/src/tools/http.js CHANGED
@@ -4,6 +4,23 @@ const fetch = require('node-fetch');
4
4
  const FORM_TYPE = 'application/x-www-form-urlencoded';
5
5
  const JSON_TYPE = 'application/json';
6
6
  const JSON_TYPE_UTF8 = 'application/json; charset=utf-8';
7
+ const BINARY_TYPE = 'application/octet-stream';
8
+ const HTML_TYPE = 'text/html';
9
+ const TEXT_TYPE = 'text/plain';
10
+ const YAML_TYPE = 'application/yaml';
11
+ const XML_TYPE = 'application/xml';
12
+ const JSONAPI_TYPE = 'application/vnd.api+json';
13
+
14
+ const ALLOWED_HTTP_DATA_CONTENT_TYPES = new Set([
15
+ FORM_TYPE,
16
+ JSON_TYPE,
17
+ JSON_TYPE_UTF8,
18
+ HTML_TYPE,
19
+ TEXT_TYPE,
20
+ YAML_TYPE,
21
+ XML_TYPE,
22
+ JSONAPI_TYPE,
23
+ ]);
7
24
 
8
25
  const getContentType = (headers) => {
9
26
  const headerKeys = Object.keys(headers);
@@ -101,6 +118,13 @@ module.exports = {
101
118
  FORM_TYPE,
102
119
  JSON_TYPE,
103
120
  JSON_TYPE_UTF8,
121
+ BINARY_TYPE,
122
+ HTML_TYPE,
123
+ TEXT_TYPE,
124
+ YAML_TYPE,
125
+ XML_TYPE,
126
+ JSONAPI_TYPE,
127
+ ALLOWED_HTTP_DATA_CONTENT_TYPES,
104
128
  getContentType,
105
129
  parseDictHeader,
106
130
  unheader,
@@ -4,9 +4,11 @@ const crypto = require('crypto');
4
4
 
5
5
  const { DehydrateError } = require('../errors');
6
6
 
7
- // Max URL length for Node is 8KB (https://stackoverflow.com/a/56954244/)
8
- // 8 * 1024 * (3 / 4) = 6144 max, minus some room for additional overhead
9
- const MAX_PAYLOAD_SIZE = 6000;
7
+ // https://nodejs.org/docs/latest-v16.x/api/http.html#httpmaxheadersize
8
+ // Base64 encoding adds approx 4/3 to the original size
9
+ // To account for encoding, we use the inverse to calc the max original size (3/4)
10
+ // 16kb limit * 1024 * (3 / 4) = 12.228 kb max, minus some room for additional overhead
11
+ const MAX_PAYLOAD_SIZE = 12000;
10
12
 
11
13
  const wrapHydrate = (payload) => {
12
14
  payload = JSON.stringify(payload);
@@ -0,0 +1,2 @@
1
+ export type * from './zapier.generated';
2
+ export * from './zapier.custom';
@@ -0,0 +1,152 @@
1
+ import type {
2
+ AfterResponseMiddleware,
3
+ BeforeRequestMiddleware,
4
+ Bundle,
5
+ PerformFunction,
6
+ ZObject,
7
+ } from './zapier.custom';
8
+ import type {
9
+ App,
10
+ Authentication,
11
+ AuthenticationOAuth2Config,
12
+ BasicActionOperation,
13
+ BasicCreateActionOperation,
14
+ BasicDisplay,
15
+ BasicHookOperation,
16
+ BasicPollingOperation,
17
+ Create,
18
+ Search,
19
+ Trigger,
20
+ } from './zapier.generated';
21
+
22
+ import { expectType } from 'tsd';
23
+
24
+ const basicDisplay: BasicDisplay = {
25
+ label: 'some-label',
26
+ description: 'some-description',
27
+ directions: 'some-directions',
28
+ hidden: false,
29
+ };
30
+ expectType<BasicDisplay>(basicDisplay);
31
+
32
+ const oauth2Config: AuthenticationOAuth2Config = {
33
+ authorizeUrl: 'https://example.com/authorize',
34
+ getAccessToken: 'https://example.com/token',
35
+ refreshAccessToken: async (z: ZObject, b: Bundle) => 'some-refresh-token',
36
+ };
37
+ expectType<AuthenticationOAuth2Config>(oauth2Config);
38
+
39
+ const authentication: Authentication = {
40
+ type: 'oauth2',
41
+ test: async (z: ZObject, b: Bundle) => ({ data: true }),
42
+ oauth2Config,
43
+ };
44
+ expectType<Authentication>(authentication);
45
+
46
+ const createOperation: BasicCreateActionOperation = {
47
+ inputFields: [{ key: 'some-input-key-1', type: 'string', required: true }],
48
+ perform: async (z: ZObject, b: Bundle) => ({ data: true }),
49
+ sample: { id: 'some-id', name: 'some-name' },
50
+ };
51
+ expectType<BasicCreateActionOperation>(createOperation);
52
+
53
+ const create: Create = {
54
+ key: 'some_create_key_v1',
55
+ noun: 'Some Noun',
56
+ display: {
57
+ label: 'some create label',
58
+ description: 'some trigger description',
59
+ },
60
+ operation: createOperation,
61
+ };
62
+ expectType<Create>(create);
63
+
64
+ const pollingOperation: BasicPollingOperation = {
65
+ type: 'polling',
66
+ inputFields: [{ key: 'some-input-key-1', type: 'number', required: true }],
67
+ perform: async (z: ZObject, b: Bundle) => ({ data: true }),
68
+ };
69
+
70
+ const pollingTrigger: Trigger = {
71
+ key: 'some_polling_trigger_key_v1',
72
+ noun: 'Some Noun',
73
+ display: {
74
+ label: 'some polling trigger label',
75
+ description: 'some polling trigger description',
76
+ },
77
+ operation: pollingOperation,
78
+ };
79
+ expectType<Trigger>(pollingTrigger);
80
+
81
+ const hookOperation: BasicHookOperation = {
82
+ type: 'hook',
83
+ inputFields: [{ key: 'some-input-key-1', type: 'boolean', required: false }],
84
+ perform: async (z: ZObject, b: Bundle) => ({ data: true }),
85
+ performList: async (z: ZObject, b: Bundle) => [{ data: true }],
86
+ performSubscribe: async (z: ZObject, b: Bundle) => ({ data: true }),
87
+ performUnsubscribe: async (z: ZObject, b: Bundle) => ({ data: true }),
88
+ };
89
+ expectType<BasicHookOperation>(hookOperation);
90
+
91
+ const hookTrigger: Trigger = {
92
+ key: 'some_hook_trigger_key_v1',
93
+ noun: 'Some Noun',
94
+ display: {
95
+ label: 'some hook label',
96
+ description: 'some hook description',
97
+ },
98
+ operation: hookOperation,
99
+ };
100
+ expectType<Trigger>(hookTrigger);
101
+
102
+ const searchOperation: BasicActionOperation = {
103
+ inputFields: [{ key: 'some-input-key-1', type: 'file', required: true }],
104
+ perform: async (z: ZObject, b: Bundle) => [{ data: true }],
105
+ };
106
+ expectType<BasicActionOperation>(searchOperation);
107
+
108
+ const search: Search = {
109
+ key: 'some_search_key_v1',
110
+ noun: 'Some Noun',
111
+ display: {
112
+ label: 'some search label',
113
+ description: 'some search description',
114
+ },
115
+ operation: searchOperation,
116
+ };
117
+ expectType<Search>(search);
118
+
119
+ const addBearerHeader: BeforeRequestMiddleware = (request, z, bundle) => {
120
+ if (bundle?.authData?.access_token && !request.headers!.Authorization) {
121
+ request.headers!.Authorization = `Bearer ${bundle.authData.access_token}`;
122
+ }
123
+ return request;
124
+ };
125
+ expectType<BeforeRequestMiddleware>(addBearerHeader);
126
+
127
+ const checkPermissionsError: AfterResponseMiddleware = (response, z) => {
128
+ if (response.status === 403) {
129
+ throw new z.errors.Error(
130
+ response.json?.['o:errorDetails']?.[0].detail,
131
+ response.status.toString()
132
+ );
133
+ }
134
+ return response;
135
+ };
136
+ expectType<AfterResponseMiddleware>(checkPermissionsError);
137
+
138
+ const app: App = {
139
+ platformVersion: '0.0.1',
140
+ version: '0.0.1',
141
+
142
+ beforeRequest: [addBearerHeader],
143
+ afterResponse: [checkPermissionsError],
144
+
145
+ creates: { [create.key]: create },
146
+ triggers: {
147
+ [pollingTrigger.key]: pollingTrigger,
148
+ [hookTrigger.key]: hookTrigger,
149
+ },
150
+ searches: { [search.key]: search },
151
+ };
152
+ expectType<App>(app);
@@ -22,10 +22,6 @@ export const createAppTester: (
22
22
  clearZcacheBeforeUse?: boolean
23
23
  ) => Promise<T>; // appTester always returns a promise
24
24
 
25
- // internal only
26
- // export const integrationTestHandler: () => any;
27
- // export const createAppHandler: (appRaw: object) => any
28
-
29
25
  type HttpMethod =
30
26
  | 'GET'
31
27
  | 'POST'
@@ -118,9 +114,9 @@ export interface HttpResponse extends BaseHttpResponse {
118
114
 
119
115
  export interface RawHttpResponse extends BaseHttpResponse {
120
116
  body: NodeJS.ReadableStream;
121
- buffer(): Promise<Buffer>;
122
- json(): Promise<object>;
123
- text(): Promise<string>;
117
+ buffer(): Promise<Buffer>;
118
+ json(): Promise<object>;
119
+ text(): Promise<string>;
124
120
  }
125
121
 
126
122
  type DehydrateFunc = <T>(
@@ -204,3 +200,26 @@ export interface ZObject {
204
200
  delete: (key: string) => Promise<boolean>;
205
201
  };
206
202
  }
203
+
204
+ /**
205
+ * A function that performs the action.
206
+ *
207
+ * @template BI The shape of data in the `bundle.inputData` object.
208
+ * @template R The return type of the function.
209
+ */
210
+ export type PerformFunction<BI = Record<string, any>, R = any> = (
211
+ z: ZObject,
212
+ bundle: Bundle<BI>
213
+ ) => Promise<R>;
214
+
215
+ export type BeforeRequestMiddleware = (
216
+ request: HttpRequestOptions,
217
+ z?: ZObject,
218
+ bundle?: Bundle
219
+ ) => HttpRequestOptions;
220
+
221
+ export type AfterResponseMiddleware = (
222
+ response: HttpResponse,
223
+ z: ZObject,
224
+ bundle?: Bundle
225
+ ) => HttpResponse;