|
1 | 1 | import * as uuid from "uuid"; |
2 | | -import * as request from "request"; |
3 | 2 |
|
4 | 3 | import { encodeFirestoreValue } from "./firestore/encodeFirestoreValue"; |
5 | 4 | import * as utils from "./utils"; |
6 | 5 | import { EmulatedTriggerDefinition } from "./emulator/functionsEmulatorShared"; |
7 | 6 | import { FunctionsEmulatorShell } from "./emulator/functionsEmulatorShell"; |
8 | 7 | import { AuthMode, AuthType, EventOptions } from "./emulator/events/types"; |
| 8 | +import { Client, ClientResponse, ClientVerbOptions } from "./apiv2"; |
| 9 | + |
| 10 | +// HTTPS_SENTINEL is sent when a HTTPS call is made via functions:shell. |
| 11 | +export const HTTPS_SENTINEL = "Request sent to function."; |
9 | 12 |
|
10 | 13 | /** |
11 | 14 | * LocalFunction produces EmulatedTriggerDefinition into a function that can be called inside the nodejs repl. |
@@ -33,25 +36,119 @@ export default class LocalFunction { |
33 | 36 | }); |
34 | 37 | } |
35 | 38 |
|
36 | | - private constructCallableFunc( |
37 | | - data: string | object, |
38 | | - opts: { instanceIdToken?: string } |
39 | | - ): request.Request { |
| 39 | + private constructCallableFunc(data: string | object, opts: { instanceIdToken?: string }): void { |
40 | 40 | opts = opts || {}; |
41 | 41 |
|
42 | 42 | const headers: Record<string, string> = {}; |
43 | 43 | if (opts.instanceIdToken) { |
44 | 44 | headers["Firebase-Instance-ID-Token"] = opts.instanceIdToken; |
45 | 45 | } |
46 | 46 |
|
47 | | - return request.post({ |
48 | | - callback: (...args) => this.requestCallBack(...args), |
49 | | - baseUrl: this.url, |
50 | | - uri: "", |
51 | | - body: { data }, |
52 | | - json: true, |
53 | | - headers: headers, |
| 47 | + if (!this.url) { |
| 48 | + throw new Error("No URL provided"); |
| 49 | + } |
| 50 | + |
| 51 | + const client = new Client({ urlPrefix: this.url, auth: false }); |
| 52 | + void client |
| 53 | + .post<any, any>("", data, { headers }) |
| 54 | + .then((res) => { |
| 55 | + this.requestCallBack<any>(undefined, res, res.body); |
| 56 | + }) |
| 57 | + .catch((err) => { |
| 58 | + this.requestCallBack(err); |
| 59 | + }); |
| 60 | + } |
| 61 | + |
| 62 | + private constructHttpsFunc(): requestShim { |
| 63 | + if (!this.url) { |
| 64 | + throw new Error("No URL provided"); |
| 65 | + } |
| 66 | + const callClient = new Client({ urlPrefix: this.url, auth: false }); |
| 67 | + type verbFn = (...args: any) => Promise<string>; |
| 68 | + const verbFactory = ( |
| 69 | + hasRequestBody: boolean, |
| 70 | + method: ( |
| 71 | + path: string, |
| 72 | + bodyOrOpts?: any, |
| 73 | + opts?: ClientVerbOptions |
| 74 | + ) => Promise<ClientResponse<any>> |
| 75 | + ): verbFn => { |
| 76 | + return async (pathOrOptions?: string | HttpsOptions, options?: HttpsOptions) => { |
| 77 | + const { path, opts } = this.extractArgs(pathOrOptions, options); |
| 78 | + try { |
| 79 | + const res = hasRequestBody |
| 80 | + ? await method(path, opts.body, toClientVerbOptions(opts)) |
| 81 | + : await method(path, toClientVerbOptions(opts)); |
| 82 | + this.requestCallBack(undefined, res, res.body); |
| 83 | + } catch (err) { |
| 84 | + this.requestCallBack(err); |
| 85 | + } |
| 86 | + return HTTPS_SENTINEL; |
| 87 | + }; |
| 88 | + }; |
| 89 | + |
| 90 | + const shim = verbFactory(true, (path: string, json?: any, opts?: ClientVerbOptions) => { |
| 91 | + const req = Object.assign(opts || {}, { |
| 92 | + path: path, |
| 93 | + body: json, |
| 94 | + method: opts?.method || "GET", |
| 95 | + }); |
| 96 | + return callClient.request(req); |
54 | 97 | }); |
| 98 | + const verbs: verbMethods = { |
| 99 | + post: verbFactory(true, (path: string, json?: any, opts?: ClientVerbOptions) => |
| 100 | + callClient.post(path, json, opts) |
| 101 | + ), |
| 102 | + put: verbFactory(true, (path: string, json?: any, opts?: ClientVerbOptions) => |
| 103 | + callClient.put(path, json, opts) |
| 104 | + ), |
| 105 | + patch: verbFactory(true, (path: string, json?: any, opts?: ClientVerbOptions) => |
| 106 | + callClient.patch(path, json, opts) |
| 107 | + ), |
| 108 | + get: verbFactory(false, (path: string, opts?: ClientVerbOptions) => |
| 109 | + callClient.get(path, opts) |
| 110 | + ), |
| 111 | + del: verbFactory(false, (path: string, opts?: ClientVerbOptions) => |
| 112 | + callClient.delete(path, opts) |
| 113 | + ), |
| 114 | + delete: verbFactory(false, (path: string, opts?: ClientVerbOptions) => |
| 115 | + callClient.delete(path, opts) |
| 116 | + ), |
| 117 | + options: verbFactory(false, (path: string, opts?: ClientVerbOptions) => |
| 118 | + callClient.options(path, opts) |
| 119 | + ), |
| 120 | + }; |
| 121 | + return Object.assign(shim, verbs); |
| 122 | + } |
| 123 | + |
| 124 | + private extractArgs( |
| 125 | + pathOrOptions?: string | HttpsOptions, |
| 126 | + options?: HttpsOptions |
| 127 | + ): { path: string; opts: HttpsOptions } { |
| 128 | + // Case: No arguments provided |
| 129 | + if (!pathOrOptions && !options) { |
| 130 | + return { path: "/", opts: {} }; |
| 131 | + } |
| 132 | + |
| 133 | + // Case: pathOrOptions is provided as a string |
| 134 | + if (typeof pathOrOptions === "string") { |
| 135 | + return { path: pathOrOptions, opts: options || {} }; |
| 136 | + } |
| 137 | + |
| 138 | + // Case: pathOrOptions is an object (HttpsOptions), and options is not provided |
| 139 | + if (typeof pathOrOptions !== "string" && !!pathOrOptions && !options) { |
| 140 | + return { path: "/", opts: pathOrOptions }; |
| 141 | + } |
| 142 | + |
| 143 | + // Error case: Invalid combination of arguments |
| 144 | + if (typeof pathOrOptions !== "string" || !options) { |
| 145 | + throw new Error( |
| 146 | + `Invalid argument combination: Expected a string and/or HttpsOptions, got ${typeof pathOrOptions} and ${typeof options}` |
| 147 | + ); |
| 148 | + } |
| 149 | + |
| 150 | + // Default return, though this point should not be reached |
| 151 | + return { path: "/", opts: {} }; |
55 | 152 | } |
56 | 153 |
|
57 | 154 | constructAuth(auth?: EventOptions["auth"], authType?: AuthType): AuthMode { |
@@ -120,11 +217,11 @@ export default class LocalFunction { |
120 | 217 | }; |
121 | 218 | } |
122 | 219 |
|
123 | | - private requestCallBack(err: unknown, response: request.Response, body: string | object) { |
| 220 | + private requestCallBack<T>(err: unknown, response?: ClientResponse<T>, body?: string | object) { |
124 | 221 | if (err) { |
125 | 222 | return console.warn("\nERROR SENDING REQUEST: " + err); |
126 | 223 | } |
127 | | - const status = response ? response.statusCode + ", " : ""; |
| 224 | + const status = response ? response.status + ", " : ""; |
128 | 225 |
|
129 | 226 | // If the body is a string we want to check if we can parse it as JSON |
130 | 227 | // and pretty-print it. We can't blindly stringify because stringifying |
@@ -240,14 +337,46 @@ export default class LocalFunction { |
240 | 337 | if (isCallable) { |
241 | 338 | return (data: any, opt: any) => this.constructCallableFunc(data, opt); |
242 | 339 | } else { |
243 | | - return request.defaults({ |
244 | | - callback: (...args) => this.requestCallBack(...args), |
245 | | - baseUrl: this.url, |
246 | | - uri: "", |
247 | | - }); |
| 340 | + return this.constructHttpsFunc(); |
248 | 341 | } |
249 | 342 | } else { |
250 | 343 | return (data: any, opt: any) => this.triggerEvent(data, opt); |
251 | 344 | } |
252 | 345 | } |
253 | 346 | } |
| 347 | + |
| 348 | +// requestShim is a minimal implementation of the public API of the deprecated `request` package |
| 349 | +// We expose it as part of `functions:shell` so that we can keep the previous API while removing |
| 350 | +// our dependency on `request`. |
| 351 | +interface requestShim extends verbMethods { |
| 352 | + (...args: any): any; |
| 353 | + // TODO(taeold/blidd/joehan) What other methods do we need to add? form? json? multipart? |
| 354 | +} |
| 355 | + |
| 356 | +interface verbMethods { |
| 357 | + get(...args: any): any; |
| 358 | + post(...args: any): any; |
| 359 | + put(...args: any): any; |
| 360 | + patch(...args: any): any; |
| 361 | + del(...args: any): any; |
| 362 | + delete(...args: any): any; |
| 363 | + options(...args: any): any; |
| 364 | +} |
| 365 | + |
| 366 | +// HttpsOptions is a subset of request's CoreOptions |
| 367 | +// https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/request/index.d.ts#L107 |
| 368 | +// We intentionally omit options that are likely useless for `functions:shell` |
| 369 | +type HttpsOptions = { |
| 370 | + method?: "GET" | "PUT" | "POST" | "DELETE" | "PATCH" | "OPTIONS" | "HEAD"; |
| 371 | + headers?: Record<string, any>; |
| 372 | + body?: any; |
| 373 | + qs?: any; |
| 374 | +}; |
| 375 | + |
| 376 | +function toClientVerbOptions(opts: HttpsOptions): ClientVerbOptions { |
| 377 | + return { |
| 378 | + method: opts.method, |
| 379 | + headers: opts.headers, |
| 380 | + queryParams: opts.qs, |
| 381 | + }; |
| 382 | +} |
0 commit comments