zapier-platform-core 15.18.0 → 15.19.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,6 +1,6 @@
1
1
  {
2
2
  "name": "zapier-platform-core",
3
- "version": "15.18.0",
3
+ "version": "15.19.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/",
@@ -53,7 +53,7 @@
53
53
  "node-fetch": "2.6.7",
54
54
  "oauth-sign": "0.9.0",
55
55
  "semver": "7.5.2",
56
- "zapier-platform-schema": "15.18.0"
56
+ "zapier-platform-schema": "15.19.0"
57
57
  },
58
58
  "devDependencies": {
59
59
  "@types/node-fetch": "^2.6.11",
@@ -9,19 +9,25 @@ const wrapHydrate = require('./wrap-hydrate');
9
9
  const createDehydrator = (input, type = 'method') => {
10
10
  const app = _.get(input, '_zapier.app');
11
11
 
12
- return (func, inputData) => {
12
+ return (func, inputData, cacheExpiration) => {
13
13
  inputData = inputData || {};
14
14
  if (inputData.inputData) {
15
15
  throw new DehydrateError(
16
16
  'Oops! You passed a full `bundle` - really you should pass what you want under `inputData`!'
17
17
  );
18
18
  }
19
- return wrapHydrate({
19
+ const payload = {
20
20
  type,
21
21
  method: resolveMethodPath(app, func),
22
22
  // inputData vs. bundle is a legacy oddity
23
23
  bundle: _.omit(inputData, 'environment'), // don't leak the environment
24
- });
24
+ };
25
+
26
+ if (cacheExpiration) {
27
+ payload.cacheExpiration = cacheExpiration;
28
+ }
29
+
30
+ return wrapHydrate(payload);
25
31
  };
26
32
  };
27
33
 
@@ -48,7 +48,7 @@ const createHttpPatch = (event) => {
48
48
  return originalRequest(options, callback);
49
49
  }
50
50
 
51
- // Ignore requests made via the request client
51
+ // Ignore requests made via the request client (z.request)
52
52
  if (_.get(options.headers, 'user-agent', []).indexOf('Zapier') !== -1) {
53
53
  return originalRequest(options, callback);
54
54
  }
@@ -112,7 +112,14 @@ const createHttpPatch = (event) => {
112
112
  }
113
113
  };
114
114
 
115
- response.on('data', (chunk) => chunks.push(chunk));
115
+ const originalEmit = response.emit;
116
+
117
+ response.emit = function (event, ...args) {
118
+ if (event === 'data') {
119
+ chunks.push(args[0]);
120
+ }
121
+ return originalEmit.apply(this, [event, ...args]);
122
+ };
116
123
  response.on('end', logResponse);
117
124
  response.on('error', logResponse);
118
125
 
@@ -17,6 +17,7 @@ const createLogger = require('./create-logger');
17
17
  const createRpcClient = require('./create-rpc-client');
18
18
  const environmentTools = require('./environment');
19
19
  const schemaTools = require('./schema');
20
+ const wrapFetchWithLogger = require('./fetch-logger');
20
21
  const ZapierPromise = require('./promise');
21
22
 
22
23
  const isDefinedPrimitive = (value) => {
@@ -189,10 +190,15 @@ const createLambdaHandler = (appRawOrPath) => {
189
190
  // Wait for all async events to complete before callback returns.
190
191
  // This is not strictly necessary since this is the default now when
191
192
  // using the callback; just putting it here to be explicit.
193
+ // In some cases, the code hangs and never exits because the event loop is not
194
+ // empty, so we can override the default behavior and exit after the app is done.
192
195
  context.callbackWaitsForEmptyEventLoop = true;
196
+ if (event.skipWaitForAsync === true) {
197
+ context.callbackWaitsForEmptyEventLoop = false;
198
+ }
193
199
 
194
200
  // replace native Promise with bluebird (works better with domains)
195
- if (!event.calledFromCli) {
201
+ if (!event.calledFromCli || event.calledFromCliInvoke) {
196
202
  ZapierPromise.patchGlobal();
197
203
  }
198
204
 
@@ -253,10 +259,16 @@ const createLambdaHandler = (appRawOrPath) => {
253
259
 
254
260
  const { skipHttpPatch } = appRaw.flags || {};
255
261
  // Adds logging for _all_ kinds of http(s) requests, no matter the library
256
- if (!skipHttpPatch && !event.calledFromCli) {
262
+ if (
263
+ !skipHttpPatch &&
264
+ (!event.calledFromCli || event.calledFromCliInvoke)
265
+ ) {
257
266
  const httpPatch = createHttpPatch(event);
258
267
  httpPatch(require('http'), logger);
259
268
  httpPatch(require('https'), logger); // 'https' needs to be patched separately
269
+ if (global.fetch) {
270
+ global.fetch = wrapFetchWithLogger(global.fetch, logger);
271
+ }
260
272
  }
261
273
 
262
274
  // TODO: Avoid calling prepareApp(appRaw) repeatedly here as createApp()
@@ -264,6 +276,7 @@ const createLambdaHandler = (appRawOrPath) => {
264
276
  const compiledApp = schemaTools.prepareApp(appRaw);
265
277
 
266
278
  const input = createInput(compiledApp, event, logger, logBuffer, rpc);
279
+
267
280
  return app(input);
268
281
  })
269
282
  .then((output) => {
@@ -0,0 +1,107 @@
1
+ const _ = require('lodash');
2
+
3
+ const { ALLOWED_HTTP_DATA_CONTENT_TYPES } = require('./http');
4
+
5
+ const stringifyRequestData = (data) => {
6
+ // Be careful not to consume the data if it's a stream
7
+ if (typeof data === 'string') {
8
+ return data;
9
+ } else if (data instanceof URLSearchParams) {
10
+ return data.toString();
11
+ } else if (global.FormData && data instanceof global.FormData) {
12
+ return '<FormData>';
13
+ } else {
14
+ // See https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch#setting_a_body
15
+ // for other possible body types
16
+ return '<unsupported format>';
17
+ }
18
+ };
19
+
20
+ const normalizeRequestInfo = (input, init) => {
21
+ const result = {
22
+ method: 'GET',
23
+ headers: {},
24
+ data: '',
25
+ };
26
+
27
+ if (global.Request && input instanceof global.Request) {
28
+ result.url = input.url;
29
+ result.method = input.method || result.method;
30
+ result.headers =
31
+ Object.fromEntries(input.headers.entries()) || result.headers;
32
+ if (input.body) {
33
+ result.data = stringifyRequestData(input.body);
34
+ }
35
+ } else if (typeof input.toString === 'function') {
36
+ // This condition includes `typeof input === 'string'` as well
37
+ result.url = input.toString();
38
+ } else {
39
+ // Don't log if we don't recognize the input type
40
+ return null;
41
+ }
42
+
43
+ if (init) {
44
+ result.method = init.method || result.method;
45
+ result.headers = init.headers || result.headers;
46
+ if (init.body) {
47
+ result.data = stringifyRequestData(init.body);
48
+ }
49
+ }
50
+
51
+ return result;
52
+ };
53
+
54
+ // Is this request made by z.request()?
55
+ const isZapierUserAgent = (headers) =>
56
+ _.get(headers, 'user-agent', []).indexOf('Zapier') !== -1;
57
+
58
+ const shouldIncludeResponseContent = (contentType) => {
59
+ for (const ctype of ALLOWED_HTTP_DATA_CONTENT_TYPES) {
60
+ if (contentType.includes(ctype)) {
61
+ return true;
62
+ }
63
+ }
64
+ return false;
65
+ };
66
+
67
+ const stringifyResponseContent = async (response) => {
68
+ // Be careful not to consume the original response body, which is why we clone it
69
+ return response.clone().text();
70
+ };
71
+
72
+ // Usage:
73
+ // global.fetch = wrapFetchWithLogger(global.fetch, logger);
74
+ const wrapFetchWithLogger = (fetchFunc, logger) => {
75
+ if (fetchFunc.patchedByZapier) {
76
+ return fetchFunc;
77
+ }
78
+
79
+ const newFetch = async function (input, init) {
80
+ const response = await fetchFunc(input, init);
81
+ const requestInfo = normalizeRequestInfo(input, init);
82
+ if (requestInfo && !isZapierUserAgent(requestInfo.headers)) {
83
+ const responseContentType = response.headers.get('content-type');
84
+
85
+ logger(`${response.status} ${requestInfo.method} ${requestInfo.url}`, {
86
+ log_type: 'http',
87
+ request_type: 'patched-devplatform-outbound',
88
+ request_url: requestInfo.url,
89
+ request_method: requestInfo.method,
90
+ request_headers: requestInfo.headers,
91
+ request_data: requestInfo.data,
92
+ request_via_client: false,
93
+ response_status_code: response.status,
94
+ response_headers: Object.fromEntries(response.headers.entries()),
95
+ response_content: shouldIncludeResponseContent(responseContentType)
96
+ ? await stringifyResponseContent(response)
97
+ : '<unsupported format>',
98
+ });
99
+ }
100
+ return response;
101
+ };
102
+
103
+ newFetch.patchedByZapier = true;
104
+ return newFetch;
105
+ };
106
+
107
+ module.exports = wrapFetchWithLogger;
package/src/tools/http.js CHANGED
@@ -7,6 +7,7 @@ const JSON_TYPE_UTF8 = 'application/json; charset=utf-8';
7
7
  const BINARY_TYPE = 'application/octet-stream';
8
8
  const HTML_TYPE = 'text/html';
9
9
  const TEXT_TYPE = 'text/plain';
10
+ const TEXT_TYPE_UTF8 = 'text/plain; charset=utf-8';
10
11
  const YAML_TYPE = 'application/yaml';
11
12
  const XML_TYPE = 'application/xml';
12
13
  const JSONAPI_TYPE = 'application/vnd.api+json';
@@ -17,6 +18,7 @@ const ALLOWED_HTTP_DATA_CONTENT_TYPES = new Set([
17
18
  JSON_TYPE_UTF8,
18
19
  HTML_TYPE,
19
20
  TEXT_TYPE,
21
+ TEXT_TYPE_UTF8,
20
22
  YAML_TYPE,
21
23
  XML_TYPE,
22
24
  JSONAPI_TYPE,
@@ -37,13 +37,78 @@ export interface Bundle<InputData = { [x: string]: any }> {
37
37
  inputDataRaw: { [x: string]: string };
38
38
  meta: {
39
39
  isBulkRead: boolean;
40
+
41
+ /**
42
+ * If true, this poll is being used to populate a dynamic dropdown.
43
+ * You only need to return the fields you specified (such as id and
44
+ * name), though returning everything is fine too.
45
+ */
40
46
  isFillingDynamicDropdown: boolean;
47
+
48
+ /**
49
+ * If true, this run was initiated manually via the Zap Editor.
50
+ */
41
51
  isLoadingSample: boolean;
52
+
53
+ /**
54
+ * If true, the results of this poll will be used to initialize the
55
+ * deduplication list rather than trigger a zap. You should grab as
56
+ * many items as possible.
57
+ */
42
58
  isPopulatingDedupe: boolean;
59
+
60
+ /**
61
+ * (legacy property) If true, the poll was triggered by a user
62
+ * testing their account (via clicking “test” or during setup). We
63
+ * use this data to populate the auth label, but it’s mostly used to
64
+ * verify we made a successful authenticated request
65
+ *
66
+ * @deprecated
67
+ */
43
68
  isTestingAuth: boolean;
69
+
70
+ /**
71
+ * The number of items you should fetch. -1 indicates there’s no
72
+ * limit. Build this into your calls insofar as you are able.
73
+ */
44
74
  limit: number;
75
+
76
+ /**
77
+ * Used in paging to uniquely identify which page of results should
78
+ * be returned.
79
+ */
45
80
  page: number;
46
- zap?: { id: string };
81
+
82
+ /**
83
+ * When a create is called as part of a search-or-create step,
84
+ * this will be the key of the search.
85
+ */
86
+ withSearch?: string;
87
+
88
+ /**
89
+ * The timezone the user has configured for their account or
90
+ * specific automation. Received as TZ identifier, such as
91
+ * “America/New_York”.
92
+ */
93
+ timezone: string | null;
94
+
95
+ /** @deprecated */
96
+ zap?: {
97
+ /** @deprecated */
98
+ id: string;
99
+ /** @deprecated */
100
+ user: {
101
+ /** @deprecated use meta.timezone instead. */
102
+ timezone: string;
103
+ };
104
+ };
105
+
106
+ /**
107
+ * Contains metadata about the input fields, optionally provided
108
+ * by the inputField.meta property. Useful for storing extra data
109
+ * in dynamically created input fields.
110
+ */
111
+ inputFields: { [fieldKey: string]: { [metaKey: string]: string | number | boolean } };
47
112
  };
48
113
  rawRequest?: Partial<{
49
114
  method: HttpMethod;
@@ -108,6 +173,8 @@ export interface HttpRequestOptions {
108
173
  middlewareData?: Record<string, any>;
109
174
  }
110
175
 
176
+ type HttpRequestOptionsWithUrl = HttpRequestOptions & { url: string };
177
+
111
178
  interface BaseHttpResponse {
112
179
  status: number;
113
180
  headers: Headers;
@@ -133,7 +200,8 @@ export interface RawHttpResponse<T = any> extends BaseHttpResponse {
133
200
 
134
201
  type DehydrateFunc = <T>(
135
202
  func: (z: ZObject, bundle: Bundle<T>) => any,
136
- inputData: T
203
+ inputData?: T,
204
+ cacheExpiration?: number
137
205
  ) => string;
138
206
 
139
207
  export interface ZObject {
@@ -143,16 +211,14 @@ export interface ZObject {
143
211
  url: string,
144
212
  options: HttpRequestOptions & { raw: true }
145
213
  ): Promise<RawHttpResponse<T>>;
146
- <T = any>(
147
- options: HttpRequestOptions & { raw: true; url: string }
148
- ): Promise<RawHttpResponse<T>>;
214
+ <T = any>(options: HttpRequestOptionsWithUrl & { raw: true }): Promise<
215
+ RawHttpResponse<T>
216
+ >;
149
217
 
150
218
  <T = any>(url: string, options?: HttpRequestOptions): Promise<
151
219
  HttpResponse<T>
152
220
  >;
153
- <T = any>(options: HttpRequestOptions & { url: string }): Promise<
154
- HttpResponse<T>
155
- >;
221
+ <T = any>(options: HttpRequestOptionsWithUrl): Promise<HttpResponse<T>>;
156
222
  };
157
223
 
158
224
  console: Console;
@@ -213,7 +279,7 @@ export interface ZObject {
213
279
 
214
280
  cache: {
215
281
  get: (key: string) => Promise<any>;
216
- set: (key: string, value: any, ttl?: number) => Promise<boolean>;
282
+ set: (key: string, value: any, ttl?: number, scope?: string[], nx?: boolean) => Promise<boolean|null>;
217
283
  delete: (key: string) => Promise<boolean>;
218
284
  };
219
285
  }
@@ -230,10 +296,10 @@ export type PerformFunction<BI = Record<string, any>, R = any> = (
230
296
  ) => Promise<R>;
231
297
 
232
298
  export type BeforeRequestMiddleware = (
233
- request: HttpRequestOptions,
299
+ request: HttpRequestOptionsWithUrl,
234
300
  z: ZObject,
235
301
  bundle: Bundle
236
- ) => HttpRequestOptions | Promise<HttpRequestOptions>;
302
+ ) => HttpRequestOptionsWithUrl | Promise<HttpRequestOptionsWithUrl>;
237
303
 
238
304
  export type AfterResponseMiddleware = (
239
305
  response: HttpResponse,
@@ -4,7 +4,7 @@
4
4
  * files, and/or the schema-to-ts tool and run its CLI to regenerate
5
5
  * these typings.
6
6
  *
7
- * zapier-platform-schema version: 15.17.0
7
+ * zapier-platform-schema version: 15.19.0
8
8
  * schema-to-ts compiler version: 0.1.0
9
9
  */
10
10
 
@@ -187,6 +187,22 @@ export type FieldChoices =
187
187
  | { [k: string]: unknown }
188
188
  | (string | FieldChoiceWithLabel)[];
189
189
 
190
+ /**
191
+ * Allows for additional metadata to be stored on the field.
192
+ *
193
+ * [Docs: FieldMetaSchema](https://github.com/zapier/zapier-platform/blob/main/packages/schema/docs/build/schema.md#FieldMetaSchema)
194
+ */
195
+ export interface FieldMeta {
196
+ /**
197
+ * Only string, integer or boolean values are allowed.
198
+ *
199
+ * This interface was referenced by `FieldMetaSchema`'s JSON-Schema
200
+ * definition
201
+ * via the `patternProperty` "[^\s]+".
202
+ */
203
+ [k: string]: string | number | boolean;
204
+ }
205
+
190
206
  /**
191
207
  * Defines a field an app either needs as input, or gives as output.
192
208
  * In addition to the requirements below, the following keys are
@@ -323,6 +339,12 @@ export interface Field {
323
339
  * "https://{{input}}.yourdomain.com").
324
340
  */
325
341
  inputFormat?: string;
342
+
343
+ /**
344
+ * Allows for additional metadata to be stored on the field.
345
+ * Supports simple key-values only (no sub-objects or arrays).
346
+ */
347
+ meta?: FieldMeta;
326
348
  }
327
349
 
328
350
  /**