From 01ad68f843821301da1da4ed1b6fad49fa98c072 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Thu, 1 Aug 2024 17:57:33 +0100 Subject: [PATCH] Remove all mocking from rest.spec.ts. --- .../src/api/routes/tests/queries/rest.spec.ts | 2 - packages/server/src/integrations/rest.ts | 62 +- .../src/integrations/tests/rest.spec.ts | 795 +++++++----------- .../types/src/documents/app/datasource.ts | 12 +- packages/types/src/documents/app/query.ts | 36 +- yarn.lock | 192 ++++- 6 files changed, 544 insertions(+), 555 deletions(-) diff --git a/packages/server/src/api/routes/tests/queries/rest.spec.ts b/packages/server/src/api/routes/tests/queries/rest.spec.ts index 29bbbf3a61..6f489bc723 100644 --- a/packages/server/src/api/routes/tests/queries/rest.spec.ts +++ b/packages/server/src/api/routes/tests/queries/rest.spec.ts @@ -5,8 +5,6 @@ import { getCachedVariable } from "../../../../threads/utils" import nock from "nock" import { generator } from "@budibase/backend-core/tests" -jest.unmock("node-fetch") - describe("rest", () => { let config: TestConfiguration let datasource: Datasource diff --git a/packages/server/src/integrations/rest.ts b/packages/server/src/integrations/rest.ts index 86c059bc82..ce2ec7d545 100644 --- a/packages/server/src/integrations/rest.ts +++ b/packages/server/src/integrations/rest.ts @@ -1,4 +1,5 @@ import { + BodyType, DatasourceFieldType, HttpMethod, Integration, @@ -15,7 +16,7 @@ import { import get from "lodash/get" import * as https from "https" import qs from "querystring" -import type { Response } from "node-fetch" +import type { Response, RequestInit } from "node-fetch" import fetch from "node-fetch" import { formatBytes } from "../utilities" import { performance } from "perf_hooks" @@ -28,15 +29,6 @@ import path from "path" import { Builder as XmlBuilder } from "xml2js" import { getAttachmentHeaders } from "./utils/restUtils" -enum BodyType { - NONE = "none", - FORM_DATA = "form", - XML = "xml", - ENCODED = "encoded", - JSON = "json", - TEXT = "text", -} - const coreFields = { path: { type: DatasourceFieldType.STRING, @@ -127,7 +119,23 @@ const SCHEMA: Integration = { }, } -class RestIntegration implements IntegrationBase { +interface ParsedResponse { + data: any + info: { + code: number + size: string + time: string + } + extra?: { + raw: string | undefined + headers: Record + } + pagination?: { + cursor: any + } +} + +export class RestIntegration implements IntegrationBase { private config: RestConfig private headers: { [key: string]: string @@ -138,7 +146,10 @@ class RestIntegration implements IntegrationBase { this.config = config } - async parseResponse(response: Response, pagination: PaginationConfig | null) { + async parseResponse( + response: Response, + pagination?: PaginationConfig + ): Promise { let data: any[] | string | undefined, raw: string | undefined, headers: Record = {}, @@ -235,8 +246,8 @@ class RestIntegration implements IntegrationBase { getUrl( path: string, queryString: string, - pagination: PaginationConfig | null, - paginationValues: PaginationValues | null + pagination?: PaginationConfig, + paginationValues?: PaginationValues ): string { // Add pagination params to query string if required if (pagination?.location === "query" && paginationValues) { @@ -279,10 +290,10 @@ class RestIntegration implements IntegrationBase { addBody( bodyType: string, body: string | any, - input: any, - pagination: PaginationConfig | null, - paginationValues: PaginationValues | null - ) { + input: RequestInit, + pagination?: PaginationConfig, + paginationValues?: PaginationValues + ): RequestInit { if (!input.headers) { input.headers = {} } @@ -345,6 +356,7 @@ class RestIntegration implements IntegrationBase { string = new XmlBuilder().buildObject(object) } input.body = string + // @ts-ignore input.headers["Content-Type"] = "application/xml" break case BodyType.JSON: @@ -356,13 +368,14 @@ class RestIntegration implements IntegrationBase { object[key] = value }) input.body = JSON.stringify(object) + // @ts-ignore input.headers["Content-Type"] = "application/json" break } return input } - getAuthHeaders(authConfigId: string): { [key: string]: any } { + getAuthHeaders(authConfigId?: string): { [key: string]: any } { let headers: any = {} if (this.config.authConfigs && authConfigId) { @@ -398,7 +411,7 @@ class RestIntegration implements IntegrationBase { headers = {}, method = HttpMethod.GET, disabledHeaders, - bodyType, + bodyType = BodyType.NONE, requestBody, authConfigId, pagination, @@ -407,7 +420,7 @@ class RestIntegration implements IntegrationBase { const authHeaders = this.getAuthHeaders(authConfigId) this.headers = { - ...this.config.defaultHeaders, + ...(this.config.defaultHeaders || {}), ...headers, ...authHeaders, } @@ -420,7 +433,7 @@ class RestIntegration implements IntegrationBase { } } - let input: any = { method, headers: this.headers } + let input: RequestInit = { method, headers: this.headers } input = this.addBody( bodyType, requestBody, @@ -437,7 +450,12 @@ class RestIntegration implements IntegrationBase { // Deprecated by rejectUnauthorized if (this.config.legacyHttpParser) { + // NOTE(samwho): it seems like this code doesn't actually work because it requires + // node-fetch >=3, and we're not on that because upgrading to it produces errors to + // do with ESM that are above my pay grade. + // https://github.com/nodejs/node/issues/43798 + // @ts-ignore input.extraHttpOptions = { insecureHTTPParser: true } } diff --git a/packages/server/src/integrations/tests/rest.spec.ts b/packages/server/src/integrations/tests/rest.spec.ts index dee17a5497..e869c58875 100644 --- a/packages/server/src/integrations/tests/rest.spec.ts +++ b/packages/server/src/integrations/tests/rest.spec.ts @@ -1,281 +1,222 @@ -jest.mock("node-fetch", () => { - const obj = { - my_next_cursor: 123, - } - const str = JSON.stringify(obj) - return jest.fn(() => ({ - headers: { - raw: () => { - return { - "content-type": ["application/json"], - "content-length": str.length, - } - }, - get: (name: string) => { - const lcName = name.toLowerCase() - if (lcName === "content-type") { - return ["application/json"] - } else if (lcName === "content-length") { - return str.length - } - }, - }, - json: jest.fn(() => obj), - text: jest.fn(() => str), - })) -}) - -jest.mock("@budibase/backend-core", () => { - const core = jest.requireActual("@budibase/backend-core") - return { - ...core, - context: { - ...core.context, - getProdAppId: jest.fn(() => "app-id"), - }, - } -}) -jest.mock("uuid", () => ({ v4: () => "00000000-0000-0000-0000-000000000000" })) - -import { default as RestIntegration } from "../rest" -import { RestAuthType } from "@budibase/types" -import fetch from "node-fetch" -import { Readable } from "stream" - -const FormData = require("form-data") -const { URLSearchParams } = require("url") +import nock from "nock" +import { RestIntegration } from "../rest" +import { BodyType, RestAuthType } from "@budibase/types" +import { Response } from "node-fetch" +import TestConfiguration from "../../../src/tests/utilities/TestConfiguration" +const UUID_REGEX = + "[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}" const HEADERS = { Accept: "application/json", "Content-Type": "application/json", } -class TestConfiguration { - integration: any - - constructor(config: any = {}) { - this.integration = new RestIntegration.integration(config) - } -} - describe("REST Integration", () => { - const BASE_URL = "https://myapi.com" - let config: any + let integration: RestIntegration + const config = new TestConfiguration() + + beforeAll(async () => { + await config.init() + }) + + afterAll(async () => { + config.end() + }) beforeEach(() => { - config = new TestConfiguration({ - url: BASE_URL, - }) - jest.clearAllMocks() + integration = new RestIntegration({ url: "https://example.com" }) + nock.cleanAll() }) it("calls the create method with the correct params", async () => { - const query = { + const body = { name: "test" } + nock("https://example.com", { reqheaders: HEADERS }) + .post("/api?test=1", JSON.stringify(body)) + .reply(200, { foo: "bar" }) + + const { data } = await integration.create({ path: "api", queryString: "test=1", headers: HEADERS, - bodyType: "json", - requestBody: JSON.stringify({ - name: "test", - }), - } - await config.integration.create(query) - expect(fetch).toHaveBeenCalledWith(`${BASE_URL}/api?test=1`, { - method: "POST", - body: '{"name":"test"}', - headers: HEADERS, + bodyType: BodyType.JSON, + requestBody: JSON.stringify(body), }) + expect(data).toEqual({ foo: "bar" }) }) it("calls the read method with the correct params", async () => { - const query = { + nock("https://example.com") + .get("/api?test=1") + .matchHeader("Accept", "text/html") + .reply(200, { foo: "bar" }) + + const { data } = await integration.read({ path: "api", queryString: "test=1", headers: { Accept: "text/html", }, - } - await config.integration.read(query) - expect(fetch).toHaveBeenCalledWith(`${BASE_URL}/api?test=1`, { - headers: { - Accept: "text/html", - }, - method: "GET", }) + expect(data).toEqual({ foo: "bar" }) }) it("calls the update method with the correct params", async () => { - const query = { + nock("https://example.com") + .put("/api?test=1", { name: "test" }) + .matchHeader("Accept", "application/json") + .reply(200, { foo: "bar" }) + + const { data } = await integration.update({ path: "api", queryString: "test=1", headers: { Accept: "application/json", }, - bodyType: "json", + bodyType: BodyType.JSON, requestBody: JSON.stringify({ name: "test", }), - } - await config.integration.update(query) - expect(fetch).toHaveBeenCalledWith(`${BASE_URL}/api?test=1`, { - method: "PUT", - body: '{"name":"test"}', - headers: HEADERS, }) + expect(data).toEqual({ foo: "bar" }) }) it("calls the delete method with the correct params", async () => { - const query = { + nock("https://example.com") + .delete("/api?test=1", { name: "test" }) + .matchHeader("Accept", "application/json") + .reply(200, { foo: "bar" }) + + const { data } = await integration.delete({ path: "api", queryString: "test=1", headers: { Accept: "application/json", }, - bodyType: "json", + bodyType: BodyType.JSON, requestBody: JSON.stringify({ name: "test", }), - } - await config.integration.delete(query) - expect(fetch).toHaveBeenCalledWith(`${BASE_URL}/api?test=1`, { - method: "DELETE", - headers: HEADERS, - body: '{"name":"test"}', }) + expect(data).toEqual({ foo: "bar" }) }) describe("request body", () => { const input = { a: 1, b: 2 } it("should allow no body", () => { - const output = config.integration.addBody("none", null, {}) + const output = integration.addBody("none", null, {}) expect(output.body).toBeUndefined() - expect(Object.keys(output.headers).length).toEqual(0) + expect(Object.keys(output.headers!).length).toEqual(0) }) it("should allow text body", () => { - const output = config.integration.addBody("text", "hello world", {}) + const output = integration.addBody("text", "hello world", {}) expect(output.body).toEqual("hello world") // gets added by fetch - expect(Object.keys(output.headers).length).toEqual(0) + expect(Object.keys(output.headers!).length).toEqual(0) }) it("should allow form data", () => { const FormData = require("form-data") - const output = config.integration.addBody("form", input, {}) + const output = integration.addBody("form", input, {}) expect(output.body instanceof FormData).toEqual(true) - expect(output.body._valueLength).toEqual(2) + expect((output.body! as any)._valueLength).toEqual(2) // gets added by fetch - expect(Object.keys(output.headers).length).toEqual(0) + expect(Object.keys(output.headers!).length).toEqual(0) }) it("should allow encoded form data", () => { const { URLSearchParams } = require("url") - const output = config.integration.addBody("encoded", input, {}) + const output = integration.addBody("encoded", input, {}) expect(output.body instanceof URLSearchParams).toEqual(true) - expect(output.body.toString()).toEqual("a=1&b=2") + expect(output.body!.toString()).toEqual("a=1&b=2") // gets added by fetch - expect(Object.keys(output.headers).length).toEqual(0) + expect(Object.keys(output.headers!).length).toEqual(0) }) it("should allow JSON", () => { - const output = config.integration.addBody("json", input, {}) + const output = integration.addBody("json", input, {}) expect(output.body).toEqual(JSON.stringify(input)) - expect(output.headers["Content-Type"]).toEqual("application/json") + expect((output.headers! as any)["Content-Type"]).toEqual( + "application/json" + ) }) it("should allow raw XML", () => { - const output = config.integration.addBody("xml", "12", {}) - expect(output.body.includes("1")).toEqual(true) - expect(output.body.includes("2")).toEqual(true) - expect(output.headers["Content-Type"]).toEqual("application/xml") + const output = integration.addBody("xml", "12", {}) + const body = output.body?.toString() + expect(body!.includes("1")).toEqual(true) + expect(body!.includes("2")).toEqual(true) + expect((output.headers! as any)["Content-Type"]).toEqual( + "application/xml" + ) }) it("should allow a valid js object and parse the contents to xml", () => { - const output = config.integration.addBody("xml", input, {}) - expect(output.body.includes("1")).toEqual(true) - expect(output.body.includes("2")).toEqual(true) - expect(output.headers["Content-Type"]).toEqual("application/xml") + const output = integration.addBody("xml", input, {}) + const body = output.body?.toString() + expect(body!.includes("1")).toEqual(true) + expect(body!.includes("2")).toEqual(true) + expect((output.headers! as any)["Content-Type"]).toEqual( + "application/xml" + ) }) it("should allow a valid json string and parse the contents to xml", () => { - const output = config.integration.addBody( - "xml", - JSON.stringify(input), - {} + const output = integration.addBody("xml", JSON.stringify(input), {}) + const body = output.body?.toString() + expect(body!.includes("1")).toEqual(true) + expect(body!.includes("2")).toEqual(true) + expect((output.headers! as any)["Content-Type"]).toEqual( + "application/xml" ) - expect(output.body.includes("1")).toEqual(true) - expect(output.body.includes("2")).toEqual(true) - expect(output.headers["Content-Type"]).toEqual("application/xml") }) }) describe("response", () => { - const contentTypes = ["application/json", "text/plain", "application/xml"] - function buildInput( - json: any, - text: any, - header: any, - status: number = 200 - ) { - return { - status, - json: json ? async () => json : undefined, - text: text ? async () => text : undefined, - headers: { - get: (key: string) => { - switch (key.toLowerCase()) { - case "content-length": - return 100 - case "content-type": - return header - default: - return "" - } - }, - raw: () => ({ "content-type": header }), - }, - } - } - it("should be able to parse JSON response", async () => { const obj = { a: 1 } - const input = buildInput(obj, JSON.stringify(obj), "application/json") - const output = await config.integration.parseResponse(input) - expect(output.data).toEqual({ a: 1 }) + const output = await integration.parseResponse( + new Response(JSON.stringify(obj), { + headers: { "content-type": "application/json" }, + }) + ) + expect(output.data).toEqual(obj) expect(output.info.code).toEqual(200) - expect(output.info.size).toEqual("100B") - expect(output.extra.raw).toEqual(JSON.stringify({ a: 1 })) - expect(output.extra.headers["content-type"]).toEqual("application/json") + expect(output.info.size).toEqual("7B") }) it("should be able to parse text response", async () => { const text = "hello world" - const input = buildInput(null, text, "text/plain") - const output = await config.integration.parseResponse(input) + const output = await integration.parseResponse( + new Response(text, { + headers: { "content-type": "text/plain" }, + }) + ) expect(output.data).toEqual(text) - expect(output.extra.raw).toEqual(text) - expect(output.extra.headers["content-type"]).toEqual("text/plain") }) it("should be able to parse XML response", async () => { const text = "12" - const input = buildInput(null, text, "application/xml") - const output = await config.integration.parseResponse(input) + const output = await integration.parseResponse( + new Response(text, { + headers: { "content-type": "application/xml" }, + }) + ) expect(output.data).toEqual({ a: "1", b: "2" }) - expect(output.extra.raw).toEqual(text) - expect(output.extra.headers["content-type"]).toEqual("application/xml") }) - test.each([...contentTypes, undefined])( - "should not throw an error on 204 no content", + test.each(["application/json", "text/plain", "application/xml", undefined])( + "should not throw an error on 204 no content for content type: %s", async contentType => { - const input = buildInput(undefined, "", contentType, 204) - const output = await config.integration.parseResponse(input) + const output = await integration.parseResponse( + new Response(undefined, { + headers: { "content-type": contentType! }, + status: 204, + }) + ) expect(output.data).toEqual([]) - expect(output.extra.raw).toEqual("") expect(output.info.code).toEqual(204) - expect(output.extra.headers["content-type"]).toEqual(contentType) } ) }) @@ -301,443 +242,311 @@ describe("REST Integration", () => { } beforeEach(() => { - config = new TestConfiguration({ - url: BASE_URL, + integration = new RestIntegration({ + url: "https://example.com", authConfigs: [basicAuth, bearerAuth], }) }) it("adds basic auth", async () => { - const query = { - authConfigId: "c59c14bd1898a43baa08da68959b24686", - } - await config.integration.read(query) - expect(fetch).toHaveBeenCalledWith(`${BASE_URL}/`, { - method: "GET", - headers: { - Authorization: "Basic dXNlcjpwYXNzd29yZA==", - }, - }) + const auth = `Basic ${Buffer.from("user:password").toString("base64")}` + nock("https://example.com", { reqheaders: { Authorization: auth } }) + .get("/") + .reply(200, { foo: "bar" }) + + const { data } = await integration.read({ authConfigId: basicAuth._id }) + expect(data).toEqual({ foo: "bar" }) }) it("adds bearer auth", async () => { - const query = { - authConfigId: "0d91d732f34e4befabeff50b392a8ff3", - } - await config.integration.read(query) - expect(fetch).toHaveBeenCalledWith(`${BASE_URL}/`, { - method: "GET", - headers: { - Authorization: "Bearer mytoken", - }, + nock("https://example.com", { + reqheaders: { Authorization: "Bearer mytoken" }, }) + .get("/") + .reply(200, { foo: "bar" }) + const { data } = await integration.read({ authConfigId: bearerAuth._id }) + expect(data).toEqual({ foo: "bar" }) }) }) describe("page based pagination", () => { it("can paginate using query params", async () => { - const pageParam = "my_page_param" - const sizeParam = "my_size_param" - const pageValue = 3 - const sizeValue = 10 - const query = { + nock("https://example.com") + .get("/api?page=3&size=10") + .reply(200, { foo: "bar" }) + const { data } = await integration.read({ path: "api", pagination: { type: "page", location: "query", - pageParam, - sizeParam, + pageParam: "page", + sizeParam: "size", }, - paginationValues: { - page: pageValue, - limit: sizeValue, - }, - } - await config.integration.read(query) - expect(fetch).toHaveBeenCalledWith( - `${BASE_URL}/api?${pageParam}=${pageValue}&${sizeParam}=${sizeValue}`, - { - headers: {}, - method: "GET", - } - ) + paginationValues: { page: 3, limit: 10 }, + }) + expect(data).toEqual({ foo: "bar" }) }) it("can paginate using JSON request body", async () => { - const pageParam = "my_page_param" - const sizeParam = "my_size_param" - const pageValue = 3 - const sizeValue = 10 - const query = { - bodyType: "json", + nock("https://example.com") + .post("/api", JSON.stringify({ page: 3, size: 10 })) + .reply(200, { foo: "bar" }) + const { data } = await integration.create({ + bodyType: BodyType.JSON, path: "api", pagination: { type: "page", location: "body", - pageParam, - sizeParam, + pageParam: "page", + sizeParam: "size", }, - paginationValues: { - page: pageValue, - limit: sizeValue, - }, - } - await config.integration.create(query) - expect(fetch).toHaveBeenCalledWith(`${BASE_URL}/api`, { - body: JSON.stringify({ - [pageParam]: pageValue, - [sizeParam]: sizeValue, - }), - headers: { - "Content-Type": "application/json", - }, - method: "POST", + paginationValues: { page: 3, limit: 10 }, }) + expect(data).toEqual({ foo: "bar" }) }) it("can paginate using form-data request body", async () => { - const pageParam = "my_page_param" - const sizeParam = "my_size_param" - const pageValue = 3 - const sizeValue = 10 - const query = { - bodyType: "form", + nock("https://example.com") + .post("/api", body => { + return ( + body.includes(`name="page"\r\n\r\n3\r\n`) && + body.includes(`name="size"\r\n\r\n10\r\n`) + ) + }) + .reply(200, { foo: "bar" }) + + const { data } = await integration.create({ + bodyType: BodyType.FORM_DATA, path: "api", pagination: { type: "page", location: "body", - pageParam, - sizeParam, + pageParam: "page", + sizeParam: "size", }, - paginationValues: { - page: pageValue, - limit: sizeValue, - }, - } - await config.integration.create(query) - expect(fetch).toHaveBeenCalledWith(`${BASE_URL}/api`, { - body: expect.any(FormData), - headers: {}, - method: "POST", + paginationValues: { page: 3, limit: 10 }, }) - // @ts-ignore - const sentData = JSON.stringify(fetch.mock.calls[0][1].body) - expect(sentData).toContain(pageParam) - expect(sentData).toContain(sizeParam) + expect(data).toEqual({ foo: "bar" }) }) it("can paginate using form-encoded request body", async () => { - const pageParam = "my_page_param" - const sizeParam = "my_size_param" - const pageValue = 3 - const sizeValue = 10 - const query = { - bodyType: "encoded", + nock("https://example.com") + .post("/api", { page: "3", size: "10" }) + .reply(200, { foo: "bar" }) + + const { data } = await integration.create({ + bodyType: BodyType.ENCODED, path: "api", pagination: { type: "page", location: "body", - pageParam, - sizeParam, + pageParam: "page", + sizeParam: "size", }, - paginationValues: { - page: pageValue, - limit: sizeValue, - }, - } - await config.integration.create(query) - expect(fetch).toHaveBeenCalledWith(`${BASE_URL}/api`, { - body: expect.any(URLSearchParams), - headers: {}, - method: "POST", + paginationValues: { page: 3, limit: 10 }, }) - // @ts-ignore - const sentData = fetch.mock.calls[0][1].body - expect(sentData.has(pageParam)).toEqual(true) - expect(sentData.get(pageParam)).toEqual(pageValue.toString()) - expect(sentData.has(pageParam)).toEqual(true) - expect(sentData.get(sizeParam)).toEqual(sizeValue.toString()) + expect(data).toEqual({ foo: "bar" }) }) }) describe("cursor based pagination", () => { it("can paginate using query params", async () => { - const pageParam = "my_page_param" - const sizeParam = "my_size_param" - const pageValue = 3 - const sizeValue = 10 - const query = { + nock("https://example.com") + .get("/api?page=3&size=10") + .reply(200, { cursor: 123, foo: "bar" }) + const { data, pagination } = await integration.read({ path: "api", pagination: { type: "cursor", location: "query", - pageParam, - sizeParam, - responseParam: "my_next_cursor", + pageParam: "page", + sizeParam: "size", + responseParam: "cursor", }, - paginationValues: { - page: pageValue, - limit: sizeValue, - }, - } - const res = await config.integration.read(query) - expect(fetch).toHaveBeenCalledWith( - `${BASE_URL}/api?${pageParam}=${pageValue}&${sizeParam}=${sizeValue}`, - { - headers: {}, - method: "GET", - } - ) - expect(res.pagination.cursor).toEqual(123) + paginationValues: { page: 3, limit: 10 }, + }) + expect(pagination?.cursor).toEqual(123) + expect(data).toEqual({ cursor: 123, foo: "bar" }) }) it("can paginate using JSON request body", async () => { - const pageParam = "my_page_param" - const sizeParam = "my_size_param" - const pageValue = 3 - const sizeValue = 10 - const query = { - bodyType: "json", + nock("https://example.com") + .post("/api", JSON.stringify({ page: 3, size: 10 })) + .reply(200, { cursor: 123, foo: "bar" }) + const { data, pagination } = await integration.create({ + bodyType: BodyType.JSON, path: "api", pagination: { type: "page", location: "body", - pageParam, - sizeParam, - responseParam: "my_next_cursor", + pageParam: "page", + sizeParam: "size", + responseParam: "cursor", }, - paginationValues: { - page: pageValue, - limit: sizeValue, - }, - } - const res = await config.integration.create(query) - expect(fetch).toHaveBeenCalledWith(`${BASE_URL}/api`, { - body: JSON.stringify({ - [pageParam]: pageValue, - [sizeParam]: sizeValue, - }), - headers: { - "Content-Type": "application/json", - }, - method: "POST", + paginationValues: { page: 3, limit: 10 }, }) - expect(res.pagination.cursor).toEqual(123) + expect(data).toEqual({ cursor: 123, foo: "bar" }) + expect(pagination?.cursor).toEqual(123) }) it("can paginate using form-data request body", async () => { - const pageParam = "my_page_param" - const sizeParam = "my_size_param" - const pageValue = 3 - const sizeValue = 10 - const query = { - bodyType: "form", + nock("https://example.com") + .post("/api", body => { + return ( + body.includes(`name="page"\r\n\r\n3\r\n`) && + body.includes(`name="size"\r\n\r\n10\r\n`) + ) + }) + .reply(200, { cursor: 123, foo: "bar" }) + const { data, pagination } = await integration.create({ + bodyType: BodyType.FORM_DATA, path: "api", pagination: { type: "page", location: "body", - pageParam, - sizeParam, - responseParam: "my_next_cursor", + pageParam: "page", + sizeParam: "size", + responseParam: "cursor", }, - paginationValues: { - page: pageValue, - limit: sizeValue, - }, - } - const res = await config.integration.create(query) - expect(fetch).toHaveBeenCalledWith(`${BASE_URL}/api`, { - body: expect.any(FormData), - headers: {}, - method: "POST", + paginationValues: { page: 3, limit: 10 }, }) - // @ts-ignore - const sentData = JSON.stringify(fetch.mock.calls[0][1].body) - expect(sentData).toContain(pageParam) - expect(sentData).toContain(sizeParam) - expect(res.pagination.cursor).toEqual(123) + expect(data).toEqual({ cursor: 123, foo: "bar" }) + expect(pagination?.cursor).toEqual(123) }) it("can paginate using form-encoded request body", async () => { - const pageParam = "my_page_param" - const sizeParam = "my_size_param" - const pageValue = 3 - const sizeValue = 10 - const query = { - bodyType: "encoded", + nock("https://example.com") + .post("/api", { page: "3", size: "10" }) + .reply(200, { cursor: 123, foo: "bar" }) + const { data, pagination } = await integration.create({ + bodyType: BodyType.ENCODED, path: "api", pagination: { type: "page", location: "body", - pageParam, - sizeParam, - responseParam: "my_next_cursor", + pageParam: "page", + sizeParam: "size", + responseParam: "cursor", }, - paginationValues: { - page: pageValue, - limit: sizeValue, - }, - } - const res = await config.integration.create(query) - expect(fetch).toHaveBeenCalledWith(`${BASE_URL}/api`, { - body: expect.any(URLSearchParams), - headers: {}, - method: "POST", + paginationValues: { page: 3, limit: 10 }, }) - // @ts-ignore - const sentData = fetch.mock.calls[0][1].body - expect(sentData.has(pageParam)).toEqual(true) - expect(sentData.get(pageParam)).toEqual(pageValue.toString()) - expect(sentData.has(pageParam)).toEqual(true) - expect(sentData.get(sizeParam)).toEqual(sizeValue.toString()) - expect(res.pagination.cursor).toEqual(123) + expect(data).toEqual({ cursor: 123, foo: "bar" }) + expect(pagination?.cursor).toEqual(123) }) it("should encode query string correctly", async () => { - const query = { + nock("https://example.com", { reqheaders: HEADERS }) + .post("/api?test=1%202", JSON.stringify({ name: "test" })) + .reply(200, { foo: "bar" }) + const { data } = await integration.create({ path: "api", queryString: "test=1 2", headers: HEADERS, - bodyType: "json", + bodyType: BodyType.JSON, requestBody: JSON.stringify({ name: "test", }), - } - await config.integration.create(query) - expect(fetch).toHaveBeenCalledWith(`${BASE_URL}/api?test=1%202`, { - method: "POST", - body: '{"name":"test"}', - headers: HEADERS, }) + expect(data).toEqual({ foo: "bar" }) }) }) describe("Configuration options", () => { - it("Attaches insecureHttpParams when legacy HTTP Parser option is set", async () => { - config = new TestConfiguration({ - url: BASE_URL, + // NOTE(samwho): it seems like this code doesn't actually work because it requires + // node-fetch >=3, and we're not on that because upgrading to it produces errors to + // do with ESM that are above my pay grade. + + // eslint-disable-next-line jest/no-commented-out-tests + // it("doesn't fail when legacyHttpParser is set", async () => { + // const server = createServer((req, res) => { + // res.writeHead(200, { + // "Transfer-Encoding": "chunked", + // "Content-Length": "10", + // }) + // res.end(JSON.stringify({ foo: "bar" })) + // }) + + // server.listen() + // await new Promise(resolve => server.once("listening", resolve)) + + // const address = server.address() as AddressInfo + + // const integration = new RestIntegration({ + // url: `http://localhost:${address.port}`, + // legacyHttpParser: true, + // }) + // const { data } = await integration.read({}) + // expect(data).toEqual({ foo: "bar" }) + // }) + + it("doesn't fail when legacyHttpParser is true", async () => { + nock("https://example.com").get("/").reply(200, { foo: "bar" }) + const integration = new RestIntegration({ + url: "https://example.com", legacyHttpParser: true, }) - await config.integration.read({}) - expect(fetch).toHaveBeenCalledWith(`${BASE_URL}/`, { - method: "GET", - headers: {}, - extraHttpOptions: { - insecureHTTPParser: true, - }, + const { data } = await integration.read({}) + expect(data).toEqual({ foo: "bar" }) + }) + + it("doesn't fail when rejectUnauthorized is false", async () => { + nock("https://example.com").get("/").reply(200, { foo: "bar" }) + const integration = new RestIntegration({ + url: "https://example.com", + rejectUnauthorized: false, }) + const { data } = await integration.read({}) + expect(data).toEqual({ foo: "bar" }) }) }) - it("Attaches custom agent when Reject Unauthorized option is false", async () => { - config = new TestConfiguration({ - url: BASE_URL, - rejectUnauthorized: false, - }) - await config.integration.read({}) - - // @ts-ignore - const calls: any = fetch.mock.calls[0] - const url = calls[0] - expect(url).toBe(`${BASE_URL}/`) - - const calledConfig = calls[1] - expect(calledConfig.method).toBe("GET") - expect(calledConfig.headers).toEqual({}) - expect(calledConfig.agent.options.rejectUnauthorized).toBe(false) - }) - describe("File Handling", () => { it("uploads file to object store and returns signed URL", async () => { - const responseData = Buffer.from("teest file contnt") - const filename = "test.tar.gz" - const contentType = "application/gzip" - const mockReadable = new Readable() - mockReadable.push(responseData) - mockReadable.push(null) - ;(fetch as unknown as jest.Mock).mockImplementationOnce(() => - Promise.resolve({ - status: 200, - headers: { - raw: () => ({ - "content-type": [contentType], - "content-disposition": [`attachment; filename="${filename}"`], - }), - get: (header: any) => { - if (header === "content-type") return contentType - if (header === "content-length") return responseData.byteLength - if (header === "content-disposition") - return `attachment; filename="${filename}"` - }, - }, - body: mockReadable, + await config.doInContext(config.getAppId(), async () => { + const content = "test file content" + nock("https://example.com").get("/api").reply(200, content, { + "content-disposition": `attachment; filename="testfile.tar.gz"`, + "content-type": "text/plain", }) - ) - const query = { - path: "api", - } - - const response = await config.integration.read(query) - - expect(response.data).toEqual({ - size: responseData.byteLength, - name: "00000000-0000-0000-0000-000000000000.tar.gz", - url: expect.stringContaining( - "/files/signed/tmp-file-attachments/app-id/00000000-0000-0000-0000-000000000000.tar.gz" - ), - extension: "tar.gz", - key: expect.stringContaining( - "app-id/00000000-0000-0000-0000-000000000000.tar.gz" - ), + const { data } = await integration.read({ path: "api" }) + expect(data).toEqual({ + size: content.length, + name: expect.stringMatching(new RegExp(`^${UUID_REGEX}.tar.gz$`)), + url: expect.stringMatching( + new RegExp( + `^/files/signed/tmp-file-attachments/app.*?/${UUID_REGEX}.tar.gz.*$` + ) + ), + extension: "tar.gz", + key: expect.stringMatching( + new RegExp(`^app.*?/${UUID_REGEX}.tar.gz$`) + ), + }) }) }) it("uploads file with non ascii filename to object store and returns signed URL", async () => { - const responseData = Buffer.from("teest file contnt") - const contentType = "text/plain" - const mockReadable = new Readable() - mockReadable.push(responseData) - mockReadable.push(null) - ;(fetch as unknown as jest.Mock).mockImplementationOnce(() => - Promise.resolve({ - status: 200, - headers: { - raw: () => ({ - "content-type": [contentType], - "content-disposition": [ - // eslint-disable-next-line no-useless-escape - `attachment; filename="£ and ? rates.pdf"; filename*=UTF-8'\'%C2%A3%20and%20%E2%82%AC%20rates.pdf`, - ], - }), - get: (header: any) => { - if (header === "content-type") return contentType - if (header === "content-length") return responseData.byteLength - if (header === "content-disposition") - // eslint-disable-next-line no-useless-escape - return `attachment; filename="£ and ? rates.pdf"; filename*=UTF-8'\'%C2%A3%20and%20%E2%82%AC%20rates.pdf` - }, - }, - body: mockReadable, + await config.doInContext(config.getAppId(), async () => { + const content = "test file content" + nock("https://example.com").get("/api").reply(200, content, { + // eslint-disable-next-line no-useless-escape + "content-disposition": `attachment; filename="£ and ? rates.pdf"; filename*=UTF-8'\'%C2%A3%20and%20%E2%82%AC%20rates.pdf`, + "content-type": "text/plain", }) - ) - const query = { - path: "api", - } - - const response = await config.integration.read(query) - - expect(response.data).toEqual({ - size: responseData.byteLength, - name: "00000000-0000-0000-0000-000000000000.pdf", - url: expect.stringContaining( - "/files/signed/tmp-file-attachments/app-id/00000000-0000-0000-0000-000000000000.pdf" - ), - extension: "pdf", - key: expect.stringContaining( - "app-id/00000000-0000-0000-0000-000000000000.pdf" - ), + const { data } = await integration.read({ path: "api" }) + expect(data).toEqual({ + size: content.length, + name: expect.stringMatching(new RegExp(`^${UUID_REGEX}.pdf$`)), + url: expect.stringMatching( + new RegExp( + `^/files/signed/tmp-file-attachments/app.*?/${UUID_REGEX}.pdf.*$` + ) + ), + extension: "pdf", + key: expect.stringMatching(new RegExp(`^app.*?/${UUID_REGEX}.pdf$`)), + }) }) }) }) diff --git a/packages/types/src/documents/app/datasource.ts b/packages/types/src/documents/app/datasource.ts index e52019fc18..a0be7bd80d 100644 --- a/packages/types/src/documents/app/datasource.ts +++ b/packages/types/src/documents/app/datasource.ts @@ -45,15 +45,15 @@ export interface DynamicVariable { export interface RestConfig { url: string - rejectUnauthorized: boolean + rejectUnauthorized?: boolean downloadImages?: boolean - defaultHeaders: { + defaultHeaders?: { [key: string]: any } - legacyHttpParser: boolean - authConfigs: RestAuthConfig[] - staticVariables: { + legacyHttpParser?: boolean + authConfigs?: RestAuthConfig[] + staticVariables?: { [key: string]: string } - dynamicVariables: DynamicVariable[] + dynamicVariables?: DynamicVariable[] } diff --git a/packages/types/src/documents/app/query.ts b/packages/types/src/documents/app/query.ts index baba4def95..a545ca144e 100644 --- a/packages/types/src/documents/app/query.ts +++ b/packages/types/src/documents/app/query.ts @@ -36,31 +36,39 @@ export interface QueryResponse { pagination: any } +export enum BodyType { + NONE = "none", + FORM_DATA = "form", + XML = "xml", + ENCODED = "encoded", + JSON = "json", + TEXT = "text", +} + export interface RestQueryFields { - path: string + path?: string queryString?: string - headers: { [key: string]: any } - disabledHeaders: { [key: string]: any } - requestBody: any - bodyType: string - json: object - method: string - authConfigId: string - pagination: PaginationConfig | null - paginationValues: PaginationValues | null + headers?: { [key: string]: any } + disabledHeaders?: { [key: string]: any } + requestBody?: any + bodyType?: BodyType + method?: string + authConfigId?: string + pagination?: PaginationConfig + paginationValues?: PaginationValues } export interface PaginationConfig { type: string location: string pageParam: string - sizeParam: string | null - responseParam: string | null + sizeParam?: string + responseParam?: string } export interface PaginationValues { - page: string | number | null - limit: number | null + page?: string | number + limit?: number } export enum HttpMethod { diff --git a/yarn.lock b/yarn.lock index 2d69b37cc6..607db0b7bb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6707,22 +6707,39 @@ acorn-import-assertions@^1.9.0: resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz#507276249d684797c84e0734ef84860334cfb1ac" integrity sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA== -acorn-jsx@^5.3.2: +acorn-jsx-walk@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/acorn-jsx-walk/-/acorn-jsx-walk-2.0.0.tgz#a5ed648264e68282d7c2aead80216bfdf232573a" + integrity sha512-uuo6iJj4D4ygkdzd6jPtcxs8vZgDX9YFIkqczGImoypX2fQ4dVImmu3UzA4ynixCIMTrEOWW+95M2HuBaCEOVA== + +acorn-jsx@5.3.2, acorn-jsx@^5.3.2: version "5.3.2" resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== +acorn-loose@8.4.0: + version "8.4.0" + resolved "https://registry.yarnpkg.com/acorn-loose/-/acorn-loose-8.4.0.tgz#26d3e219756d1e180d006f5bcc8d261a28530f55" + integrity sha512-M0EUka6rb+QC4l9Z3T0nJEzNOO7JcoJlYMrBlyBCiFSXRyxjLKayd4TbQs2FDRWQU1h9FR7QVNHt+PEaoNL5rQ== + dependencies: + acorn "^8.11.0" + +acorn-walk@8.3.3, acorn-walk@^8.0.2, acorn-walk@^8.1.1, acorn-walk@^8.2.0, acorn-walk@^8.3.2: + version "8.3.3" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.3.tgz#9caeac29eefaa0c41e3d4c65137de4d6f34df43e" + integrity sha512-MxXdReSRhGO7VlFe1bRG/oI7/mdLV9B9JJT0N8vZOhF7gFRR5l3M8W9G8JxmKV+JC5mGqJ0QvqfSOLsCPa4nUw== + dependencies: + acorn "^8.11.0" + acorn-walk@^7.1.1: version "7.2.0" resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.2.0.tgz#0de889a601203909b0fbe07b8938dc21d2e967bc" integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA== -acorn-walk@^8.0.2, acorn-walk@^8.1.1, acorn-walk@^8.2.0, acorn-walk@^8.3.2: - version "8.3.3" - resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.3.tgz#9caeac29eefaa0c41e3d4c65137de4d6f34df43e" - integrity sha512-MxXdReSRhGO7VlFe1bRG/oI7/mdLV9B9JJT0N8vZOhF7gFRR5l3M8W9G8JxmKV+JC5mGqJ0QvqfSOLsCPa4nUw== - dependencies: - acorn "^8.11.0" +acorn@8.12.1: + version "8.12.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.12.1.tgz#71616bdccbe25e27a54439e0046e89ca76df2248" + integrity sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg== acorn@^5.2.1, acorn@^5.7.3: version "5.7.4" @@ -6791,6 +6808,16 @@ ajv-formats@^2.0.2: dependencies: ajv "^8.0.0" +ajv@8.17.1: + version "8.17.1" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.17.1.tgz#37d9a5c776af6bc92d7f4f9510eba4c0a60d11a6" + integrity sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g== + dependencies: + fast-deep-equal "^3.1.3" + fast-uri "^3.0.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + ajv@^6.12.3, ajv@^6.12.4: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" @@ -8484,6 +8511,11 @@ combos@^0.2.0: resolved "https://registry.yarnpkg.com/combos/-/combos-0.2.0.tgz#dc31c5a899b42293d55fe19c064d3e6e207ba4f7" integrity sha512-Z6YfvgiTCERWJTj3wQiXamFhssdvz1n4ok447rS330lw3uL72WAx8IvrLU7xiE71uyb5WF8JEP+BWB5KhOoGeg== +commander@12.1.0: + version "12.1.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-12.1.0.tgz#01423b36f501259fdaac4d0e4d60c96c991585d3" + integrity sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA== + commander@6.2.0: version "6.2.0" resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.0.tgz#b990bfb8ac030aedc6d11bc04d1488ffef56db75" @@ -9551,6 +9583,34 @@ depd@^1.1.0, depd@~1.1.2: resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ== +dependency-cruiser@^16.3.7: + version "16.3.10" + resolved "https://registry.yarnpkg.com/dependency-cruiser/-/dependency-cruiser-16.3.10.tgz#fe26a50d5e10a4496bc2b70d027fca6ded48814f" + integrity sha512-WkCnibHBfvaiaQ+S46LZ6h4AR6oj42Vsf5/0Vgtrwdwn7ZekMJdZ/ALoTwNp/RaGlKW+MbV/fhSZOvmhAWVWzQ== + dependencies: + acorn "8.12.1" + acorn-jsx "5.3.2" + acorn-jsx-walk "2.0.0" + acorn-loose "8.4.0" + acorn-walk "8.3.3" + ajv "8.17.1" + commander "12.1.0" + enhanced-resolve "5.17.1" + ignore "5.3.1" + interpret "^3.1.1" + is-installed-globally "1.0.0" + json5 "2.2.3" + memoize "10.0.0" + picocolors "1.0.1" + picomatch "4.0.2" + prompts "2.4.2" + rechoir "^0.8.0" + safe-regex "2.1.1" + semver "^7.6.3" + teamcity-service-messages "0.1.14" + tsconfig-paths-webpack-plugin "4.1.0" + watskeburt "4.1.0" + dependency-tree@^9.0.0: version "9.0.0" resolved "https://registry.yarnpkg.com/dependency-tree/-/dependency-tree-9.0.0.tgz#9288dd6daf35f6510c1ea30d9894b75369aa50a2" @@ -10221,6 +10281,14 @@ engine.io@~6.5.2: engine.io-parser "~5.2.1" ws "~8.17.1" +enhanced-resolve@5.17.1, enhanced-resolve@^5.7.0: + version "5.17.1" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz#67bfbbcc2f81d511be77d686a90267ef7f898a15" + integrity sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg== + dependencies: + graceful-fs "^4.2.4" + tapable "^2.2.0" + enhanced-resolve@^5.8.3: version "5.14.1" resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.14.1.tgz#de684b6803724477a4af5d74ccae5de52c25f6b3" @@ -11016,6 +11084,11 @@ fast-text-encoding@^1.0.0: resolved "https://registry.yarnpkg.com/fast-text-encoding/-/fast-text-encoding-1.0.6.tgz#0aa25f7f638222e3396d72bf936afcf1d42d6867" integrity sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w== +fast-uri@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.0.1.tgz#cddd2eecfc83a71c1be2cc2ef2061331be8a7134" + integrity sha512-MWipKbbYiYI0UC7cl8m/i/IWTqfC8YXsqjzybjddLsFjStroQzsHXkc73JutMvBiXmOvapk+axIl79ig5t55Bw== + fast-url-parser@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/fast-url-parser/-/fast-url-parser-1.1.3.tgz#f4af3ea9f34d8a271cf58ad2b3759f431f0b318d" @@ -11877,6 +11950,13 @@ global-agent@3.0.0: semver "^7.3.2" serialize-error "^7.0.1" +global-directory@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/global-directory/-/global-directory-4.0.1.tgz#4d7ac7cfd2cb73f304c53b8810891748df5e361e" + integrity sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q== + dependencies: + ini "4.1.1" + global-dirs@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-3.0.1.tgz#0c488971f066baceda21447aecb1a8b911d22485" @@ -12541,6 +12621,11 @@ ignore-walk@^6.0.0: dependencies: minimatch "^7.4.2" +ignore@5.3.1: + version "5.3.1" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.1.tgz#5073e554cd42c5b33b394375f538b8593e34d4ef" + integrity sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw== + ignore@^5.0.4, ignore@^5.2.0, ignore@^5.2.4: version "5.3.0" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.0.tgz#67418ae40d34d6999c95ff56016759c718c82f78" @@ -12666,6 +12751,11 @@ ini@2.0.0: resolved "https://registry.yarnpkg.com/ini/-/ini-2.0.0.tgz#e5fd556ecdd5726be978fa1001862eacb0a94bc5" integrity sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA== +ini@4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/ini/-/ini-4.1.1.tgz#d95b3d843b1e906e56d6747d5447904ff50ce7a1" + integrity sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g== + ini@^1.3.2, ini@^1.3.4, ini@^1.3.8, ini@~1.3.0: version "1.3.8" resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" @@ -12743,6 +12833,11 @@ interpret@^2.2.0: resolved "https://registry.yarnpkg.com/interpret/-/interpret-2.2.0.tgz#1a78a0b5965c40a5416d007ad6f50ad27c417df9" integrity sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw== +interpret@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/interpret/-/interpret-3.1.1.tgz#5be0ceed67ca79c6c4bc5cf0d7ee843dcea110c4" + integrity sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ== + into-stream@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/into-stream/-/into-stream-3.1.0.tgz#96fb0a936c12babd6ff1752a17d05616abd094c6" @@ -12973,6 +13068,14 @@ is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: dependencies: is-extglob "^2.1.1" +is-installed-globally@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-1.0.0.tgz#08952c43758c33d815692392f7f8437b9e436d5a" + integrity sha512-K55T22lfpQ63N4KEN57jZUAaAYqYHEe8veb/TycJRk9DdSCLLcovXz/mL6mOnhQaZsQGwPhuFopdQIlqGSEjiQ== + dependencies: + global-directory "^4.0.1" + is-path-inside "^4.0.0" + is-installed-globally@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.4.0.tgz#9a0fd407949c30f86eb6959ef1b7994ed0b7b520" @@ -13060,6 +13163,11 @@ is-path-inside@^3.0.2, is-path-inside@^3.0.3: resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== +is-path-inside@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-4.0.0.tgz#805aeb62c47c1b12fc3fd13bfb3ed1e7430071db" + integrity sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA== + is-plain-obj@^1.0.0, is-plain-obj@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" @@ -14084,6 +14192,11 @@ json-stringify-safe@^5.0.1, json-stringify-safe@~5.0.1: resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" integrity sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA== +json5@2.2.3, json5@^2.2.1, json5@^2.2.2, json5@^2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" + integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== + json5@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593" @@ -14091,11 +14204,6 @@ json5@^1.0.2: dependencies: minimist "^1.2.0" -json5@^2.2.1, json5@^2.2.2, json5@^2.2.3: - version "2.2.3" - resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" - integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== - jsonc-parser@3.2.0, jsonc-parser@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.2.0.tgz#31ff3f4c2b9793f89c67212627c51c6394f88e76" @@ -15441,6 +15549,13 @@ memdown@^5.1.0: ltgt "~2.2.0" safe-buffer "~5.2.0" +memoize@10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/memoize/-/memoize-10.0.0.tgz#43fa66b2022363c7c50cf5dfab732a808a3d7147" + integrity sha512-H6cBLgsi6vMWOcCpvVCdFFnl3kerEXbrYh9q+lY6VXvQSmM6CkmV08VOwT+WE2tzIEqRPFfAq3fm4v/UIW6mSA== + dependencies: + mimic-function "^5.0.0" + memory-pager@^1.0.2: version "1.5.0" resolved "https://registry.yarnpkg.com/memory-pager/-/memory-pager-1.5.0.tgz#d8751655d22d384682741c972f2c3d6dfa3e66b5" @@ -15549,6 +15664,11 @@ mimic-fn@^4.0.0: resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-4.0.0.tgz#60a90550d5cb0b239cca65d893b1a53b29871ecc" integrity sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw== +mimic-function@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/mimic-function/-/mimic-function-5.0.1.tgz#acbe2b3349f99b9deaca7fb70e48b83e94e67076" + integrity sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA== + mimic-response@^1.0.0, mimic-response@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b" @@ -17412,15 +17532,20 @@ phin@^2.9.1: resolved "https://registry.yarnpkg.com/phin/-/phin-2.9.3.tgz#f9b6ac10a035636fb65dfc576aaaa17b8743125c" integrity sha512-CzFr90qM24ju5f88quFC/6qohjC144rehe5n6DH900lgXmUe86+xCKc10ev56gRKC4/BkHUoG4uSiQgBiIXwDA== +picocolors@1.0.1, picocolors@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.1.tgz#a8ad579b571952f0e5d25892de5445bcfe25aaa1" + integrity sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew== + picocolors@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== -picocolors@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.1.tgz#a8ad579b571952f0e5d25892de5445bcfe25aaa1" - integrity sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew== +picomatch@4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.2.tgz#77c742931e8f3b8820946c76cd0c1f13730d1dab" + integrity sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg== picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2, picomatch@^2.2.3, picomatch@^2.3.1: version "2.3.1" @@ -18385,7 +18510,7 @@ promise.series@^0.2.0: resolved "https://registry.yarnpkg.com/promise.series/-/promise.series-0.2.0.tgz#2cc7ebe959fc3a6619c04ab4dbdc9e452d864bbd" integrity sha512-VWQJyU2bcDTgZw8kpfBpB/ejZASlCrzwz5f2hjb/zlujOEB4oeiAhHygAWq8ubsX2GVkD4kCU5V2dwOTaCY5EQ== -prompts@^2.0.1: +prompts@2.4.2, prompts@^2.0.1: version "2.4.2" resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069" integrity sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q== @@ -18952,6 +19077,11 @@ regenerator-transform@^0.15.1: dependencies: "@babel/runtime" "^7.8.4" +regexp-tree@~0.1.1: + version "0.1.27" + resolved "https://registry.yarnpkg.com/regexp-tree/-/regexp-tree-0.1.27.tgz#2198f0ef54518ffa743fe74d983b56ffd631b6cd" + integrity sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA== + regexp.prototype.flags@^1.4.3, regexp.prototype.flags@^1.5.2: version "1.5.2" resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz#138f644a3350f981a858c44f6bb1a61ff59be334" @@ -19492,6 +19622,13 @@ safe-regex-test@^1.0.3: es-errors "^1.3.0" is-regex "^1.1.4" +safe-regex@2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-2.1.1.tgz#f7128f00d056e2fe5c11e81a1324dd974aadced2" + integrity sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A== + dependencies: + regexp-tree "~0.1.1" + safe-stable-stringify@^2.1.0, safe-stable-stringify@^2.3.1: version "2.4.3" resolved "https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz#138c84b6f6edb3db5f8ef3ef7115b8f55ccbf886" @@ -19603,7 +19740,7 @@ semver-diff@^3.1.1: dependencies: semver "^6.3.0" -"semver@2 || 3 || 4 || 5", semver@7.5.3, semver@^5.5.0, semver@^5.6.0, semver@^5.7.1, semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.2.0, semver@^6.3.0, semver@^6.3.1, semver@^7.0.0, semver@^7.1.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.3, semver@^7.5.4, semver@~2.3.1: +"semver@2 || 3 || 4 || 5", semver@7.5.3, semver@^5.5.0, semver@^5.6.0, semver@^5.7.1, semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.2.0, semver@^6.3.0, semver@^6.3.1, semver@^7.0.0, semver@^7.1.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.3, semver@^7.5.4, semver@^7.6.3, semver@~2.3.1: version "7.5.3" resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.3.tgz#161ce8c2c6b4b3bdca6caadc9fa3317a4c4fe88e" integrity sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ== @@ -20867,6 +21004,11 @@ tarn@^3.0.1, tarn@^3.0.2: resolved "https://registry.yarnpkg.com/tarn/-/tarn-3.0.2.tgz#73b6140fbb881b71559c4f8bfde3d9a4b3d27693" integrity sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ== +teamcity-service-messages@0.1.14: + version "0.1.14" + resolved "https://registry.yarnpkg.com/teamcity-service-messages/-/teamcity-service-messages-0.1.14.tgz#193d420a5e4aef8e5e50b8c39e7865e08fbb5d8a" + integrity sha512-29aQwaHqm8RMX74u2o/h1KbMLP89FjNiMxD9wbF2BbWOnbM+q+d1sCEC+MqCc4QW3NJykn77OMpTFw/xTHIc0w== + tedious@^16.4.0: version "16.7.1" resolved "https://registry.yarnpkg.com/tedious/-/tedious-16.7.1.tgz#1190f30fd99a413f1dc9250dee4835cf0788b650" @@ -21258,6 +21400,15 @@ ts-node@10.8.1: v8-compile-cache-lib "^3.0.1" yn "3.1.1" +tsconfig-paths-webpack-plugin@4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-4.1.0.tgz#3c6892c5e7319c146eee1e7302ed9e6f2be4f763" + integrity sha512-xWFISjviPydmtmgeUAuXp4N1fky+VCtfhOkDUFIv5ea7p4wuTomI4QTrXvFBX2S4jZsmyTSrStQl+E+4w+RzxA== + dependencies: + chalk "^4.1.0" + enhanced-resolve "^5.7.0" + tsconfig-paths "^4.1.2" + tsconfig-paths@4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-4.0.0.tgz#1082f5d99fd127b72397eef4809e4dd06d229b64" @@ -22037,6 +22188,11 @@ walker@^1.0.8: dependencies: makeerror "1.0.12" +watskeburt@4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/watskeburt/-/watskeburt-4.1.0.tgz#3c0227669be646a97424b631164b1afe3d4d5344" + integrity sha512-KkY5H51ajqy9HYYI+u9SIURcWnqeVVhdH0I+ab6aXPGHfZYxgRCwnR6Lm3+TYB6jJVt5jFqw4GAKmwf1zHmGQw== + wcwidth@^1.0.0, wcwidth@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8"